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).
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""Extreme battery for sync-editor add/remove planners (#768 Bug C)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.sync.playlist_edit import plan_playlist_add, remove_one_occurrence
|
|
|
|
|
|
# ── plan_playlist_add: link must not duplicate ────────────────────────────
|
|
|
|
def test_link_to_existing_track_does_not_insert():
|
|
# The reported loop: matching an unmatched source to a track already in
|
|
# the playlist (an "extra") must NOT add a second copy.
|
|
plan = plan_playlist_add(["a", "b", "nv72"], "nv72", is_link=True)
|
|
assert plan["should_insert"] is False
|
|
assert plan["new_ids"] == ["a", "b", "nv72"] # unchanged
|
|
|
|
|
|
def test_link_to_absent_track_inserts():
|
|
plan = plan_playlist_add(["a", "b"], "nv99", is_link=True, position=1)
|
|
assert plan["should_insert"] is True
|
|
assert plan["new_ids"] == ["a", "nv99", "b"]
|
|
|
|
|
|
def test_non_link_add_always_inserts_even_if_present():
|
|
# A plain add (no source link) may legitimately duplicate.
|
|
plan = plan_playlist_add(["a", "b"], "a", is_link=False)
|
|
assert plan["should_insert"] is True
|
|
assert plan["new_ids"].count("a") == 2
|
|
|
|
|
|
def test_add_appends_when_no_position():
|
|
plan = plan_playlist_add(["a", "b"], "c", is_link=False)
|
|
assert plan["new_ids"] == ["a", "b", "c"]
|
|
|
|
|
|
def test_add_clamps_out_of_range_position():
|
|
assert plan_playlist_add(["a"], "c", is_link=False, position=99)["new_ids"] == ["a", "c"]
|
|
assert plan_playlist_add(["a"], "c", is_link=False, position=-5)["new_ids"] == ["c", "a"]
|
|
|
|
|
|
def test_add_stringifies_ids():
|
|
plan = plan_playlist_add([1, 2, 72], 72, is_link=True)
|
|
assert plan["should_insert"] is False
|
|
|
|
|
|
# ── remove_one_occurrence: remove ONE, not all ────────────────────────────
|
|
|
|
def test_removes_only_one_of_duplicates():
|
|
# The #768 delete bug: two copies (pos 72, 73) — removing must drop ONE.
|
|
new_ids, removed = remove_one_occurrence(["a", "nv72", "nv72", "b"], "nv72")
|
|
assert removed is True
|
|
assert new_ids == ["a", "nv72", "b"] # one copy survives
|
|
|
|
|
|
def test_removes_exact_position_when_given():
|
|
new_ids, removed = remove_one_occurrence(["x", "x", "x"], "x", position=1)
|
|
assert removed is True
|
|
assert new_ids == ["x", "x"]
|
|
|
|
|
|
def test_falls_back_to_first_when_position_mismatches():
|
|
new_ids, removed = remove_one_occurrence(["a", "b", "c"], "b", position=0)
|
|
assert removed is True
|
|
assert new_ids == ["a", "c"]
|
|
|
|
|
|
def test_remove_absent_id_reports_not_removed():
|
|
new_ids, removed = remove_one_occurrence(["a", "b"], "zzz")
|
|
assert removed is False
|
|
assert new_ids == ["a", "b"]
|
|
|
|
|
|
def test_remove_single_occurrence():
|
|
new_ids, removed = remove_one_occurrence(["a", "b", "c"], "b")
|
|
assert (new_ids, removed) == (["a", "c"], True)
|
|
|
|
|
|
def test_remove_stringifies():
|
|
new_ids, removed = remove_one_occurrence([1, 2, 2, 3], 2)
|
|
assert removed and new_ids == ["1", "2", "3"]
|