Turns the Stage-1 scorer into an end-to-end resolver + persists the result.
Still DORMANT — no consumer reads it yet, so zero behavior change.
- core/metadata/canonical_resolver.py — resolve_canonical_for_album(): builds
candidate releases from the album's per-source IDs (in source-priority order),
fetches each tracklist via an INJECTED fetch_tracklist (so it's unit-testable
without live APIs), scores them with pick_canonical_release, and returns the
best-fit {source, album_id, score}. Skips sources with no id / failed fetch;
returns None when there are no files, no candidates, or nothing clears the
confidence floor.
- database/music_database.py — set_album_canonical() / get_album_canonical()
write/read the Stage-1 columns. get returns None when unresolved, which every
consumer will treat as "fall back to today's behavior".
Tests: tests/test_canonical_resolver.py (7) — best-fit beats priority, priority
breaks true ties, skips missing-id/failed-fetch sources, None on
no-candidates/no-files/below-floor, score rounding. tests/test_canonical_db.py
(4) — set/get round-trip incl. timestamp, unresolved -> None, overwrite,
missing-album -> False. 34 canonical + DB-migration tests pass.
Remaining for Stage 2 (the trigger): read on-disk file durations/titles for an
album, gather its source IDs, call the resolver, store — wired via a backfill
repair job + an enrichment hook. Then Stages 3-4 wire the Reorganizer and Track
Number Repair to READ the pinned canonical.
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
"""DB persistence for canonical album version (#765 Stage 2)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from database.music_database import MusicDatabase
|
|
|
|
|
|
def _album(db, album_id="alb_evolve"):
|
|
# id columns are TEXT (GUID) post-migration, so insert explicit ids and a
|
|
# valid FK rather than relying on integer rowids.
|
|
conn = db._get_connection()
|
|
cur = conn.cursor()
|
|
cur.execute("INSERT INTO artists (id, name) VALUES ('art_id', 'Imagine Dragons')")
|
|
cur.execute(
|
|
"INSERT INTO albums (id, title, artist_id) VALUES (?, 'Evolve', 'art_id')",
|
|
(album_id,),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return album_id
|
|
|
|
|
|
def test_set_then_get_roundtrip(tmp_path):
|
|
db = MusicDatabase(str(tmp_path / "m.db"))
|
|
album_id = _album(db)
|
|
|
|
assert db.get_album_canonical(album_id) is None # unresolved by default
|
|
|
|
assert db.set_album_canonical(album_id, "spotify", "sp_evolve_123", 0.97) is True
|
|
got = db.get_album_canonical(album_id)
|
|
assert got["source"] == "spotify"
|
|
assert got["album_id"] == "sp_evolve_123"
|
|
assert abs(got["score"] - 0.97) < 1e-6
|
|
assert got["resolved_at"] # timestamp populated
|
|
|
|
|
|
def test_get_unresolved_returns_none(tmp_path):
|
|
db = MusicDatabase(str(tmp_path / "m.db"))
|
|
album_id = _album(db)
|
|
assert db.get_album_canonical(album_id) is None
|
|
|
|
|
|
def test_set_overwrites_previous(tmp_path):
|
|
db = MusicDatabase(str(tmp_path / "m.db"))
|
|
album_id = _album(db)
|
|
db.set_album_canonical(album_id, "spotify", "old", 0.6)
|
|
db.set_album_canonical(album_id, "musicbrainz", "new", 0.95)
|
|
got = db.get_album_canonical(album_id)
|
|
assert got["source"] == "musicbrainz" and got["album_id"] == "new"
|
|
|
|
|
|
def test_set_on_missing_album_returns_false(tmp_path):
|
|
db = MusicDatabase(str(tmp_path / "m.db"))
|
|
assert db.set_album_canonical(999999, "spotify", "x", 0.9) is False
|