soulsync/core
Broque Thomas 0701bcc213 PersonalizedPlaylistsService: bake in ID-validity gate, lift selectors
User-facing bug found in the discover-page audit: multiple sections
(hidden gems, discovery shuffle, popular picks, decade browser,
genre browser) had no `WHERE (spotify_track_id IS NOT NULL OR
itunes_track_id IS NOT NULL ...)` gate. Tracks with no source IDs
in the discovery pool got displayed, the user clicked download, the
download silently failed because there was nothing to look up.

Lift + gate

`PersonalizedPlaylistsService` had 5 selection methods that all shared
the same shape — connect to DB, run a SELECT against `discovery_pool`
with different WHERE clauses, optionally apply diversity, return
list of track dicts. ~366 lines of business logic, ~55% of which was
repeated boilerplate.

Three new private helpers consolidate everything:

- `_select_discovery_tracks(*, source, extra_where, extra_params,
  order_by, fetch_limit, extra_columns)` — shared SELECT against
  `discovery_pool`. The mandatory ID gate is hard-coded into the
  WHERE clause: no opt-out flag, every method inherits it for free.
  Plus the source filter and the blacklist filter — same shape every
  selector needs.
- `_apply_diversity_filter(tracks, *, max_per_album, max_per_artist,
  limit)` — per-album / per-artist cap loop, returns trimmed list.
  Lifted from the inline duplicates in decade / genre / popular_picks.
- `_compute_adaptive_diversity_limits(tracks, *, relaxed=False)` —
  step-function tiers based on unique-artist count. `relaxed=True`
  gives the slightly looser limits the genre playlist used vs the
  decade playlist.

Re-enable 4 library methods

`get_recently_added`, `get_top_tracks`, `get_forgotten_favorites`,
`get_familiar_favorites` were all stubs (`return []`) because they
predated the schema columns they need. Schema now has them:
`tracks.created_at`, `tracks.play_count`, `tracks.last_played`, and
the source ID columns added in earlier work.

New `_select_library_tracks(*, where_clause, params, order_by, limit)`
helper mirrors the discovery selector but targets the `tracks` table
joined against `albums` + `artists`. Mandatory ID gate lives in the
helper too: every library method automatically rejects rows where
spotify_track_id, itunes_track_id, deezer_id,
musicbrainz_recording_id, AND audiodb_id are all NULL.

Selection rules:

- `get_recently_added` — ORDER BY created_at DESC
- `get_top_tracks` — WHERE play_count > 0 ORDER BY play_count DESC
- `get_forgotten_favorites` — WHERE play_count > 5 AND last_played
  < (now - 90 days) ORDER BY play_count DESC
- `get_familiar_favorites` — WHERE play_count BETWEEN 3 AND 15

Tests

`tests/test_personalized_playlists_id_gate.py` — 17 tests pinning:

- `_select_discovery_tracks` filters NULL-id rows, honors source +
  blacklist + extra_where
- `_apply_diversity_filter` caps per-album + per-artist + stops at
  limit
- `_compute_adaptive_diversity_limits` returns the right tier for
  unique-artist count + relaxed flag
- All 5 discovery methods (decade, popular_picks, hidden_gems,
  discovery_shuffle, genre is exercised via the helper) reject
  NULL-id rows
- All 4 library methods reject NULL-id rows + honor their
  play-count rules

Behavior preserved

Same diversity tiers, same over-fetch multipliers (10x for decade /
genre, 3x for popular_picks), same `popularity DESC, RANDOM()`
ordering, same `popularity >= 60` / `< 40` thresholds, same
blacklist filter. Public method signatures unchanged — `web_server.py`
needs zero edits.

Net file: 1089 → ~1170 LOC (helpers + docstrings), but actual
business logic across the 9 methods went from ~418 lines down to
~195 (-53%).

