#901: one-time backfill — stable ids for EXISTING file-import mirrored tracks

The mirror_playlist fix only assigns stable ids to newly-imported playlists, so a
user with an existing file-import playlist would still have empty-id rows (and dead
Find & Add matches) until a manual re-import. Add an idempotent startup backfill that
assigns the SAME stable id a fresh import would to any mirrored track missing one —
so existing matches start sticking with no re-import. Runs once per db/process (the
init is guarded), only touches empty-id rows (no-op afterward), native ids untouched.

Tests: backfill fills empty ids with the exact fresh-import id, is idempotent (2nd
run = 0), and leaves native ids alone.
This commit is contained in:
BoulderBadgeDad 2026-06-22 09:06:34 -07:00
parent 6e622d30f1
commit 1ad80d77a6
2 changed files with 66 additions and 0 deletions

View file

@ -992,6 +992,39 @@ class MusicDatabase:
raise
self._init_manual_library_match_table()
self._backfill_mirrored_track_source_ids()
def _backfill_mirrored_track_source_ids(self) -> int:
"""One-time, idempotent: assign a stable source_track_id to mirrored tracks
that have none (file-import / iTunes-only playlists imported before #901), so
their existing Find & Add matches start sticking without a manual re-import.
Only touches empty-id rows, so it's a no-op once they're filled."""
from core.playlists.source_refs import stable_source_track_id
updated = 0
try:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT id, track_name, artist_name, album_name
FROM mirrored_playlist_tracks
WHERE source_track_id IS NULL OR source_track_id = ''
""")
rows = cursor.fetchall()
for r in rows:
sid = stable_source_track_id({
'track_name': r['track_name'], 'artist_name': r['artist_name'],
'album_name': r['album_name']})
if sid:
cursor.execute(
"UPDATE mirrored_playlist_tracks SET source_track_id = ? WHERE id = ?",
(sid, r['id']))
updated += 1
conn.commit()
if updated:
logger.info("Backfilled stable source_track_id on %d mirrored tracks (#901)", updated)
except Exception as e:
logger.error("mirrored track source_id backfill failed: %s", e)
return updated
# Bump when the schema's generation meaningfully changes. Stamped into
# PRAGMA user_version as a backstop indicator; nothing GATES on it yet.

View file

@ -92,3 +92,36 @@ def test_native_ids_still_used_verbatim(tmp_path):
profile_id=1)
rows = db.get_mirrored_playlist_tracks(pid)
assert rows[0]["source_track_id"] == "spotify123" # native id untouched
def test_backfill_fills_existing_empty_ids_idempotently(tmp_path):
# #901 backfill: a file-import playlist mirrored BEFORE the fix has empty-id rows.
# The backfill assigns the SAME stable ids a fresh import would, so existing
# Find & Add matches start working without a re-import.
db = MusicDatabase(str(tmp_path / "music.db"))
pid = db.mirror_playlist(source="file", source_playlist_id="old", name="Old",
tracks=[{"track_name": "Slow Ride", "artist_name": "Foghat"}], profile_id=1)
# simulate a pre-fix row: blank out the id
with db._get_connection() as conn:
conn.execute("UPDATE mirrored_playlist_tracks SET source_track_id = '' WHERE playlist_id = ?", (pid,))
conn.commit()
n = db._backfill_mirrored_track_source_ids()
assert n == 1
rows = db.get_mirrored_playlist_tracks(pid)
from core.playlists.source_refs import stable_source_track_id
assert rows[0]["source_track_id"] == stable_source_track_id(
{"track_name": "Slow Ride", "artist_name": "Foghat"}) # same id a fresh import gives
# idempotent — second run touches nothing
assert db._backfill_mirrored_track_source_ids() == 0
def test_backfill_leaves_native_ids_untouched(tmp_path):
db = MusicDatabase(str(tmp_path / "music.db"))
pid = db.mirror_playlist(source="spotify", source_playlist_id="sp", name="Sp",
tracks=[{"track_name": "S", "artist_name": "A", "source_track_id": "spotify123"}],
profile_id=1)
db._backfill_mirrored_track_source_ids()
rows = db.get_mirrored_playlist_tracks(pid)
assert rows[0]["source_track_id"] == "spotify123"