Repair stale media schema during refresh
Ensure upgraded databases have the tracks.file_size and albums.api_track_count columns after all legacy migrations run. Add defensive repair paths for Jellyfin track imports and album track-count caching so stale schemas self-heal instead of dropping full-refresh track imports. Tests cover legacy schema repair and api_track_count self-repair.
This commit is contained in:
parent
8012f41ef7
commit
f1d4f78e0e
4 changed files with 122 additions and 0 deletions
|
|
@ -64,6 +64,17 @@ def set_album_api_track_count(cursor, album_id, count):
|
||||||
(count, album_id),
|
(count, album_id),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if "api_track_count" in str(e) and "no such column" in str(e).lower():
|
||||||
|
try:
|
||||||
|
cursor.execute("ALTER TABLE albums ADD COLUMN api_track_count INTEGER DEFAULT NULL")
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE albums SET api_track_count = ? WHERE id = ?",
|
||||||
|
(count, album_id),
|
||||||
|
)
|
||||||
|
logger.info("Repaired missing api_track_count column while caching album track count")
|
||||||
|
return
|
||||||
|
except Exception as repair_error:
|
||||||
|
e = repair_error
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Failed to cache api_track_count for album %s: %s", album_id, e
|
"Failed to cache api_track_count for album %s: %s", album_id, e
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -766,6 +766,8 @@ class MusicDatabase:
|
||||||
except Exception as ps_err:
|
except Exception as ps_err:
|
||||||
logger.error(f"Personalized-playlist schema init failed: {ps_err}")
|
logger.error(f"Personalized-playlist schema init failed: {ps_err}")
|
||||||
|
|
||||||
|
self._ensure_core_media_schema_columns(cursor)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info("Database initialized successfully")
|
logger.info("Database initialized successfully")
|
||||||
|
|
||||||
|
|
@ -775,6 +777,29 @@ class MusicDatabase:
|
||||||
|
|
||||||
self._init_manual_library_match_table()
|
self._init_manual_library_match_table()
|
||||||
|
|
||||||
|
def _ensure_core_media_schema_columns(self, cursor):
|
||||||
|
"""Repair required media-library columns that older migrations may miss.
|
||||||
|
|
||||||
|
A few legacy migrations rebuild artists/albums/tracks in place. Newer
|
||||||
|
installs get these columns from CREATE TABLE, but upgraded databases can
|
||||||
|
occasionally miss one if a previous migration path failed or was marked
|
||||||
|
complete before the column existed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cursor.execute("PRAGMA table_info(tracks)")
|
||||||
|
track_cols = {c[1] for c in cursor.fetchall()}
|
||||||
|
if track_cols and 'file_size' not in track_cols:
|
||||||
|
cursor.execute("ALTER TABLE tracks ADD COLUMN file_size INTEGER")
|
||||||
|
logger.info("Repaired missing file_size column on tracks table")
|
||||||
|
|
||||||
|
cursor.execute("PRAGMA table_info(albums)")
|
||||||
|
album_cols = {c[1] for c in cursor.fetchall()}
|
||||||
|
if album_cols and 'api_track_count' not in album_cols:
|
||||||
|
cursor.execute("ALTER TABLE albums ADD COLUMN api_track_count INTEGER DEFAULT NULL")
|
||||||
|
logger.info("Repaired missing api_track_count column on albums table")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error repairing core media schema columns: %s", e)
|
||||||
|
|
||||||
def _add_mirrored_playlist_explored_column(self, cursor):
|
def _add_mirrored_playlist_explored_column(self, cursor):
|
||||||
"""Add explored_at column to mirrored_playlists to persist explore badge."""
|
"""Add explored_at column to mirrored_playlists to persist explore badge."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -5499,6 +5524,25 @@ class MusicDatabase:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
error_text = str(e).lower()
|
||||||
|
if (
|
||||||
|
'file_size' in error_text
|
||||||
|
and ('no such column' in error_text or 'no column named' in error_text)
|
||||||
|
and retry_count < max_retries - 1
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
repair_conn = conn if 'conn' in locals() else self._get_connection()
|
||||||
|
repair_cursor = repair_conn.cursor()
|
||||||
|
self._ensure_core_media_schema_columns(repair_cursor)
|
||||||
|
repair_conn.commit()
|
||||||
|
if repair_conn is not conn:
|
||||||
|
repair_conn.close()
|
||||||
|
retry_count += 1
|
||||||
|
logger.info("Repaired missing file_size column while importing media track; retrying")
|
||||||
|
continue
|
||||||
|
except Exception as schema_error:
|
||||||
|
logger.error("Failed to repair tracks.file_size during track import: %s", schema_error)
|
||||||
|
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
if "database is locked" in str(e).lower() and retry_count < max_retries:
|
if "database is locked" in str(e).lower() and retry_count < max_retries:
|
||||||
logger.warning(f"Database locked on track '{getattr(track_obj, 'title', 'Unknown')}', retrying {retry_count}/{max_retries}...")
|
logger.warning(f"Database locked on track '{getattr(track_obj, 'title', 'Unknown')}', retrying {retry_count}/{max_retries}...")
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,53 @@ def test_file_size_column_exists_after_init(db: MusicDatabase) -> None:
|
||||||
assert 'file_size' in cols
|
assert 'file_size' in cols
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_media_schema_repairs_required_refresh_columns(tmp_path: Path) -> None:
|
||||||
|
"""Upgraded installs can have old library tables plus migration markers.
|
||||||
|
Startup must repair the columns full refresh writes later."""
|
||||||
|
db_path = tmp_path / 'legacy_missing_media_columns.db'
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT)")
|
||||||
|
cur.execute("INSERT INTO metadata (key, value) VALUES ('id_columns_migrated', 'true')")
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE artists (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE albums (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
artist_id TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE tracks (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
album_id TEXT NOT NULL,
|
||||||
|
artist_id TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
file_path TEXT,
|
||||||
|
bitrate INTEGER
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
repaired = MusicDatabase(database_path=str(db_path))
|
||||||
|
conn = repaired._get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("PRAGMA table_info(tracks)")
|
||||||
|
track_cols = {row[1] for row in cur.fetchall()}
|
||||||
|
cur.execute("PRAGMA table_info(albums)")
|
||||||
|
album_cols = {row[1] for row in cur.fetchall()}
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
assert 'file_size' in track_cols
|
||||||
|
assert 'api_track_count' in album_cols
|
||||||
|
|
||||||
|
|
||||||
def test_existing_tracks_have_null_file_size_after_migration(db: MusicDatabase) -> None:
|
def test_existing_tracks_have_null_file_size_after_migration(db: MusicDatabase) -> None:
|
||||||
"""Backward-compat: rows inserted via the OLD schema (no file_size)
|
"""Backward-compat: rows inserted via the OLD schema (no file_size)
|
||||||
must still be readable, and querying file_size returns NULL — not
|
must still be readable, and querying file_size returns NULL — not
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""Tests for `worker_utils.set_album_api_track_count` — the shared helper
|
"""Tests for `worker_utils.set_album_api_track_count` — the shared helper
|
||||||
enrichment workers call to cache authoritative track counts."""
|
enrichment workers call to cache authoritative track counts."""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
from core.worker_utils import set_album_api_track_count
|
from core.worker_utils import set_album_api_track_count
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -114,3 +116,21 @@ def test_swallows_cursor_execute_errors():
|
||||||
cursor = _BrokenCursor()
|
cursor = _BrokenCursor()
|
||||||
# Should not raise.
|
# Should not raise.
|
||||||
set_album_api_track_count(cursor, "album-z", 10)
|
set_album_api_track_count(cursor, "album-z", 10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_repairs_missing_api_track_count_column():
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("CREATE TABLE albums (id TEXT PRIMARY KEY, title TEXT)")
|
||||||
|
cursor.execute("INSERT INTO albums (id, title) VALUES ('album-z', 'Album')")
|
||||||
|
|
||||||
|
set_album_api_track_count(cursor, "album-z", 10)
|
||||||
|
|
||||||
|
cursor.execute("PRAGMA table_info(albums)")
|
||||||
|
cols = {row[1] for row in cursor.fetchall()}
|
||||||
|
cursor.execute("SELECT api_track_count FROM albums WHERE id = 'album-z'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
assert 'api_track_count' in cols
|
||||||
|
assert row[0] == 10
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue