soulsync/webui
Broque Thomas 4fb9f38798 Your Albums: selectable wishlist modal + Tidal album resolution
Two-part fix to the Your Albums "Download Missing" flow on Discover.

Part A — UX redesign

The prior `downloadMissingYourAlbums()` ran a per-album loop that
fired direct-download tasks via `openDownloadMissingModalForYouTube`.
Reported as silently failing — "Queuing 2/2" toast with no actual
transfer activity. Even when downloads worked, bypassing the
wishlist meant no retry / dedup / rate-limit / source-fallback
handling.

Replaced with a selectable-grid modal mirroring the Download
Discography pattern from the library page. Click the download
button → opens a checkbox grid showing every missing album (cover,
title, artist, year, track count, source) → user picks what they
actually want → click "Add to Wishlist" → each album's tracks get
resolved + queued through the existing wishlist auto-download
processor. NDJSON progress stream renders ✓/✗ per album.

New JS helpers:
- `_openYourAlbumsBatchModal(missingAlbums)` — builds the modal
- `_renderYourAlbumsBatchCard(row, index)` — per-album card
- `_yourAlbumsBatchSelectAll(select)` — bulk toggle
- `_updateYourAlbumsBatchFooterCount()` — live count + button text
- `_closeYourAlbumsBatchModal()` — overlay teardown
- `_startYourAlbumsBatchAddToWishlist()` — submit handler, NDJSON
  progress consumer
- `_yourAlbumsPickSource(album)` — picks the single best source-id
  per row (priority: spotify → deezer → tidal → discogs)

Reuses the `.discog-*` CSS classes from the library Download
Discography modal — no new CSS. Reuses the existing
`/api/artist/<id>/download-discography` endpoint. The endpoint's URL
artist_id param is functionally unused (per-album payload carries
everything — verified by reading the endpoint body), so the modal
posts with placeholder `your-albums` and gets multi-artist
resolution for free without backend changes.

Part B — Tidal album resolution

Reported as the original bug: clicking download on Tidal-only albums
did nothing because `/api/discover/album/<source>/<album_id>` had no
`tidal` branch and `tidal_client` had no `get_album_tracks` method.

`core/tidal_client.py`: new `get_album_tracks(album_id, limit=None)`
method. Two-phase: cursor-walk
`/v2/albums/<id>/relationships/items?include=items` for track refs +
position metadata (`meta.trackNumber` + `meta.volumeNumber`),
batch-hydrate via existing `_get_tracks_batch` for artist/album
names. Returns `Track` objects with `track_number` and `disc_number`
attached. Sort by (disc, track) so multi-disc compilations render in
album order.

`web_server.py`: new `'tidal'` source branch in
`/api/discover/album/<source>/<album_id>`. Resolves album metadata
via `get_album`, tracks via `get_album_tracks`, cover art via inline
`?include=coverArt` lookup. Same response shape as Spotify/Deezer
branches.

`webui/static/discover.js`:
- `tidal_album_id` added to `trySources` for the single-album click
  flow (`openYourAlbumDownload`)
- Same source picker drives the new batch modal
- Virtual-id generation includes `tidal_album_id` so Tidal-only
  albums get stable identifiers across discover-album-* / your-
  albums-* contexts

10 new tests in `tests/test_tidal_album_tracks.py` pin:
- Single-page walk + hydration
- Multi-page cursor chain
- Multi-disc sort order (disc 1 → 2 in track order each)
- `limit` short-circuit at page boundary
- No-token short-circuit (no API call)
- HTTP error returns empty
- 429 raises (propagates to `rate_limited` decorator for retry)
- Forward-compat type filter (skips non-track entries)
- Partial-batch hydration failure containment
- Empty-album short-circuit (no batch call)

Full pytest: 2693 passed.
2026-05-11 12:36:16 -07:00
..
static Your Albums: selectable wishlist modal + Tidal album resolution 2026-05-11 12:36:16 -07:00
index.html Tidal: surface Favorite Tracks as virtual playlist (issue #502) 2026-05-10 21:36:22 -07:00