From 4c1d3c639ee5feabc6459a9009f00e4b3430953f Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sat, 11 Jul 2026 18:12:37 +0200 Subject: [PATCH] fix: scan backfills release type metadata onto pre-existing rows MonitoredRelease rows created before an artist's discography was populated (e.g. an owned album recorded before _populate_discography ran) never got primaryType/secondaryTypes backfilled, wrongly excluding them from the studio-only core view. _populate_discography now DO UPDATEs the MB type metadata on conflict instead of DO NOTHING, leaving monitored/state/currentQualityClass/firstGrabbedAt untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- worker/lyra_worker/scan.py | 6 +++++- worker/tests/test_scan.py | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/worker/lyra_worker/scan.py b/worker/lyra_worker/scan.py index d270eea..ccb0db5 100644 --- a/worker/lyra_worker/scan.py +++ b/worker/lyra_worker/scan.py @@ -53,7 +53,11 @@ def _populate_discography(conn, browser, watched_id, artist_mbid: str, artist_na 'INSERT INTO "MonitoredRelease" (id, "watchedArtistId", "artistMbid", "artistName", ' '"rgMbid", album, "primaryType", "secondaryTypes", "firstReleaseDate", monitored, state, "createdAt") ' "VALUES (gen_random_uuid()::text, %s, %s, %s, %s, %s, %s, %s, %s, false, 'wanted', now()) " - 'ON CONFLICT ("rgMbid") DO NOTHING', + 'ON CONFLICT ("rgMbid") DO UPDATE SET ' + ' "primaryType" = EXCLUDED."primaryType", ' + ' "secondaryTypes" = EXCLUDED."secondaryTypes", ' + ' "firstReleaseDate" = EXCLUDED."firstReleaseDate", ' + ' "watchedArtistId" = COALESCE("MonitoredRelease"."watchedArtistId", EXCLUDED."watchedArtistId")', (watched_id, artist_mbid, artist_name, rg.rg_mbid, rg.title, rg.primary_type or None, list(rg.secondary_types), rg.first_release_date or None), ) diff --git a/worker/tests/test_scan.py b/worker/tests/test_scan.py index 810c474..2d5e9e0 100644 --- a/worker/tests/test_scan.py +++ b/worker/tests/test_scan.py @@ -142,3 +142,29 @@ def test_scan_populates_full_discography_unmonitored(conn, tmp_path): assert cur.fetchone() == (True, "fulfilled") # owned album stays monitored+fulfilled cur.execute('SELECT monitored, state FROM "MonitoredRelease" WHERE "rgMbid"=%s', ("rg-other",)) assert cur.fetchone() == (False, "wanted") # the rest are unmonitored + + +def test_scan_backfills_type_on_preexisting_typeless_owned_row(conn, tmp_path): + from lyra_worker.adapters.fakes import FakeMbBrowser + from lyra_worker.browser import ReleaseGroupInfo + # simulate an owned row recorded by an older scan: monitored+fulfilled but NULL primaryType + with conn.cursor() as cur: + cur.execute( + 'INSERT INTO "MonitoredRelease" (id, "artistMbid", "artistName", "rgMbid", album, ' + '"secondaryTypes", monitored, state, "currentQualityClass", "createdAt") ' + "VALUES (gen_random_uuid()::text, 'a50', '50 Cent', 'rg-owned', 'Get Rich or Die Tryin''', " + "'{}', true, 'fulfilled', 2, now())" + ) + conn.commit() + _album(tmp_path, "50 Cent", "Get Rich or Die Tryin' (2003)") + resolver = FakeResolver({ + ("50 Cent", "Get Rich or Die Tryin'"): MBTarget( + artist="50 Cent", album="Get Rich or Die Tryin'", year=2003, rg_mbid="rg-owned", artist_mbid="a50"), + }) + browser = FakeMbBrowser(releases={"a50": [ + ReleaseGroupInfo("rg-owned", "Get Rich or Die Tryin'", "Album", (), "2003-02-06"), + ]}) + scan_library(conn, resolver, FakeProbe(quality_class=2), browser, dest_root=str(tmp_path)) + with conn.cursor() as cur: + cur.execute('SELECT "primaryType", monitored, state, "currentQualityClass" FROM "MonitoredRelease" WHERE "rgMbid"=%s', ("rg-owned",)) + assert cur.fetchone() == ("Album", True, "fulfilled", 2) # type backfilled, state preserved