From 0f18b129679cd47bc18e5e7ec96908334b87a539 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Tue, 17 Feb 2026 16:12:22 -0800 Subject: [PATCH] Move bubble snapshots from disk to database --- database/music_database.py | 61 ++++++++++++++ web_server.py | 165 +++++++++++-------------------------- 2 files changed, 107 insertions(+), 119 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index ad651669..31e15f88 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -306,6 +306,16 @@ class MusicDatabase: # Add external ID columns (Spotify/iTunes) to library tables (migration) self._add_external_id_columns(cursor) + # Bubble snapshots table for persisting UI state across page refreshes + cursor.execute(""" + CREATE TABLE IF NOT EXISTS bubble_snapshots ( + type TEXT PRIMARY KEY, + data TEXT NOT NULL, + timestamp TEXT NOT NULL, + snapshot_id TEXT NOT NULL + ) + """) + conn.commit() logger.info("Database initialized successfully") @@ -2637,6 +2647,57 @@ class MusicDatabase: """Get a user preference (alias for get_metadata for clarity)""" return self.get_metadata(key) + # --- Bubble Snapshot Methods --- + + def save_bubble_snapshot(self, snapshot_type: str, data_dict: dict): + """Save a bubble snapshot (upserts by type). + + Args: + snapshot_type: One of 'artist_bubbles', 'search_bubbles', 'discover_downloads' + data_dict: The bubbles/downloads dict to persist + """ + from datetime import datetime + now = datetime.now() + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT OR REPLACE INTO bubble_snapshots (type, data, timestamp, snapshot_id) VALUES (?, ?, ?, ?)", + (snapshot_type, json.dumps(data_dict), now.isoformat(), now.strftime('%Y%m%d_%H%M%S')) + ) + conn.commit() + except Exception as e: + logger.error(f"Error saving bubble snapshot '{snapshot_type}': {e}") + raise + + def get_bubble_snapshot(self, snapshot_type: str) -> Optional[Dict[str, Any]]: + """Load a bubble snapshot. + + Returns: + {'data': dict, 'timestamp': str} or None if not found + """ + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT data, timestamp FROM bubble_snapshots WHERE type = ?", (snapshot_type,)) + row = cursor.fetchone() + if row: + return {'data': json.loads(row['data']), 'timestamp': row['timestamp']} + return None + except Exception as e: + logger.error(f"Error getting bubble snapshot '{snapshot_type}': {e}") + return None + + def delete_bubble_snapshot(self, snapshot_type: str): + """Delete a bubble snapshot.""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM bubble_snapshots WHERE type = ?", (snapshot_type,)) + conn.commit() + except Exception as e: + logger.error(f"Error deleting bubble snapshot '{snapshot_type}': {e}") + # Quality profile management methods def get_quality_profile(self) -> dict: diff --git a/web_server.py b/web_server.py index aada4ce0..13b88ae9 100644 --- a/web_server.py +++ b/web_server.py @@ -18464,8 +18464,6 @@ def save_discover_download_snapshot(): Saves a snapshot of current discover download state for persistence across page refreshes. """ try: - import os - import json from datetime import datetime data = request.json @@ -18474,17 +18472,8 @@ def save_discover_download_snapshot(): downloads = data['downloads'] - # Create snapshot with timestamp - snapshot = { - 'downloads': downloads, - 'timestamp': datetime.now().isoformat(), - 'snapshot_id': datetime.now().strftime('%Y%m%d_%H%M%S') - } - - # Save to file - snapshot_file = os.path.join(os.path.dirname(__file__), 'discover_download_snapshots.json') - with open(snapshot_file, 'w') as f: - json.dump(snapshot, f, indent=2) + db = get_database() + db.save_bubble_snapshot('discover_downloads', downloads) download_count = len(downloads) print(f"📸 Saved discover download snapshot: {download_count} downloads") @@ -18492,7 +18481,7 @@ def save_discover_download_snapshot(): return jsonify({ 'success': True, 'message': f'Snapshot saved with {download_count} downloads', - 'timestamp': snapshot['timestamp'] + 'timestamp': datetime.now().isoformat() }) except Exception as e: @@ -18510,25 +18499,21 @@ def hydrate_discover_downloads(): Loads discover downloads with live status by cross-referencing snapshots with active processes. """ try: - import os - import json from datetime import datetime, timedelta - snapshot_file = os.path.join(os.path.dirname(__file__), 'discover_download_snapshots.json') + db = get_database() + snapshot = db.get_bubble_snapshot('discover_downloads') # Load snapshot if it exists - if not os.path.exists(snapshot_file): + if not snapshot: return jsonify({ 'success': True, 'downloads': {}, 'message': 'No snapshots found' }) - with open(snapshot_file, 'r') as f: - snapshot_data = json.load(f) - - saved_downloads = snapshot_data.get('downloads', {}) - snapshot_time = snapshot_data.get('timestamp', '') + saved_downloads = snapshot['data'] + snapshot_time = snapshot['timestamp'] # Clean up old snapshots (older than 48 hours) try: @@ -18537,13 +18522,13 @@ def hydrate_discover_downloads(): cutoff = datetime.now() - timedelta(hours=48) if snapshot_dt < cutoff: print(f"🧹 Cleaning up old discover download snapshot from {snapshot_time}") - os.remove(snapshot_file) + db.delete_bubble_snapshot('discover_downloads') return jsonify({ 'success': True, 'downloads': {}, 'message': 'Old snapshot cleaned up' }) - except (ValueError, OSError) as e: + except ValueError as e: print(f"⚠️ Error checking discover snapshot age: {e}") # Get current active download processes for live status @@ -18565,16 +18550,7 @@ def hydrate_discover_downloads(): # If no active processes exist, the app likely restarted - clean up snapshots if not current_processes: print(f"🧹 No active processes found - app likely restarted, cleaning up discover download snapshot") - try: - os.remove(snapshot_file) - return jsonify({ - 'success': True, - 'downloads': {}, - 'message': 'Snapshot cleaned up after app restart' - }) - except OSError as e: - print(f"⚠️ Error removing discover snapshot file: {e}") - + db.delete_bubble_snapshot('discover_downloads') return jsonify({ 'success': True, 'downloads': {}, @@ -18637,37 +18613,26 @@ def save_artist_bubble_snapshot(): Saves a snapshot of current artist bubble state for persistence across page refreshes. """ try: - import os - import json from datetime import datetime - + data = request.json if not data or 'bubbles' not in data: return jsonify({'success': False, 'error': 'No bubble data provided'}), 400 - + bubbles = data['bubbles'] - - # Create snapshot with timestamp - snapshot = { - 'bubbles': bubbles, - 'timestamp': datetime.now().isoformat(), - 'snapshot_id': datetime.now().strftime('%Y%m%d_%H%M%S') - } - - # Save to file - snapshot_file = os.path.join(os.path.dirname(__file__), 'artist_bubble_snapshots.json') - with open(snapshot_file, 'w') as f: - json.dump(snapshot, f, indent=2) - + + db = get_database() + db.save_bubble_snapshot('artist_bubbles', bubbles) + bubble_count = len(bubbles) print(f"📸 Saved artist bubble snapshot: {bubble_count} artists") - + return jsonify({ 'success': True, 'message': f'Snapshot saved with {bubble_count} artist bubbles', - 'timestamp': snapshot['timestamp'] + 'timestamp': datetime.now().isoformat() }) - + except Exception as e: print(f"❌ Error saving artist bubble snapshot: {e}") import traceback @@ -18683,26 +18648,22 @@ def hydrate_artist_bubbles(): Loads artist bubbles with live status by cross-referencing snapshots with active processes. """ try: - import os - import json from datetime import datetime, timedelta - - snapshot_file = os.path.join(os.path.dirname(__file__), 'artist_bubble_snapshots.json') - + + db = get_database() + snapshot = db.get_bubble_snapshot('artist_bubbles') + # Load snapshot if it exists - if not os.path.exists(snapshot_file): + if not snapshot: return jsonify({ 'success': True, 'bubbles': {}, 'message': 'No snapshots found' }) - - with open(snapshot_file, 'r') as f: - snapshot_data = json.load(f) - - saved_bubbles = snapshot_data.get('bubbles', {}) - snapshot_time = snapshot_data.get('timestamp', '') - + + saved_bubbles = snapshot['data'] + snapshot_time = snapshot['timestamp'] + # Clean up old snapshots (older than 48 hours) try: if snapshot_time: @@ -18710,15 +18671,15 @@ def hydrate_artist_bubbles(): cutoff = datetime.now() - timedelta(hours=48) if snapshot_dt < cutoff: print(f"🧹 Cleaning up old snapshot from {snapshot_time}") - os.remove(snapshot_file) + db.delete_bubble_snapshot('artist_bubbles') return jsonify({ 'success': True, 'bubbles': {}, 'message': 'Old snapshot cleaned up' }) - except (ValueError, OSError) as e: + except ValueError as e: print(f"⚠️ Error checking snapshot age: {e}") - + # Get current active download processes for live status current_processes = {} try: @@ -18734,27 +18695,17 @@ def hydrate_artist_bubbles(): } except Exception as e: print(f"⚠️ Error fetching active processes for hydration: {e}") - + # If no active processes exist, the app likely restarted - clean up snapshots if not current_processes: print(f"🧹 No active processes found - app likely restarted, cleaning up snapshot") - try: - os.remove(snapshot_file) - return jsonify({ - 'success': True, - 'bubbles': {}, - 'message': 'Snapshot cleaned up after app restart' - }) - except OSError as e: - print(f"⚠️ Error removing snapshot file: {e}") - # Continue with empty result anyway - + db.delete_bubble_snapshot('artist_bubbles') return jsonify({ 'success': True, 'bubbles': {}, 'message': 'No active processes - returning empty bubbles' }) - + # Update bubble statuses with live data hydrated_bubbles = {} for artist_id, bubble_data in saved_bubbles.items(): @@ -18834,8 +18785,6 @@ def save_search_bubble_snapshot(): Saves a snapshot of current search bubble state for persistence across page refreshes. """ try: - import os - import json from datetime import datetime data = request.json @@ -18844,17 +18793,8 @@ def save_search_bubble_snapshot(): bubbles = data['bubbles'] - # Create snapshot with timestamp - snapshot = { - 'bubbles': bubbles, - 'timestamp': datetime.now().isoformat(), - 'snapshot_id': datetime.now().strftime('%Y%m%d_%H%M%S') - } - - # Save to file - snapshot_file = os.path.join(os.path.dirname(__file__), 'search_bubble_snapshots.json') - with open(snapshot_file, 'w') as f: - json.dump(snapshot, f, indent=2) + db = get_database() + db.save_bubble_snapshot('search_bubbles', bubbles) bubble_count = len(bubbles) print(f"📸 Saved search bubble snapshot: {bubble_count} albums/tracks") @@ -18862,7 +18802,7 @@ def save_search_bubble_snapshot(): return jsonify({ 'success': True, 'message': f'Snapshot saved with {bubble_count} search bubbles', - 'timestamp': snapshot['timestamp'] + 'timestamp': datetime.now().isoformat() }) except Exception as e: @@ -18880,25 +18820,21 @@ def hydrate_search_bubbles(): Loads search bubbles with live status by cross-referencing snapshots with active processes. """ try: - import os - import json from datetime import datetime, timedelta - snapshot_file = os.path.join(os.path.dirname(__file__), 'search_bubble_snapshots.json') + db = get_database() + snapshot = db.get_bubble_snapshot('search_bubbles') # Load snapshot if it exists - if not os.path.exists(snapshot_file): + if not snapshot: return jsonify({ 'success': True, 'bubbles': {}, 'message': 'No snapshots found' }) - with open(snapshot_file, 'r') as f: - snapshot_data = json.load(f) - - saved_bubbles = snapshot_data.get('bubbles', {}) - snapshot_time = snapshot_data.get('timestamp', '') + saved_bubbles = snapshot['data'] + snapshot_time = snapshot['timestamp'] # Clean up old snapshots (older than 48 hours) try: @@ -18907,13 +18843,13 @@ def hydrate_search_bubbles(): cutoff = datetime.now() - timedelta(hours=48) if snapshot_dt < cutoff: print(f"🧹 Cleaning up old search snapshot from {snapshot_time}") - os.remove(snapshot_file) + db.delete_bubble_snapshot('search_bubbles') return jsonify({ 'success': True, 'bubbles': {}, 'message': 'Old snapshot cleaned up' }) - except (ValueError, OSError) as e: + except ValueError as e: print(f"⚠️ Error checking snapshot age: {e}") # Get current active download processes for live status @@ -18935,16 +18871,7 @@ def hydrate_search_bubbles(): # If no active processes exist, the app likely restarted - clean up snapshots if not current_processes: print(f"🧹 No active processes found - app likely restarted, cleaning up search snapshot") - try: - os.remove(snapshot_file) - return jsonify({ - 'success': True, - 'bubbles': {}, - 'message': 'Snapshot cleaned up after app restart' - }) - except OSError as e: - print(f"⚠️ Error removing snapshot file: {e}") - + db.delete_bubble_snapshot('search_bubbles') return jsonify({ 'success': True, 'bubbles': {},