User ask: "a modal that lists the tracks downloaded via watchlist" — extended, as discussed, to playlists too. One modal, two tabs, opened from the Watchlist page (watchlist tab preselected) and the Sync page (playlists tab) — same shared-modal-different-entry-points UX as the rest of the app. The data: library_history recorded which SERVICE a file came from but never what TRIGGERED it. New origin/origin_context columns (migration + index) are written once at the import chokepoint via core/downloads/origin.py, a pure tested deriver that reads, in priority: an explicit _dl_origin stamp (set at batch-task creation for direct playlist batches, where the playlist context otherwise only survived in folder mode), the wishlist provenance already riding in track_info.source_info (watchlist_artist_name / playlist_name — watchlist_scanner has stamped these for ages), and the folder-mode playlist thread. Manual downloads stay unclassified by design. History starts from now — provenance can't be conjured retroactively. API: GET /api/download-origins?origin=watchlist|playlist (paged) and POST /api/download-origins/delete — deletes the file on disk (resolved through the shared container/host path resolver), the matching library track row, and the history entries; a file that refuses deletion keeps its row and reports the error instead of lying. UI: webui/static/origin-history.js — tabbed modal in the revamp design language (accent light-edge, pill tabs, entry rows reusing the library-history-entry components), per-row delete + select-all bulk delete with honest result toasts, empty/loading states, per-tab totals. Tests: 8 — deriver priority/shapes (incl. the exact watchlist_scanner source_info shape and JSON-string survival), origin filtering + counts, row fetch/delete isolation between origins, delete-track-by-path.
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""Download-origin provenance: what TRIGGERED a download.
|
|
|
|
The library history records which SERVICE a file came from (Soulseek,
|
|
YouTube, ...) but not WHY it was downloaded — a watchlist scan, a playlist
|
|
sync, or a manual click. The origin-history modal (watchlist page / sync
|
|
page) answers that, so the trigger must be derived once, at the history
|
|
chokepoint (``record_library_history_download``), from the post-process
|
|
context.
|
|
|
|
Signals, in priority order:
|
|
1. explicit ``track_info._dl_origin`` / ``_dl_origin_context`` stamps
|
|
(set at batch-task creation in core/downloads/master.py)
|
|
2. wishlist provenance riding in ``track_info.source_info`` — watchlist
|
|
items carry ``watchlist_artist_name``, playlist items ``playlist_name``
|
|
3. the playlist-folder-mode ``_playlist_name`` thread
|
|
|
|
Anything unmatched derives ``(None, '')`` — manual/other downloads are
|
|
intentionally not classified.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Dict, Optional, Tuple
|
|
|
|
ORIGIN_WATCHLIST = "watchlist"
|
|
ORIGIN_PLAYLIST = "playlist"
|
|
VALID_ORIGINS = (ORIGIN_WATCHLIST, ORIGIN_PLAYLIST)
|
|
|
|
|
|
def _parse_source_info(raw: Any) -> Dict[str, Any]:
|
|
if isinstance(raw, dict):
|
|
return raw
|
|
if isinstance(raw, str) and raw:
|
|
try:
|
|
parsed = json.loads(raw)
|
|
return parsed if isinstance(parsed, dict) else {}
|
|
except (json.JSONDecodeError, TypeError):
|
|
return {}
|
|
return {}
|
|
|
|
|
|
def derive_download_origin(context: Dict[str, Any]) -> Tuple[Optional[str], str]:
|
|
"""Return ``(origin, origin_context)`` for a completed download.
|
|
|
|
``origin`` is 'watchlist' / 'playlist' / None; ``origin_context`` is the
|
|
human label (watchlist artist name / playlist name). Never raises."""
|
|
try:
|
|
ti = context.get("track_info") or {}
|
|
if not isinstance(ti, dict):
|
|
return None, ""
|
|
si = _parse_source_info(ti.get("source_info"))
|
|
|
|
# 1. Explicit stamp wins.
|
|
origin = ti.get("_dl_origin")
|
|
if origin in VALID_ORIGINS:
|
|
return origin, str(ti.get("_dl_origin_context") or "")
|
|
|
|
# 2. Wishlist provenance riding in source_info.
|
|
if si.get("watchlist_artist_name"):
|
|
return ORIGIN_WATCHLIST, str(si["watchlist_artist_name"])
|
|
if si.get("playlist_name"):
|
|
return ORIGIN_PLAYLIST, str(si["playlist_name"])
|
|
|
|
# 3. Playlist-folder-mode thread.
|
|
if ti.get("_playlist_name"):
|
|
return ORIGIN_PLAYLIST, str(ti["_playlist_name"])
|
|
|
|
return None, ""
|
|
except Exception:
|
|
return None, ""
|