feat: implemented Monochrome downloading
This commit is contained in:
@@ -145,7 +145,7 @@
|
||||
.card { padding: 16px; }
|
||||
body { padding-bottom: 64px; }
|
||||
textarea, select, input { font-size: 1rem; }
|
||||
#btn-download { width: 100%; border-radius: var(--radius); }
|
||||
#btn-download, #btn-monochrome { width: 100%; border-radius: var(--radius); }
|
||||
.job-preview { max-width: calc(100vw - 120px); }
|
||||
#toast { left: 16px; right: 16px; bottom: 72px; max-width: none; }
|
||||
#bottom-nav { display: flex; }
|
||||
@@ -156,7 +156,8 @@
|
||||
<div class="header">
|
||||
<h1 style="cursor:pointer" onclick="showPage('download')"><span>Votify</span> Web</h1>
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="showPage('download')">Download</button>
|
||||
<button class="tab active" onclick="showPage('download')">Votify</button>
|
||||
<button class="tab" onclick="showPage('monochrome')">Monochrome</button>
|
||||
<button class="tab" onclick="showPage('jobs')">Jobs</button>
|
||||
<button class="tab" onclick="showPage('files')">Files</button>
|
||||
<button class="tab" onclick="showPage('settings')">Settings</button>
|
||||
@@ -167,7 +168,7 @@
|
||||
<!-- DOWNLOAD PAGE -->
|
||||
<div id="page-download" class="page active">
|
||||
<div class="card">
|
||||
<h2>New Download</h2>
|
||||
<h2>Votify Download</h2>
|
||||
<div class="form-group">
|
||||
<label for="urls">Spotify URLs (one per line)</label>
|
||||
<textarea id="urls" placeholder="https://open.spotify.com/track/... https://open.spotify.com/album/..."></textarea>
|
||||
@@ -235,6 +236,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MONOCHROME PAGE -->
|
||||
<div id="page-monochrome" class="page">
|
||||
<div class="card">
|
||||
<h2>Monochrome Download</h2>
|
||||
<div class="form-group">
|
||||
<label for="mono-url">Spotify URL (track, album, or playlist)</label>
|
||||
<textarea id="mono-url" placeholder="https://open.spotify.com/track/... https://open.spotify.com/album/..." style="min-height:60px"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label for="mono-quality">Quality</label>
|
||||
<select id="mono-quality">
|
||||
<option value="HI_RES_LOSSLESS">HI_RES_LOSSLESS (24-bit FLAC)</option>
|
||||
<option value="LOSSLESS">LOSSLESS (16-bit FLAC)</option>
|
||||
<option value="HIGH">HIGH (AAC 320kbps)</option>
|
||||
<option value="LOW">LOW (AAC 96kbps)</option>
|
||||
<option value="MP3_320">MP3 320kbps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" id="btn-monochrome" onclick="startMonochromeDownload()">Start Download</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JOBS PAGE -->
|
||||
<div id="page-jobs" class="page">
|
||||
<div class="card">
|
||||
@@ -290,7 +315,11 @@
|
||||
<nav id="bottom-nav">
|
||||
<button class="bottom-tab active" data-page="download" onclick="showPage('download')">
|
||||
<span class="btab-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg></span>
|
||||
<span>Download</span>
|
||||
<span>Votify</span>
|
||||
</button>
|
||||
<button class="bottom-tab" data-page="monochrome" onclick="showPage('monochrome')">
|
||||
<span class="btab-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="15.5" r="2.5"/><path d="M8 17V5l12-2v12"/></svg></span>
|
||||
<span>Mono</span>
|
||||
</button>
|
||||
<button class="bottom-tab" data-page="jobs" onclick="showPage('jobs')">
|
||||
<span class="btab-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg></span>
|
||||
@@ -368,19 +397,56 @@
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
async function startMonochromeDownload() {
|
||||
const btn = document.getElementById('btn-monochrome');
|
||||
btn.disabled = true;
|
||||
|
||||
const url = document.getElementById('mono-url').value.trim();
|
||||
const quality = document.getElementById('mono-quality').value;
|
||||
|
||||
localStorage.setItem('mono-quality', quality);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/monochrome/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url, quality })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok) {
|
||||
document.getElementById('mono-url').value = '';
|
||||
showPage('jobs');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to start download', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Error: ' + e.message, 'error');
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
function parseProgress(output) {
|
||||
if (!output || output.length === 0) return null;
|
||||
let current = 0, total = 0, dlPct = 0;
|
||||
let mp3Total = 0, mp3Done = 0, inMp3 = false;
|
||||
let monoTrack = 0, monoTotal = 0, isMono = false;
|
||||
for (const line of output) {
|
||||
const tm = line.match(/Track (\d+)\/(\d+)/);
|
||||
if (tm) { current = parseInt(tm[1]); total = parseInt(tm[2]); }
|
||||
const mono = line.match(/\[monochrome\] Track (\d+)\/(\d+)/);
|
||||
if (mono) { monoTrack = parseInt(mono[1]); monoTotal = parseInt(mono[2]); isMono = true; }
|
||||
if (!isMono) {
|
||||
const tm = line.match(/Track (\d+)\/(\d+)/);
|
||||
if (tm) { current = parseInt(tm[1]); total = parseInt(tm[2]); }
|
||||
}
|
||||
const dm = line.match(/\[download\]\s+([\d.]+)%/);
|
||||
if (dm) dlPct = parseFloat(dm[1]);
|
||||
const mm = line.match(/\[mp3\] Converting (\d+) file/);
|
||||
if (mm) { mp3Total = parseInt(mm[1]); inMp3 = true; }
|
||||
if (/\[mp3\] (Done:|Failed:)/.test(line)) mp3Done++;
|
||||
}
|
||||
if (isMono && monoTotal > 0) {
|
||||
const pct = (monoTrack / monoTotal) * 100;
|
||||
return { current: monoTrack, total: monoTotal, pct: Math.min(Math.round(pct), 100), phase: 'monochrome' };
|
||||
}
|
||||
if (inMp3 && mp3Total > 0) {
|
||||
const pct = (mp3Done / mp3Total) * 100;
|
||||
return { current: mp3Done, total: mp3Total, pct: Math.min(Math.round(pct), 100), phase: 'mp3' };
|
||||
@@ -420,6 +486,8 @@
|
||||
if (progress) {
|
||||
const label = progress.phase === 'mp3'
|
||||
? `Converting to MP3 ${progress.current}/${progress.total}`
|
||||
: progress.phase === 'monochrome'
|
||||
? `Monochrome ${progress.current}/${progress.total}`
|
||||
: `Downloading ${progress.current}/${progress.total}`;
|
||||
progressHtml = `<div style="font-size:0.75rem;color:var(--text2);margin-bottom:4px">${label}</div>
|
||||
<div class="job-progress"><div class="job-progress-bar" style="width:${progress.pct}%"></div></div>`;
|
||||
@@ -654,18 +722,24 @@
|
||||
const text = e.dataTransfer.getData('text') || e.dataTransfer.getData('text/uri-list') || '';
|
||||
const lines = text.split(/[\r\n\s]+/).filter(l => l.includes('open.spotify.com'));
|
||||
if (!lines.length) return;
|
||||
const ta = document.getElementById('urls');
|
||||
// Drop onto active page's textarea
|
||||
const monoActive = document.getElementById('page-monochrome').classList.contains('active');
|
||||
const ta = document.getElementById(monoActive ? 'mono-url' : 'urls');
|
||||
ta.value = (ta.value.trim() ? ta.value.trim() + '\n' : '') + lines.join('\n');
|
||||
ta.classList.remove('drop-flash');
|
||||
void ta.offsetWidth; // reflow to restart animation
|
||||
void ta.offsetWidth;
|
||||
ta.classList.add('drop-flash');
|
||||
setTimeout(() => ta.classList.remove('drop-flash'), 650);
|
||||
showPage('download');
|
||||
if (!monoActive) showPage('download');
|
||||
});
|
||||
const urlTextarea = document.getElementById('urls');
|
||||
urlTextarea.addEventListener('dragover', () => urlTextarea.classList.add('drag-over'));
|
||||
urlTextarea.addEventListener('dragleave', () => urlTextarea.classList.remove('drag-over'));
|
||||
urlTextarea.addEventListener('drop', () => urlTextarea.classList.remove('drag-over'));
|
||||
const monoTextarea = document.getElementById('mono-url');
|
||||
monoTextarea.addEventListener('dragover', () => monoTextarea.classList.add('drag-over'));
|
||||
monoTextarea.addEventListener('dragleave', () => monoTextarea.classList.remove('drag-over'));
|
||||
monoTextarea.addEventListener('drop', () => monoTextarea.classList.remove('drag-over'));
|
||||
|
||||
// Remember settings
|
||||
const SETTINGS_KEY = 'votify-settings';
|
||||
@@ -694,6 +768,8 @@
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
const savedMonoQ = localStorage.getItem('mono-quality');
|
||||
if (savedMonoQ) document.getElementById('mono-quality').value = savedMonoQ;
|
||||
if (localStorage.getItem('votify-advanced-open')) {
|
||||
document.getElementById('advanced-section').classList.add('open');
|
||||
document.getElementById('advanced-toggle').classList.add('open');
|
||||
|
||||
Reference in New Issue
Block a user