From daaed373e71209f9cfa9035a412053d2ea7916e1 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 20 May 2026 19:31:47 -0700 Subject: [PATCH] fix(provenance): label torrent/usenet/staging downloads correctly in history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The download history modal was tagging every torrent / usenet album-bundle download as 'Soulseek FLAC 24bit' because: - core/imports/side_effects.py's source_service dict didn't have entries for 'staging', 'torrent', or 'usenet' usernames. The staging matcher in core/downloads/staging.py sets download_tasks[task_id]['username'] = 'staging', which fell through to the dict's default and got recorded as 'soulseek' in the track download provenance row. Same fate for any amazon or other source that wasn't whitelisted. - The album-bundle flow specifically wants to be labeled as 'torrent' or 'usenet' (where the bytes actually came from), not 'staging' (the intermediate). The plugin already stashes the source on the batch state as ``album_bundle_source`` for the Downloads-page status card; provenance recording can read the same field. Fixes: - core/downloads/staging.py: when marking a task post_processing after a staging match, check the batch's album_bundle_source override and use that for username instead of 'staging' when set. Falls back to 'staging' when no override exists (manual file-drop case). - core/imports/side_effects.py: source_service map gets entries for 'staging', 'torrent', 'usenet', and the previously-missing 'amazon' (which was also falling through to 'soulseek'). - webui/static/library.js: the redownload modal's serviceLabels / serviceIcons dicts extended to cover lidarr, amazon, soundcloud, auto_import, staging, torrent, usenet so badges render the correct name instead of either the raw source_service string or no badge at all. - webui/static/wishlist-tools.js: history-source-chip color palette extended for the new source labels (Torrent sky-blue, Usenet violet, Staging / Auto-Import neutral grey). Note: existing tracks in the DB still carry the wrong 'soulseek' label — only NEW downloads after this fix get the right label. A future migration could rewrite historical rows but it's cosmetic and the underlying audio + metadata are correct. --- core/downloads/staging.py | 16 ++++++++++++++-- core/imports/side_effects.py | 11 +++++++++++ webui/static/library.js | 12 ++++++------ webui/static/wishlist-tools.js | 2 +- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/core/downloads/staging.py b/core/downloads/staging.py index 12de4a16..a8dda6c3 100644 --- a/core/downloads/staging.py +++ b/core/downloads/staging.py @@ -141,12 +141,24 @@ def try_staging_match(task_id, batch_id, track, deps: StagingDeps): shutil.copy2(best_match['full_path'], dest_path) logger.info(f"[Staging] Copied to transfer: {dest_path}") - # Mark task as completed with staging context + # Mark task as completed with staging context. + # If the batch was populated by the torrent / usenet album-bundle + # flow, prefer that provenance label over generic 'staging' so the + # download history reflects the real source. + _provenance_override = None + try: + from core.runtime_state import download_batches as _db + _batch = _db.get(batch_id) if batch_id else None + if isinstance(_batch, dict): + _provenance_override = _batch.get('album_bundle_source') + except Exception: + _provenance_override = None + _provenance_username = _provenance_override or 'staging' with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['status'] = 'post_processing' download_tasks[task_id]['filename'] = dest_path - download_tasks[task_id]['username'] = 'staging' + download_tasks[task_id]['username'] = _provenance_username download_tasks[task_id]['staging_match'] = True # Run post-processing (tagging, AcoustID verification, path building) diff --git a/core/imports/side_effects.py b/core/imports/side_effects.py index 9b3d9f3a..d1580564 100644 --- a/core/imports/side_effects.py +++ b/core/imports/side_effects.py @@ -276,6 +276,7 @@ def record_download_provenance(context: Dict[str, Any]) -> None: "deezer_dl": "deezer", "lidarr": "lidarr", "soundcloud": "soundcloud", + "amazon": "amazon", # Auto-import: surfaced in provenance so the redownload modal # can tell the user "this came from staging on " instead # of falsely listing soulseek as the source. The underlying @@ -283,6 +284,16 @@ def record_download_provenance(context: Dict[str, Any]) -> None: # separately via the source-aware ID columns on the tracks # row itself. "auto_import": "auto_import", + # Generic staging-match (user dropped files manually OR a + # source we don't have a more specific label for). Better + # than defaulting to 'soulseek' which would falsely tag the + # provenance. + "staging": "staging", + # Torrent / usenet album-bundle flow — the staging matcher + # overrides 'staging' with the bundle source so the history + # shows where the files actually came from. + "torrent": "torrent", + "usenet": "usenet", }.get(username, "soulseek") ti = context.get("track_info") or context.get("search_result") or {} diff --git a/webui/static/library.js b/webui/static/library.js index f8ed0960..cbbcb74e 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -4595,8 +4595,8 @@ async function showTrackSourceInfo(track, anchorEl) { return; } - const serviceIcons = { soulseek: '🔍', youtube: '▶️', tidal: '🌊', qobuz: '🎵', hifi: '🎧', deezer: '💜' }; - const serviceLabels = { soulseek: 'Soulseek', youtube: 'YouTube', tidal: 'Tidal', qobuz: 'Qobuz', hifi: 'HiFi', deezer: 'Deezer' }; + const serviceIcons = { soulseek: '🔍', youtube: '▶️', tidal: '🌊', qobuz: '🎵', hifi: '🎧', deezer: '💜', lidarr: '📦', amazon: '🛒', soundcloud: '☁️', auto_import: '📥', staging: '📥', torrent: '🧲', usenet: '📰' }; + const serviceLabels = { soulseek: 'Soulseek', youtube: 'YouTube', tidal: 'Tidal', qobuz: 'Qobuz', hifi: 'HiFi', deezer: 'Deezer', lidarr: 'Lidarr', amazon: 'Amazon Music', soundcloud: 'SoundCloud', auto_import: 'Auto-Import', staging: 'Staging', torrent: 'Torrent', usenet: 'Usenet' }; const dl = data.downloads[0]; // Most recent download const icon = serviceIcons[dl.source_service] || '📦'; @@ -4926,8 +4926,8 @@ async function _streamRedownloadSources(overlay, track, metadata) { const startBtn = document.getElementById('redownload-start-btn'); if (!columnsEl) return; - const serviceIcons = { soulseek: '🔍', youtube: '▶️', tidal: '🌊', qobuz: '🎵', hifi: '🎧', deezer_dl: '💜', hybrid: '⚡' }; - const serviceLabels = { soulseek: 'Soulseek', youtube: 'YouTube', tidal: 'Tidal', qobuz: 'Qobuz', hifi: 'HiFi', deezer_dl: 'Deezer', hybrid: 'Auto' }; + const serviceIcons = { soulseek: '🔍', youtube: '▶️', tidal: '🌊', qobuz: '🎵', hifi: '🎧', deezer_dl: '💜', hybrid: '⚡', lidarr: '📦', amazon: '🛒', soundcloud: '☁️', torrent: '🧲', usenet: '📰' }; + const serviceLabels = { soulseek: 'Soulseek', youtube: 'YouTube', tidal: 'Tidal', qobuz: 'Qobuz', hifi: 'HiFi', deezer_dl: 'Deezer', hybrid: 'Auto', lidarr: 'Lidarr', amazon: 'Amazon Music', soundcloud: 'SoundCloud', torrent: 'Torrent', usenet: 'Usenet' }; let allCandidates = []; let firstResult = true; @@ -5049,8 +5049,8 @@ async function _streamRedownloadSources(overlay, track, metadata) { /* _renderRedownloadStep2 removed — replaced by _streamRedownloadSources above */ if (false) { - const serviceIcons = { soulseek: '🔍', youtube: '▶️', tidal: '🌊', qobuz: '🎵', hifi: '🎧', deezer_dl: '💜', hybrid: '⚡' }; - const serviceLabels = { soulseek: 'Soulseek', youtube: 'YouTube', tidal: 'Tidal', qobuz: 'Qobuz', hifi: 'HiFi', deezer_dl: 'Deezer', hybrid: 'Auto' }; + const serviceIcons = { soulseek: '🔍', youtube: '▶️', tidal: '🌊', qobuz: '🎵', hifi: '🎧', deezer_dl: '💜', hybrid: '⚡', lidarr: '📦', amazon: '🛒', soundcloud: '☁️', torrent: '🧲', usenet: '📰' }; + const serviceLabels = { soulseek: 'Soulseek', youtube: 'YouTube', tidal: 'Tidal', qobuz: 'Qobuz', hifi: 'HiFi', deezer_dl: 'Deezer', hybrid: 'Auto', lidarr: 'Lidarr', amazon: 'Amazon Music', soundcloud: 'SoundCloud', torrent: 'Torrent', usenet: 'Usenet' }; // Group candidates by source service const grouped = {}; diff --git a/webui/static/wishlist-tools.js b/webui/static/wishlist-tools.js index 1a50bf19..3807102a 100644 --- a/webui/static/wishlist-tools.js +++ b/webui/static/wishlist-tools.js @@ -3406,7 +3406,7 @@ async function loadLibraryHistory() { const sc = data.stats?.source_counts || {}; const srcEntries = Object.entries(sc).sort((a, b) => b[1] - a[1]); if (srcEntries.length > 0 && tab === 'download') { - const _srcColors = { Soulseek: '#4caf50', Tidal: '#000', YouTube: '#ff0000', Qobuz: '#4285f4', HiFi: '#00bcd4', Deezer: '#a238ff' }; + const _srcColors = { Soulseek: '#4caf50', Tidal: '#000', YouTube: '#ff0000', Qobuz: '#4285f4', HiFi: '#00bcd4', Deezer: '#a238ff', Lidarr: '#5dade2', Amazon: '#ff9900', SoundCloud: '#ff7700', Torrent: '#5dade2', Usenet: '#a78bfa', Staging: '#888', 'Auto-Import': '#888' }; sourceBar.innerHTML = srcEntries.map(([src, cnt]) => `${src}: ${cnt}` ).join('');