Watchlist: repair iTunes ids that are actually Deezer ids (the 37725457 corruption, proven live)
37725457 fixed _match_to_itunes to use the real iTunes client and flagged
the cross-source corruption as a possibility. Boulder's live DB proves it
happened: 6 of his 9 watchlist "iTunes" ids EQUAL the artist's Deezer id
(Taylor Swift's "iTunes" id was her Deezer id 12246; the real one is
159260351) — written back when the misnamed MetadataService.itunes slot
held a DeezerClient. The June-4 batch (Green Day, SOAD, Vulfpeck, ...) got
NULL instead because the slot now holds the Spotify primary.
The fix alone can't heal those rows: the backfill only fills EMPTY ids, so
a wrong non-empty id is permanent. New migration clears itunes_artist_id
where it equals deezer_artist_id (the corruption signature — distinct id
spaces, so a legitimate equal pair is effectively impossible, and the worst
case is a NULL that re-matches correctly on the next scan). Idempotent by
construction; similar_artists checked clean (its backfill always used the
registry correctly).
Tests: corrupted row cleared / legit + no-deezer rows kept / idempotent —
via a real re-init with the per-process init memo cleared (an app restart).
This commit is contained in:
parent
377254572b
commit
58df4632c4
2 changed files with 105 additions and 0 deletions
|
|
@ -396,6 +396,7 @@ class MusicDatabase:
|
|||
|
||||
# Add per-artist preferred_metadata_source column (migration)
|
||||
self._add_watchlist_preferred_metadata_source_column(cursor)
|
||||
self._clear_deezer_ids_stored_as_itunes(cursor)
|
||||
|
||||
# Make spotify_artist_id nullable for iTunes-only artists (migration)
|
||||
self._fix_watchlist_spotify_id_nullable(cursor)
|
||||
|
|
@ -2114,6 +2115,38 @@ class MusicDatabase:
|
|||
except Exception as e:
|
||||
logger.error(f"Error adding preferred_metadata_source column to watchlist_artists: {e}")
|
||||
|
||||
def _clear_deezer_ids_stored_as_itunes(self, cursor):
|
||||
"""Repair: watchlist iTunes ids that are actually Deezer ids.
|
||||
|
||||
The watchlist scanner's _match_to_itunes used to search via
|
||||
MetadataService.itunes — which holds the PRIMARY source's client, not
|
||||
iTunes — so with a Deezer primary it stored DEEZER artist ids in
|
||||
itunes_artist_id (verified live: Taylor Swift's "iTunes" id was her
|
||||
Deezer id 12246; real one is 159260351). The backfill only fills
|
||||
EMPTY ids, so these wrong ids would never self-heal. The corruption
|
||||
signature is itunes == deezer (distinct id spaces — a legit equal
|
||||
pair is effectively impossible; worst case is a NULL that re-matches
|
||||
correctly on the next scan). Idempotent: clearing kills the equality.
|
||||
"""
|
||||
try:
|
||||
cursor.execute("PRAGMA table_info(watchlist_artists)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
if 'itunes_artist_id' not in columns or 'deezer_artist_id' not in columns:
|
||||
return
|
||||
cursor.execute("""
|
||||
UPDATE watchlist_artists SET itunes_artist_id = NULL
|
||||
WHERE itunes_artist_id = deezer_artist_id
|
||||
AND deezer_artist_id IS NOT NULL AND deezer_artist_id != ''
|
||||
""")
|
||||
if cursor.rowcount:
|
||||
logger.info(
|
||||
"Cleared %d watchlist iTunes id(s) that were actually Deezer ids "
|
||||
"(pre-fix _match_to_itunes searched the primary source); they "
|
||||
"re-match via real iTunes on the next watchlist scan",
|
||||
cursor.rowcount)
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing deezer-as-itunes watchlist ids: {e}")
|
||||
|
||||
def _add_similar_artists_last_featured_column(self, cursor):
|
||||
"""Add last_featured column to similar_artists for hero slider cycling"""
|
||||
try:
|
||||
|
|
|
|||
72
tests/test_watchlist_itunes_id_repair.py
Normal file
72
tests/test_watchlist_itunes_id_repair.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Repair migration: watchlist iTunes ids that are actually Deezer ids.
|
||||
|
||||
_match_to_itunes used to search the PRIMARY source's client (the misnamed
|
||||
MetadataService.itunes slot) and store that source's artist id in the
|
||||
itunes column — with a Deezer primary, Deezer ids landed as "iTunes" ids
|
||||
(verified in a live DB: 6 of 9 rows). Since the backfill only fills EMPTY
|
||||
ids, the migration must clear the corrupted ones (signature: itunes ==
|
||||
deezer) so the fixed matcher can re-fill them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
import database.music_database as mdb
|
||||
from database.music_database import MusicDatabase
|
||||
|
||||
|
||||
def _open_raw(db_path):
|
||||
return sqlite3.connect(db_path)
|
||||
|
||||
|
||||
def _reinit(db_path):
|
||||
"""Schema init runs once per process per path (module memo) — clear the
|
||||
memo so re-construction replays the migration block like a real app
|
||||
restart (fresh process) would."""
|
||||
mdb._database_initialized_paths.clear()
|
||||
return MusicDatabase(db_path)
|
||||
|
||||
|
||||
def test_deezer_as_itunes_ids_cleared_and_legit_kept(tmp_path):
|
||||
db_path = str(tmp_path / 'm.db')
|
||||
MusicDatabase(db_path) # create schema (migration runs, table empty)
|
||||
|
||||
conn = _open_raw(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("""INSERT INTO watchlist_artists (artist_name, spotify_artist_id, itunes_artist_id, deezer_artist_id)
|
||||
VALUES ('Taylor Swift', 'sp1', '12246', '12246')""") # corrupted
|
||||
c.execute("""INSERT INTO watchlist_artists (artist_name, spotify_artist_id, itunes_artist_id, deezer_artist_id)
|
||||
VALUES ('Eminem', 'sp2', '111051', '13')""") # legit
|
||||
c.execute("""INSERT INTO watchlist_artists (artist_name, spotify_artist_id, itunes_artist_id, deezer_artist_id)
|
||||
VALUES ('NoDeezer', 'sp3', '999', NULL)""") # no deezer id -> keep
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
_reinit(db_path) # like an app restart -> migration sweeps existing rows
|
||||
|
||||
conn = _open_raw(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT artist_name, itunes_artist_id FROM watchlist_artists ORDER BY artist_name")
|
||||
got = dict(c.fetchall())
|
||||
conn.close()
|
||||
|
||||
assert got['Taylor Swift'] is None # corruption cleared
|
||||
assert got['Eminem'] == '111051' # real id untouched
|
||||
assert got['NoDeezer'] == '999' # equality needs a deezer id
|
||||
|
||||
|
||||
def test_migration_idempotent(tmp_path):
|
||||
db_path = str(tmp_path / 'm.db')
|
||||
MusicDatabase(db_path)
|
||||
conn = _open_raw(db_path)
|
||||
conn.execute("""INSERT INTO watchlist_artists (artist_name, spotify_artist_id, itunes_artist_id, deezer_artist_id)
|
||||
VALUES ('A', 'sp', '5', '5')""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
_reinit(db_path)
|
||||
_reinit(db_path) # second run: nothing left to clear, no error
|
||||
conn = _open_raw(db_path)
|
||||
val = conn.execute("SELECT itunes_artist_id FROM watchlist_artists").fetchone()[0]
|
||||
conn.close()
|
||||
assert val is None
|
||||
Loading…
Reference in a new issue