fix(provenance): label torrent/usenet/staging downloads correctly in history

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.
pull/671/head
Broque Thomas 4 days ago
parent c990ce079d
commit daaed373e7

@ -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)

@ -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 <date>" 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 {}

@ -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 = {};

@ -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]) =>
`<span class="history-source-chip" style="border-color:${_srcColors[src] || '#888'};color:${_srcColors[src] || '#888'}">${src}: ${cnt}</span>`
).join('');

Loading…
Cancel
Save