The mirror_playlist fix only assigns stable ids to newly-imported playlists, so a
user with an existing file-import playlist would still have empty-id rows (and dead
Find & Add matches) until a manual re-import. Add an idempotent startup backfill that
assigns the SAME stable id a fresh import would to any mirrored track missing one —
so existing matches start sticking with no re-import. Runs once per db/process (the
init is guarded), only touches empty-id rows (no-op afterward), native ids untouched.
Tests: backfill fills empty ids with the exact fresh-import id, is idempotent (2nd
run = 0), and leaves native ids alone.
A Find & Add on a file-import (CSV/M3U/TXT) playlist track was silently dropped and
the track re-appeared as 'extra' (radoslav-orlov). Root cause: unlike Spotify/YouTube
(native ids), file-import + iTunes-only tracks arrive with an EMPTY source_track_id —
and the whole manual-match system keys on it. _persist_find_and_add_match is a no-op
on an empty id, and find_manual_library_match_by_source_track_id returns None for one,
so the match can be neither recorded nor looked up. That's the youtube-vs-file
difference the reporter noticed.
Fix: stable_source_track_id() derives a DETERMINISTIC 'file:<hash>' id from the track
identity (artist|title|album, normalized) when there's no native id; mirror_playlist
assigns it so the SAME song gets the SAME id across re-imports/discovery — exactly
what the match lookup needs. Native ids are used verbatim; bonus: discovery extra_data
now survives a re-import for these tracks too.
Tests: helper (native passthrough, deterministic + case/field-insensitive, distinct
per song, empty-on-no-title, file: prefix); mirror_playlist integration (file tracks
get stable distinct ids, stable across re-import, native ids untouched). 319 playlist/
sync/discovery/mirrored tests green.
Expose Navidrome album coverArt as a Subsonic getCoverArt thumbnail so library refreshes keep a real album-art URL. Preserve existing album thumb_url when an incoming server album has no thumbnail, preventing manual or server-corrected covers from being cleared and later replaced by loose missing-cover searches. Add regression tests for Navidrome album thumbnails and DB thumb preservation.
Three changes folded into one perf+cleanup pass:
1. Indexed fast path for the per-artist pool fetch. The previous
`search_tracks(artist=name)` call hit
`unidecode_lower(artists.name) LIKE ?`, a function-in-WHERE that
can't use `idx_artists_name`. New `MusicDatabase.get_artist_tracks_indexed`
does a two-step lookup: exact-name match (indexed) plus a
case-insensitive fallback, then `tracks WHERE artist_id IN (...)`
via `idx_tracks_artist_id`. Drops per-artist fetch from seconds to
milliseconds for the common case. The sync helper falls back to
the old LIKE-based `search_tracks` only when the indexed lookup
finds nothing, preserving diacritic recall and `tracks.track_artist`
feature-artist matches with zero regression.
2. Public text-normalization helper. Lifted the body of
`MusicDatabase._normalize_for_comparison` into
`core/text/normalize.py:normalize_for_comparison` so callers outside
the database layer (matching engine, sync pool, future import-side
comparisons) don't reach across the module boundary into a
leading-underscore "private" method. The DB method now delegates,
so existing internal call sites stay untouched. Sync's lazy pool
now imports the public helper.
3. Artist-name walker extracted. `_artist_name` at module level in
`services/sync_service.py` replaces two near-identical inline
str-or-dict-or-fallback walkers (one in `sync_playlist`, one in
`_find_track_in_media_server`). Returns `''` for None instead of
the literal string `'None'`.
Plus three small tidies from the same review:
- `_POOL_FETCH_LIMIT = 10000` constant in place of the literal at the
pool-fetch call site.
- Trimmed the verbose docstring + comment block on the pool helper.
- Set-intersection predicate for the trigger-shape reset in
`core/automation/api.py` instead of a two-line `or` chain.
Also removed the duplicate `_get_active_media_client()` call at
sync_service.py:212/214 — pre-existing wart that was sitting in the
same block I was editing.
Tests: 21 new tests across `tests/database/`, `tests/sync/`, and
`tests/text/`, plus updates to the existing pool tests to cover the
new fast/fallback split. Full suite stays green (3953 passing).
Centralize mirrored playlist source reference normalization so edited links and IDs are stored consistently. Preserve URL-backed refresh refs, surface missing-source refresh failures, count background sync failures in pipeline summaries, and retry guarded automation skips after a short delay instead of losing a scheduled run. Add focused coverage for source refs, mirrored playlist source updates, refresh failures, and guarded retry behavior.