Move bubble snapshots from disk to database

pull/153/head
Broque Thomas 2 months ago
parent 0951b0391d
commit 0f18b12967

@ -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:

@ -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': {},

Loading…
Cancel
Save