'sami matar' was a wing-it FALLBACK stub — a placeholder the discovery pipeline
makes when it can't resolve a track to real metadata (no album, no cover). The
live sync explicitly skips wing_it_* ids for the wishlist (no metadata to act on),
but my re-add didn't — so it stored a coverless, single-classified placeholder.
That's why: sync didn't add it, no images, marked single.
Fix (parity): reconstruct refuses ids starting 'wing_it_'. Frontend renders the
'-> Wishlist' status as plain, non-clickable text for wing-it rows (with a tooltip)
since they were never actually wishlisted. Real tracks keep the working button +
the byte-identical-payload re-add from the prior fix.
Root cause (the real one): the auto-add passes original_tracks_map[id] — tracks_json
run through a specific normalization (album->dict with images/album_type/total_tracks/
release_date, artists->dicts). My re-add hand-rolled a different shape, so the stored
spotify_data didn't match and the wishlist's nebula (which reads spotify_data.album.
images[0].url) had no cover, plus album/single classification could differ.
Fix: extract that normalization into one shared build_original_tracks_map() and use it
in BOTH the live sync (core.discovery.sync) and the re-add. The re-add now resolves the
track by source_track_id through the same map — byte-identical payload. Verified on a
real sync row: re-add payload == live-sync payload, album.images present. (The shared
normalizer is also copy-safe, fixing a latent tracks_json mutation in the old inline
version.)
Fallback (track absent from tracks_json) rebuilds through the same normalizer with the
cover seeded from the row's image_url. 10 tests incl. a direct parity assertion.
The re-add showed no album/single art. Cause: reconstruct returned the full track
from tracks_json AS-IS — and some syncs store tracks_json lean (no album.images),
so the re-added wishlist entry had an empty album.images even though the track's
cover was sitting right there in the track_result's image_url.
Fix: always backfill album.images from the track_result's image_url when the album
has none (and copy the dict so tracks_json isn't mutated). Real album art is kept
when present; the 250px thumb only fills a gap. Verified against a real sync row in
all three cases (full / lean tracks_json / no tracks_json) — album.images now
populated in every one. The wishlist card reads album.images, so the cover shows.
In the dashboard Recent Syncs detail modal, the '→ Wishlist' status on unmatched
tracks is now a button. Clicking it re-adds that exact track to the wishlist with
the SAME context the sync used (source_type='playlist' + the playlist's name/id +
failure_reason), so it's indistinguishable from the original auto-add.
- reconstruct_sync_track_data() (pure, tested): prefers the full cached track from
tracks_json (by source_track_id, then index) so album art/full data carry over;
falls back to the track_result fields; refuses non-'wishlist' rows and rows with
no id (can't re-wishlist a matched/unidentifiable track).
- POST /api/sync/history/<id>/track/<i>/wishlist resolves the entry server-side and
calls the wishlist service; idempotent (reports added vs already-on-wishlist).
- button shows a busy state then '✓ Re-added' / '✓ On wishlist'.
7 pure tests (full-track preference, id-vs-index match, fallback rebuild, non-
wishlist + out-of-range refusal). JS/PY/ruff clean.
Adds the 'Align playlists' action to the out-of-order modal — a dedicated,
order-only write path that does NOT touch the normal sync. Subsonic has no
per-track move, so it overwrites the song list in source order via createPlaylist
+ playlistId (same primitive replace-mode uses; identity/id preserved).
- plan_align_rewrite() (pure, tested): matched server ids in source order; every
one must already be in the playlist (never injects a track); extras either
dropped ('Mirror source') or parked at the end ('Keep extras'); returns None on
stale data so a vanished track can't be written.
- navidrome rewrite_playlist_order() primitive (raw ordered ids).
- /api/server/playlist/<id>/align: validates ids are in the live playlist, then
rewrites. Navidrome-only for now (Plex/Jellyfin reorder = follow-up).
- modal gets two explained options; missing tracks are NOT added (normal sync's
job) and that's stated. Metadata-free by design — it only reshuffles existing
server ids, so there's no sync-parity surface.
Open: confirm createPlaylist+playlistId preserves the playlist comment/image on a
live Navidrome (same risk as replace mode); add a re-apply step if it doesn't.
The editor renders the server column in SOURCE order (reconcile_playlist pairs
each server track to its source row), so a reordered-but-same-membership playlist
read as '5 matched / in sync' when Navidrome's real order actually differed — the
reorder never reaching the server was invisible.
- compute_order_status() (pure, tested): matched tracks' server positions must be
strictly ascending in source order; uses RELATIVE order so missing/extra tracks
never false-flag. reconcile entries now carry server_index (additive).
- endpoint returns order_status + server_order (the server's actual sequence).
- editor shows an amber 'out of order' badge on the server column when membership
matches but sequence differs, opening a read-only modal of the real server order.
One-way: source order stays the source of truth; no server-side editing.
Tests reproduce the reported 'Real Love Baby moved to #2' case + guard against
false-flagging on missing/extra. The actual 'sync order' WRITE is a separate
follow-up (membership/extra semantics + live identity-preservation test pending).
carlosjfcasero round 2: after 6fa956d6 append stopped recreating the playlist,
but every sync re-appended ALL matched tracks — every track N times. His log
shows it plainly: "added 22 new tracks to 'Disney' (skipped 0 already present)"
on a playlist that already had those 22.
Root cause: the dedupe read `{t.id for t in get_playlist_tracks(...)}` — but
JellyfinTrack only defines `ratingKey`, never `id`, so the existing-ids set was
ALWAYS empty and everything looked new. NavidromeTrack is also ratingKey-only,
so the Navidrome append had the identical bug. Plex (plexapi ratingKey) was
fine. The existing tests were green because they mocked existing tracks as
SimpleNamespace(id=...) — encoding the same wrong assumption as the code.
Fix:
- New pure planner plan_playlist_append(current, desired) in
core/sync/playlist_edit.py (next to the reconcile planner): order-preserving,
drops already-present ids, dedupes within desired, stringifies (Emby numeric
vs string safe).
- Jellyfin/Emby: existing ids fetched from the canonical
/Playlists/{id}/Items endpoint (same as reconcile — works for Jellyfin GUIDs
and Emby numeric ids), ratingKey fallback if that request fails.
- Navidrome: dedupe on ratingKey (the attribute that actually exists).
Tests: planner (skip-present incl. the reporter's unchanged-playlist case,
desired-order, dupes-within-desired, int/str ids, empties) + the append-mode
suite rewritten to pin the REAL shapes (raw Items dicts for Jellyfin,
ratingKey objects for Navidrome) + a new fallback-path test. 524
playlist/sync/jellyfin/navidrome tests pass.
start_playlist_sync validated the resolved mode with 'if sync_mode not in
(replace, append): sync_mode = replace' — a pre-existing clamp two lines below
the config read I added, which silently downgraded a configured 'reconcile' to
'replace'. So config=reconcile resolved correctly then got clobbered, and every
sync ran replace regardless of the setting (and incognito didn't help — it's
backend, not cache).
Replace the hand-rolled clamp with a pure normalize_sync_mode(requested,
configured) helper (VALID_SYNC_MODES includes reconcile) so the resolution is
testable and can't silently drop a mode again. Regression tests cover
reconcile-from-config, request-overrides-config, and unknown->replace.
Replace mode (default) deletes + recreates the server playlist every sync,
which wipes its custom image, description, and identity. Add an opt-in
'reconcile' sync mode that edits the existing playlist in place — adds the
tracks now in the source, removes the ones gone — without destroying the
object, so the user's custom art/description survive.
- Pure planner plan_playlist_reconcile(current, desired) -> {add, remove}.
- Per-client reconcile_playlist: Plex addItems/removeItems on the same object;
Navidrome Subsonic updatePlaylist delta (songIdToAdd / descending
songIndexToRemove); Jellyfin add + remove-by-PlaylistItemId on /Playlists/{id}/Items.
- sync_service: reconcile branch with a replace FALLBACK (if a server's in-place
edit is unavailable/fails, sync still succeeds destructively — logged loudly).
- Default stays 'replace' (no behavior change). New Settings > Playlist sync mode
picker (replace/reconcile/append) backed by playlist_sync.mode; per-request
sync_mode still overrides.
- Reconcile skips the post-sync source-image push so a custom poster isn't
re-clobbered (the bug).
Tests: planner (add/remove/dedupe/order/empty) + reconcile-or-replace dispatch
(success / false-fallback / exception-fallback / no-method). Per-server in-place
API calls need dev validation against real Plex/Jellyfin/Navidrome.
NOTE: opt-in only; default behavior unchanged.
Find & Add on the playlist-sync page only wrote sync_match_cache, which is
DELETEd wholesale after every DB scan — so the source->library pairing (and
the user's manual matches) reverted to 'extra'/red-dot on the next shallow
scan. The three match stores (sync_match_cache, manual_library_track_matches,
discovery extra_data) were disconnected and all pointed at tracks.id, which a
rescan re-keys (esp. Jellyfin/Navidrome GUIDs).
Unify the match so it's one durable fact, recorded once, honored everywhere:
- Find & Add also writes a durable manual_library_track_matches row (one-way;
the manual-match tool has no playlist to act on, so no reverse). Carries the
library file path.
- New library_file_path column (idempotent migration) + find_track_id_by_file_path:
re-resolve a stale library_track_id after a rescan re-keys the track, and
self-heal the row.
- The sync compare display's override lookup now falls back to the durable
manual match (resolve_durable_match_server_id) when sync_match_cache misses —
so the pairing persists across a scan instead of reverting to a red dot.
Purely additive: only adds matches when the cache returns nothing.
Tests: durable resolver (valid / stale-reresolve+self-heal / no-match / not-in-
playlist / missing-methods), file_path persistence + find_track_id_by_file_path.
A source row with no art of its own (e.g. a YouTube source, which provides
none at mirror time) now borrows the cover from its MATCHED server track, so
both sides of the sync editor show an image.
The endpoint already had a borrow fallback (_server_art_map), but it matched by
an exact normalized "{artist}|{title}" key — so a YouTube-shaped row like
"Arctic Monkeys - Do I Wanna Know?" never matched the library's "Do I Wanna
Know?" and stayed blank even though the server had the cover. This borrow is
keyed off the ACTUAL source<->server pairing the reconcile already computed, so
it works for those rows once #768's canonical matching pairs them.
Done in the pure reconcile_playlist (final pass), so no frontend change is
needed — the editor already renders source_track.image_url. Guarded so it only
fills an EMPTY source image (Spotify/CDN art is never overwritten) and only when
the matched server track actually has a thumb.
Composes with the rest: #766 made the server cover URL work, #768 made the
YouTube row match, this makes the matched source row borrow that cover — so an
artless YouTube row matched to a Navidrome track with art shows on both sides.
Tests: tests/test_playlist_reconcile.py (+4) — artless source borrows the
matched cover; source with its own art keeps it; unmatched source has nothing to
borrow; borrow skipped when the server track has no thumb. 15 reconcile + 59
sync/navidrome tests pass.
Three compounding bugs hit tracks whose source metadata is YouTube/streaming-
shaped — title "Artist - Song", artist "Official Artist"/"Artist - Topic"/
"ArtistVEVO" (reported: "Arctic Monkeys - Do I Wanna Know?" by "Official Arctic
Monkeys"). Server-agnostic — affects Plex/Jellyfin/Navidrome, not just the
reporter's Navidrome.
Bug A — the match fails. The confidence scorer and the editor's reconcile both
compared the raw "Artist - Song" title against the library's clean "Song"; the
length-ratio penalty + floor drove it to ~0.18 (NO-MATCH), so the track showed
unmatched while its server copy showed as an orphan "extra". New pure
core/text/source_title.py (clean_source_artist / strip_artist_prefix /
canonical_source_track) strips the channel/video decoration, applied at BOTH
matching seams: services/sync_service._find_track_in_media_server (tries raw
then canonical, keeps the best) and the editor reconcile. Conservative: a title
prefix is stripped only when it equals the artist, so "Self-Titled", "Jay-Z",
and "Marvin Gaye" (by another artist) are untouched, and the canonical form is
an additional best-of candidate so it can only help.
Bug B — manual matches never persisted. get_server_playlist_tracks built the
per-source entry WITHOUT source_track_id, so "Find & add" posted an empty id
and _persist_find_and_add_match returned early. The match reverted to "extra"
on reload and re-adding looped. The editor's 3-pass matcher is now lifted to a
pure, tested core.sync.playlist_reconcile.reconcile_playlist that includes
source_track_id (the frontend at pages-extra.js:1836 already reads + sends it).
Bug C — manual match duplicated + delete wiped all copies. "Find & add" always
inserted, so linking a source to an already-present server track appended a
duplicate (pos 72, 73...); remove filtered out EVERY entry with the target id.
New pure core.sync.playlist_edit (plan_playlist_add: link-don't-duplicate when
the target is already present; remove_one_occurrence: drop a single copy) wired
into the Plex/Jellyfin/Navidrome add + remove branches.
Tests (extreme): tests/test_source_title.py (35), tests/test_playlist_reconcile.py
(11 — incl. the reported case, parity for override/exact/fuzzy/extra, and
duplicate-server handling), tests/test_playlist_edit.py (12). 286 matching/sync
tests still pass.
Caveats: the sync_service change and the add/remove/editor endpoints are
read-verified, not executed against a live media server (none in CI). The pure
cores they call are exhaustively unit-tested; output-shape parity of the
reconcile lift is covered. Delete removes the first matching copy (duplicates
are identical, so harmless).
Closes#585. When a Spotify source track had a versioned suffix not
present in the local file ("Iron Man - 2012 - Remaster" vs "Iron Man"),
the auto-matcher missed the pair. User could click Find & Add to pick
the right local file — that worked, file got added to the Plex
playlist — but the source track stayed in Missing while the added
file appeared in Extra, because the matcher kept no record of the
user-confirmed pairing. On the next sync the source track re-tried
to download.
Fix: every Find & Add selection now writes a (spotify_track_id →
server_track_id) override into sync_match_cache at confidence=1.0.
The matching algorithm runs an override pass BEFORE the existing
exact and fuzzy passes, so any user-confirmed pair short-circuits
straight to "matched" without going through title normalization.
Covers every mismatch class — dash-suffix remasters, covers /
karaoke, alt masters, cross-language titles, typo'd local files.
- core/sync/match_overrides.py (new) — pure helpers
resolve_match_overrides + record_manual_match. 18 boundary tests
pin: cache hits, cache misses falling through to normal matching,
stale-cache (server track removed) handled gracefully, str/int
id coercion, partial cache hits, defensive against non-dict
inputs and DB exceptions.
- web_server.py — get_server_playlist_tracks runs the override
pre-pass before exact/fuzzy matching. server_playlist_add_track
accepts source_track_id + source_title + source_artist and
persists the override after every successful add (Plex / Jellyfin
/ Navidrome). source_track_id added to source_tracks payload so
the frontend has it.
- webui/static/pages-extra.js — _serverSelectTrack sends
source_track_id + source_title + source_artist when adding a
track from a mirrored playlist context.
- Sync match cache schema unchanged — already had UNIQUE
(spotify_track_id, server_source) which fits the override
semantics perfectly. Manual overrides distinguished from
auto-discovered matches by confidence=1.0.
Full suite: 3010 passed.