soulsync/tests/test_history_match.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

63 lines
2.9 KiB
Python

"""Pure matcher that re-links a moved file to its download-history row (#934)."""
from core.downloads.history_match import pick_history_row, like_filename_filter
# (id, file_path, title, download_source)
def _row(i, path, title='', source='soulseek'):
return (i, path, title, source)
def test_exact_current_path_wins():
cands = [_row(1, '/old/song.flac', 'Song'), _row(2, '/lib/song.flac', 'Song')]
assert pick_history_row(cands, current_paths=('/lib/song.flac', None),
basename='song.flac', title='Song') == 2
def test_falls_back_to_filename_when_path_drifted():
# history has the OLD import path; scanner only knows the NEW library path.
cands = [_row(7, '/downloads/transfer/Artist/01 - Song.flac', 'Song')]
assert pick_history_row(cands, current_paths=('/music/Artist/Album/01 - Song.flac', None),
basename='01 - Song.flac', title='Song') == 7
def test_title_guard_blocks_shared_filename_collision():
# two different songs both named "01 - Intro.flac" — must NOT heal the wrong one.
cands = [_row(1, '/a/01 - Intro.flac', 'Album A Intro'),
_row(2, '/b/01 - Intro.flac', 'Album B Intro')]
got = pick_history_row(cands, current_paths=('/c/01 - Intro.flac', None),
basename='01 - Intro.flac', title='Album B Intro')
assert got == 2
def test_title_drift_still_matches_after_normalization():
cands = [_row(5, '/old/track.flac', 'Song (Remastered)')]
assert pick_history_row(cands, current_paths=('/new/track.flac', None),
basename='track.flac', title='song remastered') == 5
def test_prefers_real_download_row_over_synthetic_scan_row():
# the #934 collapse: a real download row (drifted path) + a synthetic scan dup at the
# exact current path. The REAL row must win, so the synthetic one can be deleted.
cands = [_row(10, '/old/song.flac', 'Song', source='soulseek'),
_row(11, '/lib/song.flac', 'Song', source='acoustid_scan')]
assert pick_history_row(cands, current_paths=('/lib/song.flac', None),
basename='song.flac', title='Song') == 10
def test_no_basename_match_returns_none():
cands = [_row(1, '/x/other.flac', 'Other')]
assert pick_history_row(cands, current_paths=('/x/wanted.flac', None),
basename='wanted.flac', title='Wanted') is None
def test_filename_only_substring_is_not_a_match():
# '/x/mysong.flac' must NOT satisfy basename 'song.flac'
cands = [_row(1, '/x/mysong.flac', 'My Song')]
assert pick_history_row(cands, current_paths=('/x/song.flac', None),
basename='song.flac', title='Song') is None
def test_like_filter_escapes_metacharacters():
# underscores/percents in filenames must not become LIKE wildcards
assert like_filename_filter('a_b%c.flac') == r'%a\_b\%c.flac'