soulsync/tests/test_acoustid_history_heal.py
BoulderBadgeDad 3a92571c71 downloads: stop AcoustID scan duplicating history rows / leaving verified tracks 'unverified' (#934)
the AcoustID scanner matched library_history rows by EXACT file_path, but that path is
frozen at import time while the file moves afterward (media-server import / reorganize) —
so tracks.file_path (what the scan reads) no longer equals it. two failures resulted, both
introduced in 37ea6604: verified status never reached the history row (verified tracks kept
showing 'unverified'), and a fresh acoustid_scan row was INSERTed every run (5551 rows for
3675 songs).

- new pure, tested matcher (core/downloads/history_match.py): exact path → filename guarded
  by title; prefers a real download row over a synthetic scan row.
- _persist_status now HEALS the matched row's path + status (so future scans match cleanly),
  DELETES synthetic acoustid_scan duplicates by exact path (collision-free, never a real row),
  and inserts only when the file genuinely has no row.
- a full AcoustID job now self-cleans existing duplicates — no destructive bulk migration.

8 matcher + 4 real-DB heal/dedup/insert tests; existing scanner tests updated to the new
seam (heal vs insert). 1076 acoustid/verification/download tests green.
2026-06-27 09:52:39 -07:00

95 lines
4.3 KiB
Python

"""#934: the AcoustID scanner heals the download-history row when the file moved,
instead of leaving it stuck 'unverified' and inserting duplicate scan rows.
Seeds the exact bug shape (a real download row at the OLD import path + a synthetic
'acoustid_scan' duplicate at the NEW library path) and drives the real _persist_status,
asserting it collapses to one correct, verified row.
"""
from __future__ import annotations
import types
import pytest
from core.repair_jobs.acoustid_scanner import AcoustIDScannerJob
from database.music_database import MusicDatabase
@pytest.fixture()
def db(tmp_path):
return MusicDatabase(str(tmp_path / 'm.db'))
def _scanner():
# _persist_status uses only `context`, never instance state — bypass __init__.
return AcoustIDScannerJob.__new__(AcoustIDScannerJob)
def _rows(db):
with db._get_connection() as conn:
return [dict(r) for r in conn.execute(
"SELECT file_path, download_source, verification_status FROM library_history")]
OLD = '/downloads/transfer/Artist/01 - Song.flac' # frozen import path in history
NEW = '/music/Artist/Album/01 - Song.flac' # where the file lives now (tracks path)
def test_verify_heals_drifted_row_and_drops_synthetic_dup(db):
# the bug's leftover state: a stuck real row + a synthetic scan dup for the same song.
db.add_library_history_entry('download', 'Song', file_path=OLD,
download_source='soulseek', verification_status='unverified')
db.add_library_history_entry('download', 'Song', file_path=NEW,
download_source='acoustid_scan', verification_status='unverified')
_scanner()._persist_status(
types.SimpleNamespace(db=db), track_id='t1', fpath=NEW, db_path=NEW,
status='verified', write_tag=False, expected={'title': 'Song'})
rows = _rows(db)
assert len(rows) == 1 # synthetic dup deleted, no new insert
assert rows[0]['download_source'] == 'soulseek' # the REAL row survived
assert rows[0]['verification_status'] == 'verified'
assert rows[0]['file_path'] == NEW # path healed to current location
def test_idempotent_no_growth_on_rescan(db):
db.add_library_history_entry('download', 'Song', file_path=OLD,
download_source='soulseek', verification_status='unverified')
ctx = types.SimpleNamespace(db=db)
for _ in range(3):
_scanner()._persist_status(ctx, track_id='t1', fpath=NEW, db_path=NEW,
status='verified', write_tag=False, expected={'title': 'Song'})
rows = _rows(db)
assert len(rows) == 1 and rows[0]['verification_status'] == 'verified'
def test_unknown_file_inserts_one_row_then_dedups(db):
# a file SoulSync never downloaded → first scan inserts one review-queue row...
ctx = types.SimpleNamespace(db=db)
_scanner()._persist_status(ctx, track_id='t1', fpath=NEW, db_path=NEW,
status='unverified', write_tag=False,
expected={'title': 'Song', 'artist': 'Artist'})
assert len(_rows(db)) == 1
# ...and a rescan matches it (no duplicate).
_scanner()._persist_status(ctx, track_id='t1', fpath=NEW, db_path=NEW,
status='unverified', write_tag=False,
expected={'title': 'Song', 'artist': 'Artist'})
rows = _rows(db)
assert len(rows) == 1 and rows[0]['download_source'] == 'acoustid_scan'
def test_does_not_heal_wrong_song_with_same_filename(db):
# different song, same filename, different title → must stay untouched (no false heal).
db.add_library_history_entry('download', 'A Different Song', file_path='/other/01 - Song.flac',
download_source='soulseek', verification_status='verified')
_scanner()._persist_status(
types.SimpleNamespace(db=db), track_id='t1', fpath=NEW, db_path=NEW,
status='unverified', write_tag=False, expected={'title': 'Song'})
rows = _rows(db)
# the unrelated row is untouched, and the unknown file got its own new row.
paths = {r['file_path'] for r in rows}
assert '/other/01 - Song.flac' in paths and NEW in paths
other = next(r for r in rows if r['file_path'] == '/other/01 - Song.flac')
assert other['verification_status'] == 'verified' # not corrupted