soulsync/core
Broque Thomas cceffbd8ec Honor manually-matched source IDs in per-source enrichment workers
GitHub issue #501 (@Tacobell444). After manually matching an album to
a specific source ID via the match-chip UI, clicking "Enrich" on that
album would fuzzy-search by name and overwrite the manual match with
whatever the search returned — or revert the match status to
``not_found`` if name search missed. Reorganize then read the now-
wrong ID and moved files to the wrong destination.

Root cause was in the per-source enrichment workers'
``_process_*_individual`` methods. Several workers (Spotify, iTunes)
ran search-by-name unconditionally with no check for an existing
stored ID. Others (Deezer, Tidal, Qobuz) skipped on existing-ID but
without refreshing metadata — preserved the ID but didn't actually
honor the user's intent of "use this match to pull fresh data".

Cin-shape lift: same fix needed in 5 workers, so extracted the shared
behavior into ``core/enrichment/manual_match_honoring.py``:

    honor_stored_match(
        db, entity_table, entity_id, id_column,
        client_fetch_fn, on_match_fn, log_prefix,
    ) -> bool

Per-worker variability (DB column name, client fetch method, response
shape) plugs in via callbacks. Workers call the helper at the top of
``_process_album_individual`` / ``_process_track_individual``; if it
returns True, the manual match was honored and the search-by-name
fallback is skipped. If False (no stored ID, fetch failed, or empty
response), the worker's existing search-by-name flow runs as before.

Workers wired:

- spotify_worker — album + track (was overwriting; now honors)
- itunes_worker — album + track (was overwriting; now honors)
- deezer_worker — album + track (was skip-on-id; now refreshes)
- tidal_worker — album + track (was skip-on-id; now refreshes)
- qobuz_worker — album + track (was skip-on-id; now refreshes)

Workers left alone (already correct):

- discogs_worker — already had inline stored-ID fast path that
  refreshes metadata. Same behavior, just inline; refactoring to use
  the shared helper would be churn for zero behavior change.
- audiodb_worker — same — inline fast path with full metadata refresh.
- musicbrainz_worker — preserves existing MBID and marks status,
  which is the correct behavior for MB (the MBID itself is the match
  payload — no separate metadata fetch).
- lastfm_worker / genius_worker — name-based services with no
  source-specific IDs to honor. Inherent re-search per call.

Reorganize fixed indirectly — it always honored stored IDs correctly
via ``library_reorganize._extract_source_ids``. The "Reorganize broken"
symptom was downstream of broken Enrich corrupting the stored ID.

Tests:

- ``tests/enrichment/test_manual_match_honoring.py`` — 11 tests
  pinning the shared helper contract: stored-ID fast path, no-ID
  fallthrough, empty-string treated as no ID, missing row, fetch
  exception caught and falls through, fetch returns None falls
  through, callback exceptions propagate, configurable table +
  column, defensive table-name whitelist.

- Per-worker wiring NOT tested individually — the workers depend
  on live DB / client objects that are heavy to mock. The shared
  helper's contract is pinned; per-worker call sites are short
  enough to verify by code review.

2173/2173 full suite green.

