From b6b83c8bf89559103d8a5b74153523babe80c145 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 21 May 2026 15:19:43 -0700 Subject: [PATCH] Polish torrent and usenet download UX Clarifies album-bundle progress text in the download modal and active downloads panel so release-first downloads read as downloading a release, then matching tracks after staging. Adds waiting-state copy and tooltips for rows blocked on release staging, plus source-specific library history badge styling for Torrent, Usenet, Staging, and Auto-Import. --- webui/static/downloads.js | 52 +++++++++++++++++++++++++++++----- webui/static/pages-extra.js | 51 ++++++++++++++++++++++++++++----- webui/static/style.css | 31 ++++++++++++++++++++ webui/static/wishlist-tools.js | 5 +++- 4 files changed, 124 insertions(+), 15 deletions(-) diff --git a/webui/static/downloads.js b/webui/static/downloads.js index 7df12aff..36142faf 100644 --- a/webui/static/downloads.js +++ b/webui/static/downloads.js @@ -3328,6 +3328,46 @@ function _downloadModalFormatSpeed(bytesPerSecond) { return formatted ? `${formatted}/s` : ''; } +function _downloadModalSourceLabel(source) { + const labels = { + torrent: 'Torrent', + usenet: 'Usenet', + soulseek: 'Soulseek', + youtube: 'YouTube', + tidal: 'Tidal', + qobuz: 'Qobuz', + hifi: 'HiFi', + deezer_dl: 'Deezer', + amazon: 'Amazon', + lidarr: 'Lidarr', + soundcloud: 'SoundCloud' + }; + const key = String(source || '').toLowerCase(); + return labels[key] || (source ? String(source) : 'Release'); +} + +function _downloadModalBundleStateLabel(state) { + const labels = { + searching: 'searching for release', + downloading: 'downloading release', + staged: 'matching tracks', + failed: 'release failed' + }; + const key = String(state || '').toLowerCase(); + return labels[key] || (state ? String(state).replace(/_/g, ' ') : 'downloading release'); +} + +function _downloadModalBundleProgressText(bundle) { + const percent = _downloadModalBundleProgressPercent(bundle); + const source = _downloadModalSourceLabel(bundle && bundle.source); + const state = _downloadModalBundleStateLabel(bundle && bundle.state); + const release = bundle && bundle.release ? ` - ${bundle.release}` : ''; + const speed = _downloadModalFormatSpeed(bundle && bundle.speed); + const size = _downloadModalFormatBytes(bundle && bundle.size); + const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : ''; + return `${source} ${state} ${percent}%${release}${detail}`; +} + function processModalStatusUpdate(playlistId, data) { // This function contains ALL the existing polling logic from startModalDownloadPolling // Extracted so it can be called from both individual and batched polling @@ -3384,16 +3424,12 @@ function processModalStatusUpdate(playlistId, data) { const bundle = data.album_bundle || {}; const percent = _downloadModalBundleProgressPercent(bundle); - const source = bundle.source ? `${bundle.source} ` : ''; - const release = bundle.release ? ` - ${bundle.release}` : ''; - const speed = _downloadModalFormatSpeed(bundle.speed); - const size = _downloadModalFormatBytes(bundle.size); - const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : ''; const downloadFill = document.getElementById(`download-progress-fill-${playlistId}`); const downloadText = document.getElementById(`download-progress-text-${playlistId}`); if (downloadFill) downloadFill.style.width = `${percent}%`; if (downloadText) { - downloadText.textContent = `${source}album ${bundle.state || 'downloading'} ${percent}%${release}${detail}`; + downloadText.textContent = _downloadModalBundleProgressText(bundle); + downloadText.title = 'SoulSync downloads one album release first, then matches the selected tracks from the staged files.'; } const modal = document.getElementById(`download-missing-modal-${playlistId}`); @@ -3401,7 +3437,9 @@ function processModalStatusUpdate(playlistId, data) { modal.querySelectorAll('[id^="download-"]').forEach(statusEl => { if (!statusEl.id.startsWith(`download-${playlistId}-`)) return; if (!statusEl.textContent || statusEl.textContent === '-' || statusEl.textContent.includes('Pending')) { - statusEl.textContent = 'Waiting for album bundle'; + statusEl.textContent = 'Waiting for release'; + statusEl.classList.add('album-bundle-waiting'); + statusEl.title = 'The album release is downloading first. Tracks will move to processing once SoulSync can match files from it.'; } }); } diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js index 915040f1..47a65710 100644 --- a/webui/static/pages-extra.js +++ b/webui/static/pages-extra.js @@ -2473,6 +2473,46 @@ function _adlFormatSpeed(bytesPerSecond) { return formatted ? `${formatted}/s` : ''; } +function _adlSourceLabel(source) { + const labels = { + torrent: 'Torrent', + usenet: 'Usenet', + soulseek: 'Soulseek', + youtube: 'YouTube', + tidal: 'Tidal', + qobuz: 'Qobuz', + hifi: 'HiFi', + deezer_dl: 'Deezer', + amazon: 'Amazon', + lidarr: 'Lidarr', + soundcloud: 'SoundCloud' + }; + const key = String(source || '').toLowerCase(); + return labels[key] || (source ? String(source) : 'Release'); +} + +function _adlBundleStateLabel(state) { + const labels = { + searching: 'searching for release', + downloading: 'downloading release', + staged: 'matching tracks', + failed: 'release failed' + }; + const key = String(state || '').toLowerCase(); + return labels[key] || (state ? String(state).replace(/_/g, ' ') : 'downloading release'); +} + +function _adlBundleProgressText(bundle) { + const pct = _adlBundleProgressPercent(bundle); + const source = _adlSourceLabel(bundle && bundle.source); + const state = _adlBundleStateLabel(bundle && bundle.state); + const release = bundle && bundle.release ? ` - ${bundle.release}` : ''; + const speed = _adlFormatSpeed(bundle && bundle.speed); + const size = _adlFormatBytes(bundle && bundle.size); + const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : ''; + return `${source} ${state} ${pct}%${release}${detail}`; +} + async function adlClearCompleted() { try { const resp = await fetch('/api/downloads/clear-completed', { method: 'POST' }); @@ -2565,12 +2605,7 @@ function _adlRenderBatchPanel() { phaseText = 'Analyzing...'; phaseIcon = ''; } else if (batch.phase === 'album_downloading') { - const source = albumBundle && albumBundle.source ? `${albumBundle.source} ` : ''; - const release = albumBundle && albumBundle.release ? ` - ${albumBundle.release}` : ''; - const speed = _adlFormatSpeed(albumBundle && albumBundle.speed); - const size = _adlFormatBytes(albumBundle && albumBundle.size); - const detail = speed || size ? ` (${[speed, size].filter(Boolean).join(' of ')})` : ''; - phaseText = `${source}album ${albumBundle && albumBundle.state ? albumBundle.state : 'downloading'} ${bundleProgress}%${release}${detail}`; + phaseText = _adlBundleProgressText(albumBundle); phaseIcon = ''; } else if (batch.phase === 'downloading') { phaseText = `${batch.completed}/${total} tracks`; @@ -2635,7 +2670,9 @@ function _adlRenderBatchPanel() { `; }).join(''); } else { - tracksHtml = '
No tracks loaded
'; + tracksHtml = batch.phase === 'album_downloading' + ? '
Downloading one release first. Track matching starts after staging.
' + : '
No tracks loaded
'; } } diff --git a/webui/static/style.css b/webui/static/style.css index cad3e987..d6c531ed 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -10333,6 +10333,25 @@ body.helper-mode-active #dashboard-activity-feed:hover { border: 1px solid rgba(100, 200, 130, 0.2); } +.library-history-badge.download.source-torrent { + color: #5dade2; + background: rgba(93, 173, 226, 0.1); + border-color: rgba(93, 173, 226, 0.25); +} + +.library-history-badge.download.source-usenet { + color: #a78bfa; + background: rgba(167, 139, 250, 0.1); + border-color: rgba(167, 139, 250, 0.25); +} + +.library-history-badge.download.source-staging, +.library-history-badge.download.source-auto-import { + color: rgba(255, 255, 255, 0.55); + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.12); +} + .library-history-badge.import { color: rgba(120, 160, 230, 0.9); background: rgba(120, 160, 230, 0.1); @@ -19885,6 +19904,11 @@ body.helper-mode-active #dashboard-activity-feed:hover { inset 0 1px 2px rgba(255, 255, 255, 0.3); } +.album-bundle-waiting { + color: rgba(255, 255, 255, 0.55); + font-style: italic; +} + /* Track Table Section */ .download-tracks-section { /* Premium glassmorphic card matching other sections */ @@ -58357,6 +58381,13 @@ body.reduce-effects *::after { .adl-batch-track-status.failed { color: #ef4444; } .adl-batch-track-status.queued { color: rgba(255, 255, 255, 0.3); } +.adl-batch-release-note { + padding: 6px 0; + color: rgba(255, 255, 255, 0.38); + font-size: 0.7rem; + line-height: 1.4; +} + /* Mini progress bar per track */ .adl-batch-track-progress { width: 100%; diff --git a/webui/static/wishlist-tools.js b/webui/static/wishlist-tools.js index 3807102a..fd8eee6b 100644 --- a/webui/static/wishlist-tools.js +++ b/webui/static/wishlist-tools.js @@ -3449,7 +3449,10 @@ function renderHistoryEntry(entry) { const parts = []; if (entry.download_source) parts.push(entry.download_source); if (entry.quality) parts.push(entry.quality); - badge = parts.map(p => `${escapeHtml(p)}`).join(''); + badge = parts.map(p => { + const cls = String(p || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + return `${escapeHtml(p)}`; + }).join(''); } else if (entry.event_type === 'import' && entry.server_source) { const sourceName = { plex: 'Plex', jellyfin: 'Jellyfin', navidrome: 'Navidrome' }[entry.server_source] || entry.server_source; badge = `${escapeHtml(sourceName)}`;