2222/2222 full suite green (was 2205 + 17 new). Ruff clean.
2026-05-08 07:14:36 -07:00
..
artists Surface silent exceptions in core/artists — 23 sites 2026-05-07 09:49:29 -07:00
automation Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
discovery Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
download_engine Add module logger + surface silent exceptions in 7 logger-less files — 12 sites 2026-05-07 10:27:04 -07:00
download_plugins Address Copilot doc-drift review 2026-05-05 15:46:48 -07:00
downloads Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
enrichment Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
imports Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
library Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
media_server Final review-pass nits — class docstring, dead branch, dead imports, boot resilience 2026-05-05 22:59:33 -07:00
metadata Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
playlists Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
repair_jobs Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
search Cin-6: Rename soulseek_client global → download_orchestrator 2026-05-04 23:23:32 -07:00
stats Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
streaming Cin-6: Rename soulseek_client global → download_orchestrator 2026-05-04 23:23:32 -07:00
watchlist Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
wishlist Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
workers Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
acoustid_client.py Clean up 286 ruff lint errors to unblock CI and fix 10 latent bugs 2026-04-21 13:30:52 -07:00
acoustid_verification.py Reject AcoustID matches whose version disagrees with the expected track 2026-05-07 13:25:30 -07:00
album_consistency.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
api_call_tracker.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
artist_source_detail.py Move metadata helpers into package modules 2026-04-29 11:28:42 +03:00
artist_source_lookup.py Make artist_name Optional in find_library_artist_for_source 2026-04-22 22:15:03 -07:00
audiodb_client.py Add API Rate Monitor dashboard with real-time speedometer gauges 2026-04-02 10:46:43 -07:00
audiodb_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
auto_import_worker.py Surface silent exceptions in import pipeline — 11 sites 2026-05-07 09:50:15 -07:00
automation_engine.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
connection_detect.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
connection_test.py Cin-6: Rename soulseek_client global → download_orchestrator 2026-05-04 23:23:32 -07:00
database_update_worker.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
debug_info.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
deezer_client.py Add download buttons + bulk action to artist top-tracks sidebar 2026-05-07 15:44:47 -07:00
deezer_download_client.py Surface silent exceptions in metadata clients — 37 sites 2026-05-07 09:33:38 -07:00
deezer_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
discogs_client.py Surface silent exceptions in metadata clients — 37 sites 2026-05-07 09:33:38 -07:00
discogs_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
download_orchestrator.py Lift shared download dataclasses + boot via singleton factory 2026-05-05 09:08:39 -07:00
genius_client.py Add API Rate Monitor dashboard with real-time speedometer gauges 2026-04-02 10:46:43 -07:00
genius_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
genre_filter.py Expand default genre whitelist from 223 to 272 genres 2026-04-18 20:32:49 -07:00
hifi_client.py Surface engine-not-wired errors + exclude soulseek from monitor aggregation 2026-05-05 12:20:51 -07:00
hydrabase_client.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
hydrabase_worker.py Improve graceful shutdown and rollback safety 2026-04-12 15:17:18 +03:00
itunes_client.py Surface silent exceptions in metadata clients — 37 sites 2026-05-07 09:33:38 -07:00
itunes_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
jellyfin_client.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
lastfm_client.py Add Last.fm Track Radio to Discover page 2026-04-14 23:41:36 -07:00
lastfm_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
library_reorganize.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
lidarr_download_client.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
listenbrainz_client.py Remove emojis from all Python log and print statements 2026-04-11 21:11:02 -07:00
listenbrainz_manager.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
listening_stats_worker.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
lyrics_client.py Remove emojis from all Python log and print statements 2026-04-11 21:11:02 -07:00
matching_engine.py Merge remote-tracking branch 'origin/dev' into refactor/media-server-engine 2026-05-05 20:36:31 -07:00
metadata_service.py Move profile Spotify cache into registry 2026-04-29 12:36:37 +03:00
musicbrainz_client.py MusicBrainz: Resolve release-group MBIDs to a release on album click 2026-04-24 08:48:02 -07:00
musicbrainz_search.py MusicBrainz: Dedupe same-named homonyms in artist search results 2026-04-24 10:27:45 -07:00
musicbrainz_service.py Add metadata cache maintenance and health monitoring 2026-03-30 07:40:20 -07:00
musicbrainz_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
navidrome_client.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
personalized_playlists.py PersonalizedPlaylistsService: bake in ID-validity gate, lift selectors 2026-05-08 07:14:36 -07:00
plex_client.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
qobuz_client.py Address Copilot doc-drift review 2026-05-05 15:46:48 -07:00
qobuz_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
reorganize_queue.py Reorganize queue: race + dedupe fixes from kettui review 2026-04-26 08:40:24 -07:00
reorganize_runner.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
repair_worker.py Surface silent exceptions in repair_worker — 16 sites 2026-05-07 09:17:51 -07:00
replaygain.py Add module logger + surface silent exceptions in 7 logger-less files — 12 sites 2026-05-07 10:27:04 -07:00
runtime_state.py Add module logger + surface silent exceptions in 7 logger-less files — 12 sites 2026-05-07 10:27:04 -07:00
seasonal_discovery.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
socketio_cors.py Socket.IO CORS: handle self-review nits 2026-04-26 19:24:43 -07:00
soulid_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
soulseek_client.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
soulsync_client.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
soundcloud_client.py Surface engine-not-wired errors + exclude soulseek from monitor aggregation 2026-05-05 12:20:51 -07:00
spotify_client.py Add download buttons + bulk action to artist top-tracks sidebar 2026-05-07 15:44:47 -07:00
spotify_public_scraper.py Add Spotify Link tab for public playlist/album scraping without API credentials 2026-03-15 21:25:05 -07:00
spotify_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
tag_writer.py Surface silent exceptions across remaining modules — ~70 sites 2026-05-07 10:28:58 -07:00
tidal_client.py Surface silent exceptions in metadata clients — 37 sites 2026-05-07 09:33:38 -07:00
tidal_download_client.py Surface engine-not-wired errors + exclude soulseek from monitor aggregation 2026-05-05 12:20:51 -07:00
tidal_worker.py Surface silent exceptions in workers + repair jobs — ~30 sites 2026-05-07 10:27:24 -07:00
watchlist_scanner.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00
web_scan_manager.py MS Cin-5: Drop per-server globals — engine owns the clients 2026-05-05 18:05:45 -07:00
wishlist_service.py Extract wishlist logic into dedicated package 2026-04-28 21:17:24 +03:00
worker_utils.py Fix Album Completeness job reporting zero findings for every album 2026-04-24 12:39:41 -07:00
youtube_client.py Final silent-exception sweep + ruff S110 lint guardrail — ~45 sites 2026-05-07 11:16:06 -07:00