feat: implemented unified 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, #btn-monochrome { width: 100%; border-radius: var(--radius); }
|
||||
#btn-download, #btn-monochrome, #btn-unified { 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; }
|
||||
@@ -154,10 +154,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1 style="cursor:pointer" onclick="showPage('download')"><span>Votify</span> Web</h1>
|
||||
<h1 style="cursor:pointer" onclick="showPage('unified')"><span>Votify</span> Web</h1>
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="showPage('download')">Votify</button>
|
||||
<button class="tab" onclick="showPage('monochrome')">Monochrome</button>
|
||||
<button class="tab active" onclick="showPage('unified')">Download</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>
|
||||
@@ -165,9 +164,26 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- DOWNLOAD PAGE -->
|
||||
<div id="page-download" class="page active">
|
||||
<!-- UNIFIED DOWNLOAD PAGE -->
|
||||
<div id="page-unified" class="page active">
|
||||
<div class="card">
|
||||
<h2>Download</h2>
|
||||
<div class="form-group">
|
||||
<label for="unified-urls">Spotify URLs (one per line)</label>
|
||||
<textarea id="unified-urls" placeholder="https://open.spotify.com/track/... https://open.spotify.com/album/..."></textarea>
|
||||
</div>
|
||||
<button class="btn" id="btn-unified" onclick="startUnifiedDownload()">Download</button>
|
||||
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
||||
<button class="btn btn-sm btn-secondary" onclick="showPage('download')">Votify (Spotify)</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="showPage('monochrome')">Monochrome (Tidal)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VOTIFY PAGE (hidden, accessible via button) -->
|
||||
<div id="page-download" class="page">
|
||||
<div class="card">
|
||||
<button class="btn btn-sm btn-secondary" onclick="showPage('unified')" style="margin-bottom:12px">← Back</button>
|
||||
<h2>Votify Download</h2>
|
||||
<div class="form-group">
|
||||
<label for="urls">Spotify URLs (one per line)</label>
|
||||
@@ -239,6 +255,7 @@
|
||||
<!-- MONOCHROME PAGE -->
|
||||
<div id="page-monochrome" class="page">
|
||||
<div class="card">
|
||||
<button class="btn btn-sm btn-secondary" onclick="showPage('unified')" style="margin-bottom:12px">← Back</button>
|
||||
<h2>Monochrome Download</h2>
|
||||
<div class="form-group">
|
||||
<label for="mono-url">Spotify URL (track, album, or playlist)</label>
|
||||
@@ -285,6 +302,14 @@
|
||||
<a href="/logout" class="btn btn-danger btn-sm" style="text-decoration:none">Logout</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h2>Fallback Quality</h2>
|
||||
<p style="font-size:0.85rem; color:var(--text2); margin-bottom:12px">Quality for Votify fallback when Monochrome can't find a track</p>
|
||||
<select id="fallback-quality">
|
||||
<option value="aac-medium">AAC 128kbps</option>
|
||||
<option value="aac-high">AAC 256kbps (Premium)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Cookies</h2>
|
||||
<div class="cookie-status" id="cookie-status">
|
||||
@@ -313,13 +338,9 @@
|
||||
</div>
|
||||
|
||||
<nav id="bottom-nav">
|
||||
<button class="bottom-tab active" data-page="download" onclick="showPage('download')">
|
||||
<button class="bottom-tab active" data-page="unified" onclick="showPage('unified')">
|
||||
<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>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>
|
||||
<span>Download</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>
|
||||
@@ -346,13 +367,16 @@
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('page-' + name).classList.add('active');
|
||||
const matchingTab = document.querySelector(`.tab[onclick="showPage('${name}')"]`);
|
||||
|
||||
// For hidden pages (download/monochrome), highlight the "Download" tab
|
||||
const tabName = (name === 'download' || name === 'monochrome') ? 'unified' : name;
|
||||
const matchingTab = document.querySelector(`.tab[onclick="showPage('${tabName}')"]`);
|
||||
if (matchingTab) matchingTab.classList.add('active');
|
||||
document.querySelectorAll('.bottom-tab').forEach(t => t.classList.toggle('active', t.dataset.page === name));
|
||||
document.querySelectorAll('.bottom-tab').forEach(t => t.classList.toggle('active', t.dataset.page === tabName));
|
||||
|
||||
if (name === 'jobs') loadJobs();
|
||||
if (name === 'files') loadFiles("");
|
||||
if (name === 'settings') { checkCookies(); checkWvd(); }
|
||||
if (name === 'settings') { checkCookies(); checkWvd(); loadFallbackQuality(); }
|
||||
|
||||
if (jobPollInterval) clearInterval(jobPollInterval);
|
||||
if (name === 'jobs') jobPollInterval = setInterval(loadJobs, 3000);
|
||||
@@ -387,7 +411,7 @@
|
||||
const result = await res.json();
|
||||
if (res.ok) {
|
||||
document.getElementById('urls').value = '';
|
||||
document.querySelector('[onclick="showPage(\'jobs\')"]').click();
|
||||
showPage('jobs');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to start download', 'error');
|
||||
}
|
||||
@@ -425,6 +449,51 @@
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
async function startUnifiedDownload() {
|
||||
const btn = document.getElementById('btn-unified');
|
||||
btn.disabled = true;
|
||||
const url = document.getElementById('unified-urls').value.trim();
|
||||
if (!url) { showToast('Enter a Spotify URL', 'error'); btn.disabled = false; return; }
|
||||
try {
|
||||
const res = await fetch('/api/unified/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok) {
|
||||
document.getElementById('unified-urls').value = '';
|
||||
showPage('jobs');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to start download', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Error: ' + e.message, 'error');
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
async function loadFallbackQuality() {
|
||||
try {
|
||||
const res = await fetch('/api/settings');
|
||||
const data = await res.json();
|
||||
document.getElementById('fallback-quality').value = data.fallback_quality || 'aac-medium';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
document.getElementById('fallback-quality').addEventListener('change', async function() {
|
||||
try {
|
||||
await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fallback_quality: this.value })
|
||||
});
|
||||
showToast('Fallback quality saved');
|
||||
} catch (e) {
|
||||
showToast('Error saving setting', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
function parseProgress(output) {
|
||||
if (!output || output.length === 0) return null;
|
||||
let current = 0, total = 0, dlPct = 0;
|
||||
@@ -724,22 +793,23 @@
|
||||
if (!lines.length) return;
|
||||
// Drop onto active page's textarea
|
||||
const monoActive = document.getElementById('page-monochrome').classList.contains('active');
|
||||
const ta = document.getElementById(monoActive ? 'mono-url' : 'urls');
|
||||
const votifyActive = document.getElementById('page-download').classList.contains('active');
|
||||
let taId = 'unified-urls';
|
||||
if (monoActive) taId = 'mono-url';
|
||||
else if (votifyActive) taId = 'urls';
|
||||
const ta = document.getElementById(taId);
|
||||
ta.value = (ta.value.trim() ? ta.value.trim() + '\n' : '') + lines.join('\n');
|
||||
ta.classList.remove('drop-flash');
|
||||
void ta.offsetWidth;
|
||||
ta.classList.add('drop-flash');
|
||||
setTimeout(() => ta.classList.remove('drop-flash'), 650);
|
||||
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'));
|
||||
for (const id of ['urls', 'mono-url', 'unified-urls']) {
|
||||
const ta = document.getElementById(id);
|
||||
ta.addEventListener('dragover', () => ta.classList.add('drag-over'));
|
||||
ta.addEventListener('dragleave', () => ta.classList.remove('drag-over'));
|
||||
ta.addEventListener('drop', () => ta.classList.remove('drag-over'));
|
||||
}
|
||||
|
||||
// Remember settings
|
||||
const SETTINGS_KEY = 'votify-settings';
|
||||
|
||||
Reference in New Issue
Block a user