A single enriched against the deluxe gets every source ID pointing at the deluxe, so the organizer filed it as e.g. track 2 of a 10-track album. Root cause: the canonical resolver only ever scored the editions already linked — the correct single was never even a candidate, and the misfit deluxe scored so low (0.1, below the 0.5 floor) that nothing got pinned and the priority-walk grabbed the deluxe anyway. Fix, in three tested layers: - resolve_canonical_for_album gains a fetch_alternates seam: when no linked edition clears the floor, it scores the source's OTHER editions of the same release and re-picks by best fit (dedup, injected, pure). - default_fetch_alternates lists the artist's editions and keeps the same-release ones (edition-blind name match: Deluxe / - Single / [Remastered] all collapse), returning their tracklists. Favors recall; the scorer is the precision gate. - _resolve_source does the misfit check inline: it fit-scores the walked edition and only on a clear misfit searches for a better edition, then persists the pin on apply (Track Number Repair + future runs agree). Cost-neutral and behavior- identical for well-fitting albums (no extra API calls); strict_source and the #758 manual lock are never overridden. Tests: +4 resolver (expand/no-expand/dedupe/back-compat), +7 alternates (name matcher + fetcher over fake APIs + cap), +3 organizer end-to-end (misfit->single +pin, well-fit->no-expand, strict->no-expand). 300 passed across the reorganize + canonical family, lint clean.
119 lines
5 KiB
Python
119 lines
5 KiB
Python
"""#767-2: the reorganizer's on-demand alternate-edition path.
|
|
|
|
When the walked edition (the first source we have an ID for) clearly misfits the
|
|
on-disk files — e.g. a 1-track single whose only ID points at the 10-track deluxe
|
|
— `_resolve_source` must find a better-fitting edition, use it for the plan, and
|
|
(on apply) persist the canonical pin. A well-fitting album must keep today's exact
|
|
behavior and never trigger an alternate fetch."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import core.library_reorganize as lr
|
|
import core.metadata.canonical_resolver as cr
|
|
|
|
# Provider-shaped raw tracklists (what get_album_tracks_for_source returns).
|
|
SINGLE_RAW = [{"name": "Scatterbrain", "track_number": 1, "duration_ms": 129_000}]
|
|
DELUXE_RAW = [{"name": "Intro", "track_number": 1, "duration_ms": 200_000}] + [
|
|
{"name": "Scatterbrain", "track_number": 2, "duration_ms": 130_000}
|
|
] + [
|
|
{"name": f"Bonus {i}", "track_number": i + 2, "duration_ms": 180_000}
|
|
for i in range(1, 9)
|
|
]
|
|
# Resolver-normalised shape (what default_fetch_tracklist returns).
|
|
SINGLE_NORM = [{"title": "Scatterbrain", "track_number": 1, "duration_ms": 129_000}]
|
|
DELUXE_NORM = [{"title": t["name"], "duration_ms": t["duration_ms"]} for t in DELUXE_RAW]
|
|
|
|
ALBUM_META = {
|
|
"sp_deluxe": {"name": "Scatterbrain (Deluxe)"},
|
|
"sp_single": {"name": "Scatterbrain - Single"},
|
|
}
|
|
TRACKLISTS = {"sp_deluxe": DELUXE_RAW, "sp_single": SINGLE_RAW}
|
|
|
|
|
|
def _wire(monkeypatch, *, alternates):
|
|
"""Patch the source-API seams the reorganizer + resolver funnel through."""
|
|
monkeypatch.setattr(lr, "get_source_priority", lambda primary: ["spotify"])
|
|
monkeypatch.setattr(lr, "get_album_for_source", lambda s, aid: ALBUM_META.get(aid))
|
|
monkeypatch.setattr(lr, "get_album_tracks_for_source", lambda s, aid: TRACKLISTS.get(aid))
|
|
# Resolver-internal fetchers (imported by name inside _resolve_better_edition).
|
|
norm = {"sp_deluxe": DELUXE_NORM, "sp_single": SINGLE_NORM}
|
|
monkeypatch.setattr(cr, "default_fetch_tracklist", lambda s, aid: norm.get(aid))
|
|
monkeypatch.setattr(cr, "default_fetch_alternates", alternates)
|
|
|
|
|
|
def test_misfit_single_resolves_to_the_single_edition(monkeypatch):
|
|
alt_calls = []
|
|
|
|
def alternates(source, aid, **kw):
|
|
alt_calls.append((source, aid))
|
|
return [
|
|
{"album_id": "sp_single", "tracks": SINGLE_NORM},
|
|
{"album_id": "sp_deluxe", "tracks": DELUXE_NORM},
|
|
]
|
|
|
|
_wire(monkeypatch, alternates=alternates)
|
|
pins = []
|
|
album_data = {
|
|
"spotify_album_id": "sp_deluxe", "title": "Scatterbrain",
|
|
"artist_id": "a1", "artist_name": "The Band",
|
|
}
|
|
file_tracks = [{"duration_ms": 129_000, "title": "Scatterbrain"}] # owns the single
|
|
|
|
source, api_album, items = lr._resolve_source(
|
|
album_data, "spotify",
|
|
file_tracks=file_tracks,
|
|
on_better_edition=lambda s, aid, sc: pins.append((s, aid, sc)),
|
|
)
|
|
|
|
assert source == "spotify"
|
|
assert api_album == ALBUM_META["sp_single"] # used the single, not the deluxe
|
|
assert len(items) == 1
|
|
assert alt_calls, "misfit must trigger an alternate-edition fetch"
|
|
assert pins and pins[0][1] == "sp_single", "apply must persist the better pin"
|
|
|
|
|
|
def test_well_fitting_album_keeps_walk_and_never_expands(monkeypatch):
|
|
alt_calls = []
|
|
|
|
def alternates(source, aid, **kw):
|
|
alt_calls.append((source, aid))
|
|
return [{"album_id": "sp_single", "tracks": SINGLE_NORM}]
|
|
|
|
_wire(monkeypatch, alternates=alternates)
|
|
pins = []
|
|
# The library actually IS the deluxe (10 matching tracks) -> walk fits -> no expand.
|
|
album_data = {
|
|
"spotify_album_id": "sp_deluxe", "title": "Scatterbrain (Deluxe)",
|
|
"artist_id": "a1", "artist_name": "The Band",
|
|
}
|
|
file_tracks = [{"duration_ms": t["duration_ms"], "title": t["name"]} for t in DELUXE_RAW]
|
|
|
|
source, api_album, items = lr._resolve_source(
|
|
album_data, "spotify",
|
|
file_tracks=file_tracks,
|
|
on_better_edition=lambda s, aid, sc: pins.append((s, aid, sc)),
|
|
)
|
|
|
|
assert source == "spotify" and api_album == ALBUM_META["sp_deluxe"]
|
|
assert alt_calls == [], "a well-fitting edition must not trigger any alternate fetch"
|
|
assert pins == [], "no pin written when the walk already fits"
|
|
|
|
|
|
def test_strict_source_never_expands(monkeypatch):
|
|
# User explicitly picked the source in the modal -> their choice wins, even on
|
|
# a misfit. No alternate search.
|
|
alt_calls = []
|
|
|
|
def alternates(source, aid, **kw):
|
|
alt_calls.append((source, aid))
|
|
return [{"album_id": "sp_single", "tracks": SINGLE_NORM}]
|
|
|
|
_wire(monkeypatch, alternates=alternates)
|
|
album_data = {"spotify_album_id": "sp_deluxe", "title": "Scatterbrain"}
|
|
file_tracks = [{"duration_ms": 129_000, "title": "Scatterbrain"}]
|
|
|
|
source, api_album, items = lr._resolve_source(
|
|
album_data, "spotify", strict_source=True, file_tracks=file_tracks,
|
|
)
|
|
assert source == "spotify" and api_album == ALBUM_META["sp_deluxe"]
|
|
assert alt_calls == [], "strict_source must not trigger alternate expansion"
|