feat: implemented Monochrome downloading

This commit is contained in:
2026-03-08 20:14:30 +01:00
parent 6dff83ac61
commit f4dee850f3
8 changed files with 1157 additions and 9 deletions

View File

@@ -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/...&#10;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/...&#10;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');