Move bubble snapshots from disk to database

This commit is contained in:
Broque Thomas 2026-02-17 16:12:22 -08:00
parent 0951b0391d
commit 0f18b12967
2 changed files with 107 additions and 119 deletions

View file

@ -306,6 +306,16 @@ class MusicDatabase:
# Add external ID columns (Spotify/iTunes) to library tables (migration) # Add external ID columns (Spotify/iTunes) to library tables (migration)
self._add_external_id_columns(cursor) 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() conn.commit()
logger.info("Database initialized successfully") logger.info("Database initialized successfully")
@ -2637,6 +2647,57 @@ class MusicDatabase:
"""Get a user preference (alias for get_metadata for clarity)""" """Get a user preference (alias for get_metadata for clarity)"""
return self.get_metadata(key) 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 # Quality profile management methods
def get_quality_profile(self) -> dict: def get_quality_profile(self) -> dict:

View file

@ -18464,8 +18464,6 @@ def save_discover_download_snapshot():
Saves a snapshot of current discover download state for persistence across page refreshes. Saves a snapshot of current discover download state for persistence across page refreshes.
""" """
try: try:
import os
import json
from datetime import datetime from datetime import datetime
data = request.json data = request.json
@ -18474,17 +18472,8 @@ def save_discover_download_snapshot():
downloads = data['downloads'] downloads = data['downloads']
# Create snapshot with timestamp db = get_database()
snapshot = { db.save_bubble_snapshot('discover_downloads', downloads)
'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)
download_count = len(downloads) download_count = len(downloads)
print(f"📸 Saved discover download snapshot: {download_count} downloads") print(f"📸 Saved discover download snapshot: {download_count} downloads")
@ -18492,7 +18481,7 @@ def save_discover_download_snapshot():
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'Snapshot saved with {download_count} downloads', 'message': f'Snapshot saved with {download_count} downloads',
'timestamp': snapshot['timestamp'] 'timestamp': datetime.now().isoformat()
}) })
except Exception as e: 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. Loads discover downloads with live status by cross-referencing snapshots with active processes.
""" """
try: try:
import os
import json
from datetime import datetime, timedelta 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 # Load snapshot if it exists
if not os.path.exists(snapshot_file): if not snapshot:
return jsonify({ return jsonify({
'success': True, 'success': True,
'downloads': {}, 'downloads': {},
'message': 'No snapshots found' 'message': 'No snapshots found'
}) })
with open(snapshot_file, 'r') as f: saved_downloads = snapshot['data']
snapshot_data = json.load(f) snapshot_time = snapshot['timestamp']
saved_downloads = snapshot_data.get('downloads', {})
snapshot_time = snapshot_data.get('timestamp', '')
# Clean up old snapshots (older than 48 hours) # Clean up old snapshots (older than 48 hours)
try: try:
@ -18537,13 +18522,13 @@ def hydrate_discover_downloads():
cutoff = datetime.now() - timedelta(hours=48) cutoff = datetime.now() - timedelta(hours=48)
if snapshot_dt < cutoff: if snapshot_dt < cutoff:
print(f"🧹 Cleaning up old discover download snapshot from {snapshot_time}") print(f"🧹 Cleaning up old discover download snapshot from {snapshot_time}")
os.remove(snapshot_file) db.delete_bubble_snapshot('discover_downloads')
return jsonify({ return jsonify({
'success': True, 'success': True,
'downloads': {}, 'downloads': {},
'message': 'Old snapshot cleaned up' 'message': 'Old snapshot cleaned up'
}) })
except (ValueError, OSError) as e: except ValueError as e:
print(f"⚠️ Error checking discover snapshot age: {e}") print(f"⚠️ Error checking discover snapshot age: {e}")
# Get current active download processes for live status # 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 no active processes exist, the app likely restarted - clean up snapshots
if not current_processes: if not current_processes:
print(f"🧹 No active processes found - app likely restarted, cleaning up discover download snapshot") print(f"🧹 No active processes found - app likely restarted, cleaning up discover download snapshot")
try: db.delete_bubble_snapshot('discover_downloads')
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}")
return jsonify({ return jsonify({
'success': True, 'success': True,
'downloads': {}, 'downloads': {},
@ -18637,8 +18613,6 @@ def save_artist_bubble_snapshot():
Saves a snapshot of current artist bubble state for persistence across page refreshes. Saves a snapshot of current artist bubble state for persistence across page refreshes.
""" """
try: try:
import os
import json
from datetime import datetime from datetime import datetime
data = request.json data = request.json
@ -18647,17 +18621,8 @@ def save_artist_bubble_snapshot():
bubbles = data['bubbles'] bubbles = data['bubbles']
# Create snapshot with timestamp db = get_database()
snapshot = { db.save_bubble_snapshot('artist_bubbles', bubbles)
'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)
bubble_count = len(bubbles) bubble_count = len(bubbles)
print(f"📸 Saved artist bubble snapshot: {bubble_count} artists") print(f"📸 Saved artist bubble snapshot: {bubble_count} artists")
@ -18665,7 +18630,7 @@ def save_artist_bubble_snapshot():
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'Snapshot saved with {bubble_count} artist bubbles', 'message': f'Snapshot saved with {bubble_count} artist bubbles',
'timestamp': snapshot['timestamp'] 'timestamp': datetime.now().isoformat()
}) })
except Exception as e: except Exception as e:
@ -18683,25 +18648,21 @@ def hydrate_artist_bubbles():
Loads artist bubbles with live status by cross-referencing snapshots with active processes. Loads artist bubbles with live status by cross-referencing snapshots with active processes.
""" """
try: try:
import os
import json
from datetime import datetime, timedelta 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 # Load snapshot if it exists
if not os.path.exists(snapshot_file): if not snapshot:
return jsonify({ return jsonify({
'success': True, 'success': True,
'bubbles': {}, 'bubbles': {},
'message': 'No snapshots found' 'message': 'No snapshots found'
}) })
with open(snapshot_file, 'r') as f: saved_bubbles = snapshot['data']
snapshot_data = json.load(f) snapshot_time = snapshot['timestamp']
saved_bubbles = snapshot_data.get('bubbles', {})
snapshot_time = snapshot_data.get('timestamp', '')
# Clean up old snapshots (older than 48 hours) # Clean up old snapshots (older than 48 hours)
try: try:
@ -18710,13 +18671,13 @@ def hydrate_artist_bubbles():
cutoff = datetime.now() - timedelta(hours=48) cutoff = datetime.now() - timedelta(hours=48)
if snapshot_dt < cutoff: if snapshot_dt < cutoff:
print(f"🧹 Cleaning up old snapshot from {snapshot_time}") print(f"🧹 Cleaning up old snapshot from {snapshot_time}")
os.remove(snapshot_file) db.delete_bubble_snapshot('artist_bubbles')
return jsonify({ return jsonify({
'success': True, 'success': True,
'bubbles': {}, 'bubbles': {},
'message': 'Old snapshot cleaned up' 'message': 'Old snapshot cleaned up'
}) })
except (ValueError, OSError) as e: except ValueError as e:
print(f"⚠️ Error checking snapshot age: {e}") print(f"⚠️ Error checking snapshot age: {e}")
# Get current active download processes for live status # Get current active download processes for live status
@ -18738,17 +18699,7 @@ def hydrate_artist_bubbles():
# If no active processes exist, the app likely restarted - clean up snapshots # If no active processes exist, the app likely restarted - clean up snapshots
if not current_processes: if not current_processes:
print(f"🧹 No active processes found - app likely restarted, cleaning up snapshot") print(f"🧹 No active processes found - app likely restarted, cleaning up snapshot")
try: db.delete_bubble_snapshot('artist_bubbles')
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
return jsonify({ return jsonify({
'success': True, 'success': True,
'bubbles': {}, 'bubbles': {},
@ -18834,8 +18785,6 @@ def save_search_bubble_snapshot():
Saves a snapshot of current search bubble state for persistence across page refreshes. Saves a snapshot of current search bubble state for persistence across page refreshes.
""" """
try: try:
import os
import json
from datetime import datetime from datetime import datetime
data = request.json data = request.json
@ -18844,17 +18793,8 @@ def save_search_bubble_snapshot():
bubbles = data['bubbles'] bubbles = data['bubbles']
# Create snapshot with timestamp db = get_database()
snapshot = { db.save_bubble_snapshot('search_bubbles', bubbles)
'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)
bubble_count = len(bubbles) bubble_count = len(bubbles)
print(f"📸 Saved search bubble snapshot: {bubble_count} albums/tracks") print(f"📸 Saved search bubble snapshot: {bubble_count} albums/tracks")
@ -18862,7 +18802,7 @@ def save_search_bubble_snapshot():
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'Snapshot saved with {bubble_count} search bubbles', 'message': f'Snapshot saved with {bubble_count} search bubbles',
'timestamp': snapshot['timestamp'] 'timestamp': datetime.now().isoformat()
}) })
except Exception as e: 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. Loads search bubbles with live status by cross-referencing snapshots with active processes.
""" """
try: try:
import os
import json
from datetime import datetime, timedelta 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 # Load snapshot if it exists
if not os.path.exists(snapshot_file): if not snapshot:
return jsonify({ return jsonify({
'success': True, 'success': True,
'bubbles': {}, 'bubbles': {},
'message': 'No snapshots found' 'message': 'No snapshots found'
}) })
with open(snapshot_file, 'r') as f: saved_bubbles = snapshot['data']
snapshot_data = json.load(f) snapshot_time = snapshot['timestamp']
saved_bubbles = snapshot_data.get('bubbles', {})
snapshot_time = snapshot_data.get('timestamp', '')
# Clean up old snapshots (older than 48 hours) # Clean up old snapshots (older than 48 hours)
try: try:
@ -18907,13 +18843,13 @@ def hydrate_search_bubbles():
cutoff = datetime.now() - timedelta(hours=48) cutoff = datetime.now() - timedelta(hours=48)
if snapshot_dt < cutoff: if snapshot_dt < cutoff:
print(f"🧹 Cleaning up old search snapshot from {snapshot_time}") print(f"🧹 Cleaning up old search snapshot from {snapshot_time}")
os.remove(snapshot_file) db.delete_bubble_snapshot('search_bubbles')
return jsonify({ return jsonify({
'success': True, 'success': True,
'bubbles': {}, 'bubbles': {},
'message': 'Old snapshot cleaned up' 'message': 'Old snapshot cleaned up'
}) })
except (ValueError, OSError) as e: except ValueError as e:
print(f"⚠️ Error checking snapshot age: {e}") print(f"⚠️ Error checking snapshot age: {e}")
# Get current active download processes for live status # 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 no active processes exist, the app likely restarted - clean up snapshots
if not current_processes: if not current_processes:
print(f"🧹 No active processes found - app likely restarted, cleaning up search snapshot") print(f"🧹 No active processes found - app likely restarted, cleaning up search snapshot")
try: db.delete_bubble_snapshot('search_bubbles')
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}")
return jsonify({ return jsonify({
'success': True, 'success': True,
'bubbles': {}, 'bubbles': {},