fix(worker): a corrupt/unreadable album no longer aborts the whole scan
Live-testing #9 surfaced a pre-existing gap: one truncated file mutagen chokes on ("file said 4 bytes, read 0 bytes") raised out of _process_album and aborted the entire library scan (worklist abandoned). Now scan_chunk catches a per-album error, rolls back, logs it, counts it as skipped, marks the item done, and continues — so a single bad file can't block the rest of the library. worker tests: new case asserts a corrupt album is skipped while a good one still imports (and captures its on-disk trackNames). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,7 +169,17 @@ def scan_chunk(conn: psycopg.Connection, resolver, probe: AudioProbe, browser,
|
|||||||
imported = skipped = 0
|
imported = skipped = 0
|
||||||
browsed: set[str] = set() # artists whose discography we've populated this chunk
|
browsed: set[str] = set() # artists whose discography we've populated this chunk
|
||||||
for item_id, artist_name, album_folder, album_path in items:
|
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":
|
if outcome == "imported":
|
||||||
imported += 1
|
imported += 1
|
||||||
elif outcome == "skipped":
|
elif outcome == "skipped":
|
||||||
|
|||||||
@@ -59,6 +59,29 @@ def test_scan_records_have_monitored_and_follows_artist(conn, tmp_path):
|
|||||||
assert cur.fetchone()[0] is False
|
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):
|
def test_scan_skips_unmatched_albums(conn, tmp_path):
|
||||||
_album(tmp_path, "Obscure", "Nope (1999)")
|
_album(tmp_path, "Obscure", "Nope (1999)")
|
||||||
result = scan_library(conn, FakeResolver({}), FakeProbe(), FakeMbBrowser(), dest_root=str(tmp_path))
|
result = scan_library(conn, FakeResolver({}), FakeProbe(), FakeMbBrowser(), dest_root=str(tmp_path))
|
||||||
|
|||||||
Reference in New Issue
Block a user