soulsync/core/wishlist
Broque Thomas 45ecf2730d Wishlist: harden Spotify backfill — poisoned tn=1 can't mask lean album
Residual per-track wishlist downloads (single tracks from different
albums, below the album-bundle threshold) were producing folders
without a year subfolder whenever the wishlist row carried a stale
``track_number=1`` from an older payload default.

Why: ``core/downloads/candidates.py`` had a single API-fetch branch
that served two concerns — resolving the track position AND
hydrating the lean ``spotify_album_context`` (release_date /
total_tracks / cover image) — gated entirely on track_number being
unresolved. When the wishlist row's ``track_number`` happened to
be 1 (a poisoned default rather than a real value), the gate
short-circuited and the album hydration the same call would have
done was skipped. Deezer-sourced discovery matches don't ship
release_date in their search-result album shape, so without the
backfill the folder lost its year.

The two concerns split:
  - track_number resolution keeps its track_info → track object →
    API precedence chain. track_info defaults still win.
  - album hydration runs whenever release_date or total_tracks are
    missing, independent of where (or whether) track_number was
    resolved.

The single API round-trip still serves both — the cost contract
is preserved. The side-effect coupling is gone.

Lifted into ``core/downloads/track_metadata_backfill.py``
(``hydrate_download_metadata``) so the precedence chain is pinned
in isolation. 24 unit tests cover the precedence chain, the
poisoned-tn=1 regression case, defensive non-dict/None inputs,
the cost guard (API called at most once per invocation), and
disc_number resolution.

Also lands the upstream piece: ``core/wishlist/routes.py:_build_track_data``
no longer defaults ``track_number=1`` / ``disc_number=1`` /
``total_tracks=1`` / ``release_date=''`` when the library-modal add
payload omits them. Missing values now flow through as ``None`` so
the downstream pipeline can detect-and-recover instead of locking
to a fake position.
2026-05-27 16:47:26 -07:00
..
__init__.py Extract wishlist logic into dedicated package 2026-04-28 21:17:24 +03:00
album_grouping.py Wishlist: only engage album-bundle when multiple tracks from same album (PR 1/4) 2026-05-27 13:42:04 -07:00
classification.py Coerce wishlist track counts before category checks 2026-04-30 21:42:43 +03:00
payloads.py Wishlist: fix three regressions causing all imports to land as track 01 with no year 2026-05-27 15:39:22 -07:00
presence.py Add module logger + surface silent exceptions in 7 logger-less files — 12 sites 2026-05-07 10:27:04 -07:00
processing.py Wishlist: distinguish Queued from Analyzing for executor-pending batches 2026-05-27 14:52:02 -07:00
reporting.py Extract wishlist logic into dedicated package 2026-04-28 21:17:24 +03:00
resolution.py Make wishlist respect configured providers 2026-04-30 07:41:22 +03:00
routes.py Wishlist: harden Spotify backfill — poisoned tn=1 can't mask lean album 2026-05-27 16:47:26 -07:00
selection.py Make wishlist respect configured providers 2026-04-30 07:41:22 +03:00
service.py Accept wishlist track_data aliases 2026-04-30 07:59:43 +03:00
state.py Extract wishlist logic into dedicated package 2026-04-28 21:17:24 +03:00