diff --git a/worker/lyra_worker/scan.py b/worker/lyra_worker/scan.py index 820b7b5..6a7c983 100644 --- a/worker/lyra_worker/scan.py +++ b/worker/lyra_worker/scan.py @@ -169,7 +169,17 @@ def scan_chunk(conn: psycopg.Connection, resolver, probe: AudioProbe, browser, imported = skipped = 0 browsed: set[str] = set() # artists whose discography we've populated this chunk for item_id, artist_name, album_folder, album_path in items: - outcome = _process_album(conn, resolver, probe, browser, artist_name, album_folder, album_path, browsed) + try: + outcome = _process_album(conn, resolver, probe, browser, artist_name, album_folder, album_path, browsed) + except Exception as e: + # a single unreadable/corrupt album (e.g. a truncated file mutagen chokes on) must + # not abort the whole scan — skip it, mark it done, and keep going. + try: + conn.rollback() + except Exception: + pass + print(f"scan: skipping {artist_name}/{album_folder}: {e}", flush=True) + outcome = "skipped" if outcome == "imported": imported += 1 elif outcome == "skipped": diff --git a/worker/tests/test_scan.py b/worker/tests/test_scan.py index 2d5e9e0..3bfaf40 100644 --- a/worker/tests/test_scan.py +++ b/worker/tests/test_scan.py @@ -59,6 +59,29 @@ def test_scan_records_have_monitored_and_follows_artist(conn, tmp_path): assert cur.fetchone()[0] is False +def test_scan_skips_a_corrupt_album_and_captures_tracknames(conn, tmp_path): + _album(tmp_path, "Good Artist", "Good Album (2020)", files=("01 One.flac", "02 Two.flac")) + _album(tmp_path, "Bad Artist", "Bad Album (2020)") + + class RaisingProbe: + def probe(self, path): + if "Bad Artist" in path: # a truncated/corrupt file mutagen chokes on + raise RuntimeError("file said 4 bytes, read 0 bytes") + return ProbeResult(artist="", album="", fmt="FLAC", quality_class=3) + + resolver = FakeResolver({ + ("Good Artist", "Good Album"): MBTarget( + artist="Good Artist", album="Good Album", year=2020, rg_mbid="rgG", artist_mbid="aG"), + }) + result = scan_library(conn, resolver, RaisingProbe(), FakeMbBrowser(), dest_root=str(tmp_path)) + + # the corrupt album is skipped, the good one still imports — the scan completes, not aborts + assert (result.imported, result.skipped) == (1, 1) + with conn.cursor() as cur: + cur.execute('SELECT "trackNames" FROM "LibraryItem" WHERE artist=%s', ("Good Artist",)) + assert cur.fetchone()[0] == ["01 One.flac", "02 Two.flac"] # on-disk tracks captured at scan + + def test_scan_skips_unmatched_albums(conn, tmp_path): _album(tmp_path, "Obscure", "Nope (1999)") result = scan_library(conn, FakeResolver({}), FakeProbe(), FakeMbBrowser(), dest_root=str(tmp_path))