soulsync/tests/test_source_title.py
BoulderBadgeDad bba0836324 Fix #768: playlist sync editor refusing to match certain tracks
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).
2026-06-02 10:16:21 -07:00

113 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Extreme test battery for source-track normalization (#768).
YouTube/streaming sources carry "Artist - Song" titles and "Official Artist"/
"Artist - Topic"/"ArtistVEVO" artist names; the library has clean metadata, so
matching fails and tracks are reported missing. These helpers strip the
decoration. The batteries below pin both the positives (must clean) and the
negatives (must NOT mangle real titles/artists).
"""
from __future__ import annotations
import pytest
from core.text.source_title import (
canonical_source_track,
clean_source_artist,
strip_artist_prefix,
)
# ── clean_source_artist ───────────────────────────────────────────────────
@pytest.mark.parametrize("raw,expected", [
("Official Arctic Monkeys", "Arctic Monkeys"),
("The Official Weeknd", "Weeknd"),
("Arctic Monkeys - Topic", "Arctic Monkeys"),
("Coldplay - Topic", "Coldplay"),
("ColdplayVEVO", "Coldplay"),
("Coldplay VEVO", "Coldplay"),
("EminemVEVO", "Eminem"),
(" Official Radiohead ", "Radiohead"),
])
def test_clean_source_artist_strips_decoration(raw, expected):
assert clean_source_artist(raw) == expected
@pytest.mark.parametrize("raw", [
"Arctic Monkeys", # already clean
"Coldplay",
"Twenty One Pilots",
"Death",
"U2",
"AJR", # would be emptied by a naive vevo/official strip
"",
])
def test_clean_source_artist_leaves_clean_names(raw):
assert clean_source_artist(raw) == raw
def test_clean_source_artist_never_empties():
# Pathological: artist that is ONLY decoration must not become "".
assert clean_source_artist("VEVO") == "VEVO"
assert clean_source_artist("Official ") == "Official"
# ── strip_artist_prefix ───────────────────────────────────────────────────
@pytest.mark.parametrize("title,artist,expected", [
("Arctic Monkeys - Do I Wanna Know?", "Arctic Monkeys", "Do I Wanna Know?"),
("Death - Pull the Plug", "Death", "Pull the Plug"),
("Coldplay Yellow", "Coldplay", "Yellow"), # en dash
("Coldplay — Yellow", "Coldplay", "Yellow"), # em dash
("Eminem: Lose Yourself", "Eminem", "Lose Yourself"), # colon
("Daft Punk | Get Lucky", "Daft Punk", "Get Lucky"), # pipe
("ARCTIC MONKEYS - 505", "arctic monkeys", "505"), # case-fold
])
def test_strip_artist_prefix_strips_when_prefix_is_artist(title, artist, expected):
assert strip_artist_prefix(title, artist) == expected
@pytest.mark.parametrize("title,artist", [
("Marvin Gaye", "Charlie Puth"), # title is not "artist - ..."
("Do I Wanna Know?", "Arctic Monkeys"), # already clean
("Self-Titled", "Whoever"), # hyphen w/o spaces — not a sep
("Jay-Z Anthem", "Somebody"), # hyphen inside a word
("Song - Live", "Coldplay"), # prefix "Song" != artist
("Stay With Me", "Sam Smith"),
("", "Arctic Monkeys"),
("Arctic Monkeys -", "Arctic Monkeys"), # nothing after sep -> unchanged
])
def test_strip_artist_prefix_leaves_others_untouched(title, artist):
assert strip_artist_prefix(title, artist) == title
def test_strip_only_first_separator():
# "Artist - Song - Remix" -> strip only the leading artist segment.
assert strip_artist_prefix("Gorillaz - Feel Good Inc - Remix", "Gorillaz") == "Feel Good Inc - Remix"
# ── canonical_source_track (combined) ─────────────────────────────────────
def test_canonical_handles_youtube_channel_and_prefix():
# The reported case: channel-name artist + "Artist - Title" title.
title, artist = canonical_source_track(
"Arctic Monkeys - Do I Wanna Know?", "Official Arctic Monkeys",
)
assert title == "Do I Wanna Know?"
assert artist == "Arctic Monkeys"
def test_canonical_strips_prefix_using_raw_artist_when_clean_differs():
# Title prefixed with the channel-style raw artist itself.
title, artist = canonical_source_track(
"Official Arctic Monkeys - 505", "Official Arctic Monkeys",
)
# cleaned artist is "Arctic Monkeys"; raw prefix "Official Arctic Monkeys"
# also stripped via the raw-artist fallback.
assert title == "505"
assert artist == "Arctic Monkeys"
def test_canonical_noop_on_clean_input():
assert canonical_source_track("Yellow", "Coldplay") == ("Yellow", "Coldplay")