From 8d46d3746bb3d8d4d1b67541c376d7eb4998f011 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:44:00 -0700 Subject: [PATCH] Fix Docker upgrade crashes from stale volume mounts and partial DB migrations --- database/music_database.py | 106 +++++++++++++++++-------------------- entrypoint.sh | 12 ++--- web_server.py | 43 ++++++++++++--- 3 files changed, 88 insertions(+), 73 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index 94444049..cdcf5bbf 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -1116,39 +1116,55 @@ class MusicDatabase: """Add MusicBrainz tracking columns to library tables for metadata enrichment""" columns_added = False try: - # Check if musicbrainz_id column exists in artists table + # --- Artists --- cursor.execute("PRAGMA table_info(artists)") artists_columns = [column[1] for column in cursor.fetchall()] - + if 'musicbrainz_id' not in artists_columns: - logger.info("Adding MusicBrainz columns to artists table...") cursor.execute("ALTER TABLE artists ADD COLUMN musicbrainz_id TEXT") + columns_added = True + if 'musicbrainz_last_attempted' not in artists_columns: cursor.execute("ALTER TABLE artists ADD COLUMN musicbrainz_last_attempted TIMESTAMP") + columns_added = True + if 'musicbrainz_match_status' not in artists_columns: cursor.execute("ALTER TABLE artists ADD COLUMN musicbrainz_match_status TEXT") columns_added = True + if columns_added: logger.info("✅ Added MusicBrainz columns to artists table") - - # Check if musicbrainz_release_id column exists in albums table + + # --- Albums --- cursor.execute("PRAGMA table_info(albums)") albums_columns = [column[1] for column in cursor.fetchall()] - + + added_albums = False if 'musicbrainz_release_id' not in albums_columns: - logger.info("Adding MusicBrainz columns to albums table...") cursor.execute("ALTER TABLE albums ADD COLUMN musicbrainz_release_id TEXT") + added_albums = True + if 'musicbrainz_last_attempted' not in albums_columns: cursor.execute("ALTER TABLE albums ADD COLUMN musicbrainz_last_attempted TIMESTAMP") + added_albums = True + if 'musicbrainz_match_status' not in albums_columns: cursor.execute("ALTER TABLE albums ADD COLUMN musicbrainz_match_status TEXT") + added_albums = True + if added_albums: columns_added = True logger.info("✅ Added MusicBrainz columns to albums table") - - # Check if musicbrainz_recording_id column exists in tracks table + + # --- Tracks --- cursor.execute("PRAGMA table_info(tracks)") tracks_columns = [column[1] for column in cursor.fetchall()] - + + added_tracks = False if 'musicbrainz_recording_id' not in tracks_columns: - logger.info("Adding MusicBrainz columns to tracks table...") cursor.execute("ALTER TABLE tracks ADD COLUMN musicbrainz_recording_id TEXT") + added_tracks = True + if 'musicbrainz_last_attempted' not in tracks_columns: cursor.execute("ALTER TABLE tracks ADD COLUMN musicbrainz_last_attempted TIMESTAMP") + added_tracks = True + if 'musicbrainz_match_status' not in tracks_columns: cursor.execute("ALTER TABLE tracks ADD COLUMN musicbrainz_match_status TEXT") + added_tracks = True + if added_tracks: columns_added = True logger.info("✅ Added MusicBrainz columns to tracks table") @@ -1169,36 +1185,13 @@ class MusicDatabase: ) """) - # Only proceed with index creation if we successfully checked for columns - # Re-verify columns exist before creating indexes to prevent errors - cursor.execute("PRAGMA table_info(artists)") - artists_columns = [column[1] for column in cursor.fetchall()] - cursor.execute("PRAGMA table_info(albums)") - albums_columns = [column[1] for column in cursor.fetchall()] - cursor.execute("PRAGMA table_info(tracks)") - tracks_columns = [column[1] for column in cursor.fetchall()] - - # Verify all required columns exist before creating indexes - if 'musicbrainz_match_status' in artists_columns: - cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_mbid ON artists (musicbrainz_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_mb_status ON artists (musicbrainz_match_status)") - else: - logger.error("❌ CRITICAL: musicbrainz_match_status column missing from artists table after migration!") - raise Exception("MusicBrainz artists column migration failed - column not found after ALTER TABLE") - - if 'musicbrainz_match_status' in albums_columns: - cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_mbid ON albums (musicbrainz_release_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_mb_status ON albums (musicbrainz_match_status)") - else: - logger.error("❌ CRITICAL: musicbrainz_match_status column missing from albums table after migration!") - raise Exception("MusicBrainz albums column migration failed - column not found after ALTER TABLE") - - if 'musicbrainz_match_status' in tracks_columns: - cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_mbid ON tracks (musicbrainz_recording_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_mb_status ON tracks (musicbrainz_match_status)") - else: - logger.error("❌ CRITICAL: musicbrainz_match_status column missing from tracks table after migration!") - raise Exception("MusicBrainz tracks column migration failed - column not found after ALTER TABLE") + # Create indexes (safe even if columns were already present) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_mbid ON artists (musicbrainz_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_mb_status ON artists (musicbrainz_match_status)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_mbid ON albums (musicbrainz_release_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_mb_status ON albums (musicbrainz_match_status)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_mbid ON tracks (musicbrainz_recording_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_mb_status ON tracks (musicbrainz_match_status)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_mb_cache_entity ON musicbrainz_cache (entity_type, entity_name)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_mb_cache_mbid ON musicbrainz_cache (musicbrainz_id)") @@ -1207,47 +1200,44 @@ class MusicDatabase: logger.info("🎉 MusicBrainz migration completed successfully") except Exception as e: - logger.error(f"❌ CRITICAL ERROR in MusicBrainz migration: {e}") - logger.error("⚠️ Database may be in an inconsistent state - MusicBrainz features will not work") - logger.error("⚠️ You may need to manually add the missing columns or reset your database") - # Re-raise the exception to prevent the application from starting with a broken database - raise + logger.error(f"Error in MusicBrainz migration: {e}") + # Don't raise - this is a migration, database can still function def _add_external_id_columns(self, cursor): """Add Spotify/iTunes external ID columns to library tables for enrichment""" try: - # Artists table + # --- Artists --- cursor.execute("PRAGMA table_info(artists)") artists_columns = [column[1] for column in cursor.fetchall()] if 'spotify_artist_id' not in artists_columns: cursor.execute("ALTER TABLE artists ADD COLUMN spotify_artist_id TEXT") + if 'itunes_artist_id' not in artists_columns: cursor.execute("ALTER TABLE artists ADD COLUMN itunes_artist_id TEXT") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_spotify_id ON artists (spotify_artist_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_itunes_id ON artists (itunes_artist_id)") - logger.info("Added external ID columns to artists table") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_spotify_id ON artists (spotify_artist_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_itunes_id ON artists (itunes_artist_id)") - # Albums table + # --- Albums --- cursor.execute("PRAGMA table_info(albums)") albums_columns = [column[1] for column in cursor.fetchall()] if 'spotify_album_id' not in albums_columns: cursor.execute("ALTER TABLE albums ADD COLUMN spotify_album_id TEXT") + if 'itunes_album_id' not in albums_columns: cursor.execute("ALTER TABLE albums ADD COLUMN itunes_album_id TEXT") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_spotify_id ON albums (spotify_album_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_itunes_id ON albums (itunes_album_id)") - logger.info("Added external ID columns to albums table") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_spotify_id ON albums (spotify_album_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_itunes_id ON albums (itunes_album_id)") - # Tracks table + # --- Tracks --- cursor.execute("PRAGMA table_info(tracks)") tracks_columns = [column[1] for column in cursor.fetchall()] if 'spotify_track_id' not in tracks_columns: cursor.execute("ALTER TABLE tracks ADD COLUMN spotify_track_id TEXT") + if 'itunes_track_id' not in tracks_columns: cursor.execute("ALTER TABLE tracks ADD COLUMN itunes_track_id TEXT") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_spotify_id ON tracks (spotify_track_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_itunes_id ON tracks (itunes_track_id)") - logger.info("Added external ID columns to tracks table") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_spotify_id ON tracks (spotify_track_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_itunes_id ON tracks (itunes_track_id)") except Exception as e: logger.error(f"Error adding external ID columns: {e}") diff --git a/entrypoint.sh b/entrypoint.sh index 5461a09b..1c9f1382 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -57,13 +57,11 @@ else echo " ✅ config.json already exists" fi -if [ ! -f "/app/config/settings.py" ]; then - echo " 📄 Creating default settings.py..." - cp /defaults/settings.py /app/config/settings.py - chown soulsync:soulsync /app/config/settings.py 2>/dev/null || true -else - echo " ✅ settings.py already exists" -fi +# Always update settings.py — it's application code, not user configuration. +# Stale versions from older releases cause startup crashes (missing methods). +echo " 📄 Updating settings.py to current version..." +cp /defaults/settings.py /app/config/settings.py +chown soulsync:soulsync /app/config/settings.py 2>/dev/null || true # Ensure all directories exist and have proper permissions mkdir -p /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging diff --git a/web_server.py b/web_server.py index 872ddd7f..e3c50628 100644 --- a/web_server.py +++ b/web_server.py @@ -63,8 +63,24 @@ from core.matching_engine import MusicMatchingEngine from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker from core.web_scan_manager import WebScanManager from core.lyrics_client import lyrics_client -from database.music_database import get_database +from database.music_database import get_database, MusicDatabase from services.sync_service import PlaylistSyncService + +# --- Docker Volume Mount Guard --- +# Pre-v1.3 docker-compose files mounted soulsync_database:/app/database, which overlays +# the Python package with stale volume contents. Detect this after import. +if not hasattr(MusicDatabase, 'get_system_automation_by_action'): + print("=" * 70) + print("🔴 ERROR: Stale database module detected!") + print(" MusicDatabase is missing required methods. This usually means") + print(" your docker-compose.yml has an outdated volume mount:") + print("") + print(" FIX: Change your docker-compose.yml volume:") + print(" OLD: soulsync_database:/app/database") + print(" NEW: soulsync_database:/app/data") + print("") + print(" Then run: docker compose down && docker compose up -d") + print("=" * 70) from datetime import datetime, timezone import yt_dlp from core.matching_engine import MusicMatchingEngine @@ -3941,7 +3957,10 @@ def hydrabase_status(): connected = _hydrabase_ws is not None and _hydrabase_ws.connected except Exception: connected = False - hydra_config = config_manager.get_hydrabase_config() + try: + hydra_config = config_manager.get_hydrabase_config() + except AttributeError: + hydra_config = {} peer_count = None try: if hydrabase_client and hydrabase_client.last_peer_count is not None: @@ -34366,13 +34385,21 @@ if __name__ == '__main__': # Register action handlers and start automation engine _register_automation_handlers() if automation_engine: - print("🔧 Starting automation engine...") - automation_engine.start() - print("✅ Automation engine started") try: - automation_engine.emit('app_started', {}) - except Exception: - pass + print("🔧 Starting automation engine...") + automation_engine.start() + print("✅ Automation engine started") + try: + automation_engine.emit('app_started', {}) + except Exception: + pass + except AttributeError as e: + print(f"⚠️ Automation engine failed to start: {e}") + print(" If using Docker, check that your volume mount is /app/data (not /app/database)") + logger.error(f"Automation engine start error (possible stale Docker volume): {e}") + except Exception as e: + print(f"⚠️ Automation engine failed to start: {e}") + logger.error(f"Automation engine start error: {e}") # Add startup activity add_activity_item("🚀", "System Started", "SoulSync Web UI Server initialized", "Now")