Across all background workers (Spotify/Tidal/Deezer/Qobuz/iTunes/
Discogs/Genius/AudioDB/MusicBrainz/Last.fm/SoulID + the metadata-update
worker) and the repair-job scanners. All converted to
`logger.debug("...: %s", e)`.
Two `_e` renames in genius_worker and soulid_worker where outer scope
was already binding `e`. Two finally-block sites in repair_jobs/
library_reorganize.py left silent (conn.close on shutdown path).
Refs #369
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.
Four enrichment workers (Last.fm, MusicBrainz, Tidal, Qobuz) had a
bug where every background loop re-processed the same rows because
the existing-ID short-circuit path never set match_status, and two
workers queried the wrong column when checking for an existing ID.
lastfm_worker._get_existing_id queried a non-existent lastfm_id
column; the real column is lastfm_url. The method now reads
lastfm_url for all three entity types.
musicbrainz_worker._get_existing_id queried musicbrainz_id for all
entity types, but albums use musicbrainz_release_id and tracks use
musicbrainz_recording_id. The method now uses a per-type column map.
All four workers (lastfm, musicbrainz, tidal, qobuz) now write
match_status='matched' when they short-circuit on an already-present
external ID, so these rows are no longer re-selected on the next
worker sweep.
A new migration (_backfill_match_status_for_existing_ids) runs once
on startup to retroactively set match_status='matched' for rows that
already have an external ID but NULL match_status. This covers legacy
data, manual matches, and rows populated from file tags outside the
worker.
New core/genre_filter.py with ~180 curated default genres. When strict
mode is enabled in Settings → Library Preferences → Genre Whitelist,
only whitelisted genres pass through during enrichment. Junk tags from
Last.fm (artist names, radio shows, playlist names) are silently dropped.
Applied at all 10 genre write points: Spotify, Last.fm, AudioDB, Deezer,
Discogs, iTunes, Qobuz enrichment workers + post-processing genre merge
+ initial download artist/album creation.
Strict mode is OFF by default — zero behavior change for existing users.
First enable auto-populates the whitelist with defaults. Users can add,
remove, search, and reset genres via the Settings UI.
- Add interruptible stop events to background workers so shutdown
wakes out of long sleeps instead of waiting on fixed delays.
- Stop scan managers, repair worker, executors, and cleanup helpers
deterministically so process exit does not leave background threads
alive.
- Add startup warnings for stale SQLite WAL/SHM sidecars so unclean
shutdowns are easier to spot before init/migration errors cascade.
- Prevent forced kills from leaving SQLite sidecars behind, which
made rollbacks to older branches fail with malformed database
errors.
The original #221 fix only covered Genius and AudioDB. All other
workers (Spotify, iTunes, Last.fm, MusicBrainz, Deezer, Tidal,
Qobuz) had the same bug: enrichment overwrites manual match status
to not_found when name search fails. Each worker now checks for an
existing service ID before searching by name and returns early if
one exists, preserving the manual match.
Helper system phases 2-7:
- Setup Progress: onboarding checklist with progress ring, auto-detection
via /status, /api/settings, /api/library, /api/watchlist, /api/automations
- Quick Actions: accent pill buttons in popovers (service cards get
"Open Settings" and "View Docs" actions)
- Keyboard Shortcuts: full-screen overlay with key cap styling, grouped
by scope (Global, Player, Helper, Forms)
- Search: fuzzy search across 200+ help entries, 11 tours, and shortcuts
with cross-page navigation via _guessPageFromSelector()
- What's New: version-tagged highlights with "Show me" navigation,
red badge on ? button for unseen versions, older version cycling
- Troubleshoot: scans dashboard service cards for disconnected/error
states, shows fix steps with action buttons, "All Clear" when healthy
- Contextual menu: page-aware tour suggestion at top of menu
- Ctrl+K / Cmd+K opens helper search globally
- First-launch welcome tooltip with pulsing ? button
- Redesigned floating button (48px, accent gradient, glass effect)
- Redesigned menu (unified card panel, accent left-stripe on contextual)
Enrichment worker fixes:
- AcoustID: individual recording matches downgraded INFO→DEBUG to reduce
log noise (14 lines for one track → 1 summary line)
- Name normalization: strip " - Suffix" dash format (Spotify) same as
"(Suffix)" parens format across all 8 workers. Fixes false mismatch
on tracks like "Electric Eyes (Studio Brussels Remix)" vs
"Electric Eyes - Studio Brussels Remix" (was 0.54, now matches)
- All 9 enrichment workers: stop auto-retrying 'error' status items (was infinite loop)
Only 'not_found' items retry after configured days; errors require manual full refresh
- Cover art dedup: check both 'pending' AND 'resolved' findings to prevent recreation
- Cover art scanner: top-level Spotify rate limit check skips Spotify entirely when
banned, falls back to iTunes/Deezer only, logs once instead of spamming 429s
Pending count queries included NULL-ID rows that _get_next_item filters
out, so pending stayed > 0 even when no processable items remained.
Workers reported running instead of idle, UI never turned green. Added
AND id IS NOT NULL to _count_pending_items across all 9 workers to
match the _get_next_item filter.
Workers would endlessly match the same track because UPDATE WHERE id =
NULL matches 0 rows in SQL. Added AND id IS NOT NULL to all enrichment
queries (individual, batch EXISTS, and batch fetch) across all 9
workers. Also added process-level guard for belt-and-suspenders safety.
Fix Deezer get_track → get_track_details method name mismatch.