Move bubble snapshots from disk to database
This commit is contained in:
parent
0951b0391d
commit
0f18b12967
2 changed files with 107 additions and 119 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
165
web_server.py
165
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.
|
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,37 +18613,26 @@ 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
|
||||||
if not data or 'bubbles' not in data:
|
if not data or 'bubbles' not in data:
|
||||||
return jsonify({'success': False, 'error': 'No bubble data provided'}), 400
|
return jsonify({'success': False, 'error': 'No bubble data provided'}), 400
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
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:
|
||||||
print(f"❌ Error saving artist bubble snapshot: {e}")
|
print(f"❌ Error saving artist bubble snapshot: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
@ -18683,26 +18648,22 @@ 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:
|
||||||
if snapshot_time:
|
if snapshot_time:
|
||||||
|
|
@ -18710,15 +18671,15 @@ 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
|
||||||
current_processes = {}
|
current_processes = {}
|
||||||
try:
|
try:
|
||||||
|
|
@ -18734,27 +18695,17 @@ def hydrate_artist_bubbles():
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error fetching active processes for hydration: {e}")
|
print(f"⚠️ Error fetching active processes for hydration: {e}")
|
||||||
|
|
||||||
# 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': {},
|
||||||
'message': 'No active processes - returning empty bubbles'
|
'message': 'No active processes - returning empty bubbles'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Update bubble statuses with live data
|
# Update bubble statuses with live data
|
||||||
hydrated_bubbles = {}
|
hydrated_bubbles = {}
|
||||||
for artist_id, bubble_data in saved_bubbles.items():
|
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.
|
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': {},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue