The /api/library/watchlist-all-unwatched endpoint required the user's currently active metadata source's ID column on each library artist. A Spotify-primary user with library artists only matched against iTunes or Deezer saw them silently skipped — surfacing on Discord as "Library and Watchlist not syncing correctly". The per- artist Enhanced View sync sometimes "fixed" them because it triggered metadata enrichment that occasionally populated the missing Spotify ID, but couldn't help artists Spotify simply doesn't carry. Extracts the picker as a standalone helper so it can be tested directly: core/watchlist/source_picker.py:pick_artist_id_for_watchlist Picks the active source first when available, then falls back through spotify -> itunes -> deezer -> discogs in registration order. Empty strings count as missing. Numeric IDs are coerced to str so SQLite's TEXT columns store them in the same form library code reads back. Returns (None, None) only when the artist has zero source IDs — the only legitimate skip reason now. Adds 10 regression tests covering active-source priority for each supported primary, fallback ordering through every secondary, the zero-IDs base case, unrecognized active source (e.g. hydrabase still falls through), empty-string handling, and numeric coercion.
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
"""Active-source-aware artist ID picker for bulk watchlist add.
|
|
|
|
The bulk "Add unwatched library artists to watchlist" endpoint used
|
|
to drop artists silently whenever they didn't carry an ID for the
|
|
user's currently active metadata source. A Spotify-primary user with
|
|
library artists matched only against iTunes/Deezer would see them
|
|
counted as ``skipped_no_id`` and never make it onto the watchlist —
|
|
surfacing on Discord as "Library and Watchlist not syncing
|
|
correctly". The per-artist Enhanced View sync sometimes "fixed" them
|
|
because it re-ran enrichment that occasionally populated the missing
|
|
ID, but that workaround couldn't help artists Spotify simply doesn't
|
|
have.
|
|
|
|
This helper picks the active source's ID first, then falls back
|
|
through every other supported source so an artist makes it onto the
|
|
watchlist as long as ANY metadata source can identify them.
|
|
"""
|
|
|
|
from typing import Any, Dict, Optional, Tuple
|
|
|
|
|
|
# (source_name, library-artist-row column). Order is also the
|
|
# fallback priority — Spotify > iTunes > Deezer > Discogs by default
|
|
# coverage. The active source moves to the front of the queue inside
|
|
# pick_artist_id_for_watchlist.
|
|
SOURCE_ID_COLUMNS = (
|
|
('spotify', 'spotify_artist_id'),
|
|
('itunes', 'itunes_artist_id'),
|
|
('deezer', 'deezer_id'),
|
|
('discogs', 'discogs_id'),
|
|
)
|
|
|
|
|
|
def pick_artist_id_for_watchlist(
|
|
artist: Dict[str, Any],
|
|
active_source: Optional[str],
|
|
) -> Tuple[Optional[str], Optional[str]]:
|
|
"""Pick a (source-id, source-name) pair for adding ``artist`` to
|
|
the watchlist.
|
|
|
|
Tries ``active_source`` first when it appears in
|
|
``SOURCE_ID_COLUMNS``, then falls back through every other source
|
|
in registration order. Empty strings count as missing. Returns
|
|
``(None, None)`` only when the artist truly has no usable source
|
|
ID — that's the only legitimate skip reason for the bulk-add
|
|
flow.
|
|
|
|
The returned ID is always coerced to ``str`` because watchlist
|
|
columns are TEXT and SQLite will happily store the original int
|
|
type otherwise (which then breaks ID-based equality checks
|
|
between watchlist and library code paths).
|
|
"""
|
|
preferred = next(
|
|
((src, col) for src, col in SOURCE_ID_COLUMNS if src == active_source),
|
|
None,
|
|
)
|
|
ordered = [preferred] if preferred else []
|
|
ordered.extend(
|
|
(src, col) for src, col in SOURCE_ID_COLUMNS if (src, col) != preferred
|
|
)
|
|
for src, col in ordered:
|
|
if not src:
|
|
continue
|
|
value = artist.get(col)
|
|
if value:
|
|
return str(value), src
|
|
return None, None
|