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.
This commit is contained in:
Broque Thomas 2026-05-21 15:19:43 -07:00
parent 6c9b43225a
commit b6b83c8bf8
4 changed files with 124 additions and 15 deletions

View file

@ -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.';
}
});
}

View file

@ -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 = '<span class="adl-spinner" style="margin-right:4px"></span>';
} 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 = '<span class="adl-spinner" style="margin-right:4px"></span>';
} else if (batch.phase === 'downloading') {
phaseText = `${batch.completed}/${total} tracks`;
@ -2635,7 +2670,9 @@ function _adlRenderBatchPanel() {
</div>`;
}).join('');
} else {
tracksHtml = '<div style="font-size:0.7rem;color:rgba(255,255,255,0.3);padding:4px 0">No tracks loaded</div>';
tracksHtml = batch.phase === 'album_downloading'
? '<div class="adl-batch-release-note">Downloading one release first. Track matching starts after staging.</div>'
: '<div style="font-size:0.7rem;color:rgba(255,255,255,0.3);padding:4px 0">No tracks loaded</div>';
}
}

View file

@ -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%;

View file

@ -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 => `<span class="library-history-badge download">${escapeHtml(p)}</span>`).join('');
badge = parts.map(p => {
const cls = String(p || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
return `<span class="library-history-badge download source-${escapeHtml(cls)}">${escapeHtml(p)}</span>`;
}).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 = `<span class="library-history-badge import">${escapeHtml(sourceName)}</span>`;