Closes #501.
2026-05-06 19:00:53 -07:00
..
artists Fix Download Discography pulling wrong artist + log routing 2026-05-06 13:03:43 -07:00
automation Lift /api/automations/blocks static config into core/automation/blocks.py 2026-04-27 18:31:36 -07:00
discovery MS Cin-5: Drop per-server globals — engine owns the clients 2026-05-05 18:05:45 -07:00
download_engine Surface engine-not-wired errors + exclude soulseek from monitor aggregation 2026-05-05 12:20:51 -07:00
download_plugins Address Copilot doc-drift review 2026-05-05 15:46:48 -07:00
downloads Surface engine-not-wired errors + exclude soulseek from monitor aggregation 2026-05-05 12:20:51 -07:00
enrichment Honor manually-matched source IDs in per-source enrichment workers 2026-05-06 19:00:53 -07:00
imports Migrate album-info builders to typed Album path 2026-05-03 22:53:12 -07:00
library Lift shared download dataclasses + boot via singleton factory 2026-05-05 09:08:39 -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 Fix Download Discography pulling wrong artist + log routing 2026-05-06 13:03:43 -07:00
playlists Lift _prepare_stream_task + playlist_explorer_build_tree to core/ 2026-04-29 11:26:07 -07:00
repair_jobs Fix: maintenance findings badge inflated when scan dedup-skipped 2026-05-04 08:55:13 -07:00
search Cin-6: Rename soulseek_client global → download_orchestrator 2026-05-04 23:23:32 -07:00
stats Add Library Disk Usage card to System Statistics 2026-05-03 20:17:06 -07:00
streaming Cin-6: Rename soulseek_client global → download_orchestrator 2026-05-04 23:23:32 -07:00
watchlist Bulk watchlist add: fall back through every source ID, not just active 2026-04-30 20:27:42 -07:00
wishlist Coerce wishlist track counts before category checks 2026-04-30 21:42:43 +03:00
workers Lift WebMetadataUpdateWorker to core/workers/metadata_update.py 2026-04-29 14:20:41 -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 Fix: tasks showed Completed when file was quarantined 2026-05-03 18:28:32 -07:00
album_consistency.py Picard-style release preference scoring for MusicBrainz matching 2026-04-16 13:29:04 -07:00
api_call_tracker.py Make ApiCallTracker.save() atomic to prevent corrupt history files 2026-04-22 22:15:09 -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 Add genre whitelist for filtering junk tags during enrichment 2026-04-18 20:23:53 -07:00
auto_import_worker.py Rename unused loop var to silence ruff B007 2026-05-02 23:18:05 -07:00
automation_engine.py Surface handler-returned errors in automation last_error 2026-04-30 15:45:28 -07:00
connection_detect.py Lift run_detection to core/connection_detect.py 2026-04-29 22:40:34 -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 Add "All Libraries (combined)" mode to PlexClient 2026-05-06 15:39:19 -07:00
debug_info.py Cin-6: Rename soulseek_client global → download_orchestrator 2026-05-04 23:23:32 -07:00
deezer_client.py Move metadata API into package 2026-04-29 08:10:18 +03:00
deezer_download_client.py Surface engine-not-wired errors + exclude soulseek from monitor aggregation 2026-05-05 12:20:51 -07:00
deezer_worker.py Honor manually-matched source IDs in per-source enrichment workers 2026-05-06 19:00:53 -07:00
discogs_client.py Add Discogs collection as a Your Albums source 2026-05-03 21:27:46 -07:00
discogs_worker.py Discogs: count rows with empty type_ as real tracks too 2026-04-25 08:27:44 -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 Improve graceful shutdown and rollback safety 2026-04-12 15:17:18 +03: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 Add missing _get_artist_image_from_albums to HydrabaseClient 2026-03-23 17:54:21 -07:00
hydrabase_worker.py Improve graceful shutdown and rollback safety 2026-04-12 15:17:18 +03:00
itunes_client.py Move metadata API into package 2026-04-29 08:10:18 +03:00
itunes_worker.py Honor manually-matched source IDs in per-source enrichment workers 2026-05-06 19:00:53 -07:00
jellyfin_client.py MS Gap 1: Lift shared TrackInfo + PlaylistInfo to neutral types module 2026-05-05 18:25:28 -07:00
lastfm_client.py Add Last.fm Track Radio to Discover page 2026-04-14 23:41:36 -07:00
lastfm_worker.py fix: stop enrichment workers from re-processing rows forever 2026-04-19 15:22:24 -07:00
library_reorganize.py Reorganize: fix moved-count + status/total UX issues from PR #377 review 2026-04-25 09:07:44 -07:00
lidarr_download_client.py Lift shared download dataclasses + boot via singleton factory 2026-05-05 09:08:39 -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 Add Last.fm Track Radio to Discover page 2026-04-14 23:41:36 -07:00
listening_stats_worker.py MS Cin-5: Drop per-server globals — engine owns the clients 2026-05-05 18:05:45 -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 fix: stop enrichment workers from re-processing rows forever 2026-04-19 15:22:24 -07:00
navidrome_client.py MS Gap 1: Lift shared TrackInfo + PlaylistInfo to neutral types module 2026-05-05 18:25:28 -07:00
personalized_playlists.py Replace all legacy metadata_service imports with canonical functions 2026-04-10 12:52:36 -07:00
plex_client.py Add cross-section dedup to all-libraries listing layer 2026-05-06 16:01:37 -07:00
qobuz_client.py Address Copilot doc-drift review 2026-05-05 15:46:48 -07:00
qobuz_worker.py Honor manually-matched source IDs in per-source enrichment workers 2026-05-06 19:00:53 -07:00
reorganize_queue.py Reorganize queue: race + dedupe fixes from kettui review 2026-04-26 08:40:24 -07:00
reorganize_runner.py Library reorganize: FIFO queue with live status panel 2026-04-25 18:01:32 -07:00
repair_worker.py Fix: maintenance findings badge inflated when scan dedup-skipped 2026-05-04 08:55:13 -07:00
replaygain.py Fix: ReplayGain wrote same +52 dB gain to every track 2026-05-03 19:08:35 -07:00
runtime_state.py PR4b: lift cancel + clear download routes to core/downloads/cancel.py 2026-04-27 21:41:35 -07:00
seasonal_discovery.py Clean up 286 ruff lint errors to unblock CI and fix 10 latent bugs 2026-04-21 13:30:52 -07:00
socketio_cors.py Socket.IO CORS: handle self-review nits 2026-04-26 19:24:43 -07:00
soulid_worker.py Tighten legacy logging output 2026-04-21 18:00:31 +03:00
soulseek_client.py Lift shared download dataclasses + boot via singleton factory 2026-05-05 09:08:39 -07:00
soulsync_client.py MS Cin-1 + Cin-2: Explicit contract inheritance + generic accessors 2026-05-05 16:59:05 -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 Make Spotify status updates event-driven 2026-05-02 22:02:01 +03: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 Honor manually-matched source IDs in per-source enrichment workers 2026-05-06 19:00:53 -07:00
tag_writer.py Separate AcoustID file logging 2026-04-21 18:00:31 +03:00
tidal_client.py Clean up 286 ruff lint errors to unblock CI and fix 10 latent bugs 2026-04-21 13:30:52 -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 Honor manually-matched source IDs in per-source enrichment workers 2026-05-06 19:00:53 -07:00
watchlist_scanner.py Make extract_external_ids recognize all source-tagging conventions 2026-05-02 18:26:12 -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 Surface engine-not-wired errors + exclude soulseek from monitor aggregation 2026-05-05 12:20:51 -07:00