Discovery lift (10/N): save_*_bubble_snapshot -> shared helper
Final cluster: the four structurally-identical snapshot endpoints
(discover_downloads, artist_bubbles, search_bubbles, beatport_bubbles) ->
core.discovery.endpoints.save_bubble_snapshot(...), wired via
_save_source_bubble_snapshot. All four validate a payload key, persist via
db.save_bubble_snapshot(kind, items, profile_id=...), and return a count +
timestamp; they differ only by:
- payload_key ('downloads' for discover, 'bubbles' for the rest) + its
no_data_error message.
- snapshot_kind, success_noun, and the info/except log subject + noun
("downloads"/"artists"/"albums/tracks"/"charts").
get_database / get_current_profile_id injected; get_json (request.json) invoked
inside the try, preserving the original 400/500 behavior incl. traceback dump.
Tests: +5 (missing key 400, None body 400, happy path with kind/profile/count/
timestamp, discover_downloads variant, exception -> 500). Full discovery suite:
210 passed.
web_server.py: -98 lines.
This commit is contained in:
parent
4caf36deb1
commit
d5f6a14ba1
3 changed files with 151 additions and 116 deletions
|
|
@ -398,6 +398,61 @@ def get_playlist_states(
|
|||
return {"error": str(e)}, 500
|
||||
|
||||
|
||||
def save_bubble_snapshot(
|
||||
get_json,
|
||||
*,
|
||||
payload_key: str,
|
||||
no_data_error: str,
|
||||
snapshot_kind: str,
|
||||
success_noun: str,
|
||||
log_subject: str,
|
||||
log_noun: str,
|
||||
get_database,
|
||||
get_current_profile_id,
|
||||
) -> Tuple[Dict[str, Any], int]:
|
||||
"""Persist a bubble/download snapshot for cross-refresh hydration.
|
||||
|
||||
1:1 lift of the four structurally-identical snapshot endpoints
|
||||
(discover_downloads, artist_bubbles, search_bubbles, beatport_bubbles),
|
||||
which differ only by:
|
||||
|
||||
- ``payload_key`` ('downloads' for discover, 'bubbles' for the rest) and
|
||||
its ``no_data_error`` message.
|
||||
- ``snapshot_kind`` — the db.save_bubble_snapshot category.
|
||||
- ``success_noun`` — fills "Snapshot saved with N <noun>".
|
||||
- ``log_subject`` / ``log_noun`` — the info ("Saved <subject>: N <noun>")
|
||||
and except ("Error saving <subject>") log lines.
|
||||
|
||||
Returns ``(payload, status_code)``. ``get_json`` is invoked inside the try
|
||||
like the original ``request.json``.
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
data = get_json()
|
||||
if not data or payload_key not in data:
|
||||
return {'success': False, 'error': no_data_error}, 400
|
||||
|
||||
items = data[payload_key]
|
||||
|
||||
db = get_database()
|
||||
db.save_bubble_snapshot(snapshot_kind, items, profile_id=get_current_profile_id())
|
||||
|
||||
count = len(items)
|
||||
logger.info(f"Saved {log_subject}: {count} {log_noun}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'Snapshot saved with {count} {success_noun}',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}, 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving {log_subject}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'success': False, 'error': str(e)}, 500
|
||||
|
||||
|
||||
def update_playlist_phase(
|
||||
states: Dict[str, Any],
|
||||
key: str,
|
||||
|
|
|
|||
|
|
@ -877,3 +877,81 @@ def test_update_phase_exception_500():
|
|||
error_label='Tidal', valid_phases=_PHASES,
|
||||
apply_extra_fields=False)
|
||||
assert code == 500 and 'error' in body
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# save_bubble_snapshot
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _SnapDB:
|
||||
def __init__(self):
|
||||
self.saved = []
|
||||
|
||||
def save_bubble_snapshot(self, kind, items, profile_id=None):
|
||||
self.saved.append((kind, items, profile_id))
|
||||
|
||||
|
||||
def _snap_kwargs(db, *, payload_key='bubbles', no_data_error='No bubble data provided',
|
||||
snapshot_kind='artist_bubbles', success_noun='artist bubbles',
|
||||
log_subject='artist bubble snapshot', log_noun='artists'):
|
||||
return dict(
|
||||
payload_key=payload_key, no_data_error=no_data_error, snapshot_kind=snapshot_kind,
|
||||
success_noun=success_noun, log_subject=log_subject, log_noun=log_noun,
|
||||
get_database=lambda: db, get_current_profile_id=lambda: 7,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_missing_payload_key():
|
||||
from core.discovery.endpoints import save_bubble_snapshot
|
||||
db = _SnapDB()
|
||||
body, code = save_bubble_snapshot(lambda: {'other': 1}, **_snap_kwargs(db))
|
||||
assert code == 400 and body == {'success': False, 'error': 'No bubble data provided'}
|
||||
assert db.saved == []
|
||||
|
||||
|
||||
def test_snapshot_none_body():
|
||||
from core.discovery.endpoints import save_bubble_snapshot
|
||||
db = _SnapDB()
|
||||
body, code = save_bubble_snapshot(lambda: None,
|
||||
**_snap_kwargs(db, payload_key='downloads',
|
||||
no_data_error='No download data provided'))
|
||||
assert code == 400 and body['error'] == 'No download data provided'
|
||||
|
||||
|
||||
def test_snapshot_happy_path():
|
||||
from core.discovery.endpoints import save_bubble_snapshot
|
||||
db = _SnapDB()
|
||||
items = [{'a': 1}, {'b': 2}, {'c': 3}]
|
||||
body, code = save_bubble_snapshot(lambda: {'bubbles': items},
|
||||
**_snap_kwargs(db, snapshot_kind='search_bubbles',
|
||||
success_noun='search bubbles'))
|
||||
assert code == 200
|
||||
assert body['success'] is True
|
||||
assert body['message'] == 'Snapshot saved with 3 search bubbles'
|
||||
assert isinstance(body['timestamp'], str) and body['timestamp']
|
||||
# persisted with kind + profile id
|
||||
assert db.saved == [('search_bubbles', items, 7)]
|
||||
|
||||
|
||||
def test_snapshot_discover_downloads_key():
|
||||
from core.discovery.endpoints import save_bubble_snapshot
|
||||
db = _SnapDB()
|
||||
body, code = save_bubble_snapshot(lambda: {'downloads': [1, 2]},
|
||||
**_snap_kwargs(db, payload_key='downloads',
|
||||
no_data_error='No download data provided',
|
||||
snapshot_kind='discover_downloads',
|
||||
success_noun='downloads',
|
||||
log_subject='discover download snapshot',
|
||||
log_noun='downloads'))
|
||||
assert code == 200
|
||||
assert body['message'] == 'Snapshot saved with 2 downloads'
|
||||
assert db.saved[0][0] == 'discover_downloads'
|
||||
|
||||
|
||||
def test_snapshot_exception_returns_500():
|
||||
from core.discovery.endpoints import save_bubble_snapshot
|
||||
class _BoomDB:
|
||||
def save_bubble_snapshot(self, *a, **k):
|
||||
raise RuntimeError('db down')
|
||||
body, code = save_bubble_snapshot(lambda: {'bubbles': [1]}, **_snap_kwargs(_BoomDB()))
|
||||
assert code == 500 and body == {'success': False, 'error': 'db down'}
|
||||
|
|
|
|||
134
web_server.py
134
web_server.py
|
|
@ -20856,6 +20856,7 @@ from core.discovery.endpoints import (
|
|||
start_sync as _start_sync_core,
|
||||
update_discovery_match as _update_discovery_match_core,
|
||||
update_playlist_phase as _update_playlist_phase_core,
|
||||
save_bubble_snapshot as _save_bubble_snapshot_core,
|
||||
playlist_name_attr_or_unknown as _pl_name_attr_or_unknown,
|
||||
playlist_name_strict as _pl_name_strict,
|
||||
playlist_name_safe as _pl_name_safe,
|
||||
|
|
@ -20985,6 +20986,19 @@ def _update_source_playlist_phase(states, key, not_found_message, error_label,
|
|||
return jsonify(body), code
|
||||
|
||||
|
||||
def _save_source_bubble_snapshot(payload_key, no_data_error, snapshot_kind,
|
||||
success_noun, log_subject, log_noun):
|
||||
"""Thin glue for the snapshot routes (discover_downloads / artist_bubbles /
|
||||
search_bubbles / beatport_bubbles)."""
|
||||
body, code = _save_bubble_snapshot_core(
|
||||
lambda: request.json, payload_key=payload_key, no_data_error=no_data_error,
|
||||
snapshot_kind=snapshot_kind, success_noun=success_noun, log_subject=log_subject,
|
||||
log_noun=log_noun, get_database=get_database,
|
||||
get_current_profile_id=get_current_profile_id,
|
||||
)
|
||||
return jsonify(body), code
|
||||
|
||||
|
||||
def _build_tidal_discovery_deps():
|
||||
"""Build the TidalDiscoveryDeps bundle from web_server.py globals on each call."""
|
||||
return _discovery_tidal.TidalDiscoveryDeps(
|
||||
|
|
@ -23519,35 +23533,7 @@ def save_discover_download_snapshot():
|
|||
"""
|
||||
Saves a snapshot of current discover download state for persistence across page refreshes.
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
data = request.json
|
||||
if not data or 'downloads' not in data:
|
||||
return jsonify({'success': False, 'error': 'No download data provided'}), 400
|
||||
|
||||
downloads = data['downloads']
|
||||
|
||||
db = get_database()
|
||||
db.save_bubble_snapshot('discover_downloads', downloads, profile_id=get_current_profile_id())
|
||||
|
||||
download_count = len(downloads)
|
||||
logger.info(f"Saved discover download snapshot: {download_count} downloads")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Snapshot saved with {download_count} downloads',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving discover download snapshot: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
return _save_source_bubble_snapshot("downloads", "No download data provided", "discover_downloads", "downloads", "discover download snapshot", "downloads")
|
||||
|
||||
@app.route('/api/discover_downloads/hydrate', methods=['GET'])
|
||||
def hydrate_discover_downloads():
|
||||
|
|
@ -23668,35 +23654,7 @@ def save_artist_bubble_snapshot():
|
|||
"""
|
||||
Saves a snapshot of current artist bubble state for persistence across page refreshes.
|
||||
"""
|
||||
try:
|
||||
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']
|
||||
|
||||
db = get_database()
|
||||
db.save_bubble_snapshot('artist_bubbles', bubbles, profile_id=get_current_profile_id())
|
||||
|
||||
bubble_count = len(bubbles)
|
||||
logger.info(f"Saved artist bubble snapshot: {bubble_count} artists")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Snapshot saved with {bubble_count} artist bubbles',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving artist bubble snapshot: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
return _save_source_bubble_snapshot("bubbles", "No bubble data provided", "artist_bubbles", "artist bubbles", "artist bubble snapshot", "artists")
|
||||
|
||||
@app.route('/api/artist_bubbles/hydrate', methods=['GET'])
|
||||
def hydrate_artist_bubbles():
|
||||
|
|
@ -23840,35 +23798,7 @@ def save_search_bubble_snapshot():
|
|||
"""
|
||||
Saves a snapshot of current search bubble state for persistence across page refreshes.
|
||||
"""
|
||||
try:
|
||||
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']
|
||||
|
||||
db = get_database()
|
||||
db.save_bubble_snapshot('search_bubbles', bubbles, profile_id=get_current_profile_id())
|
||||
|
||||
bubble_count = len(bubbles)
|
||||
logger.info(f"Saved search bubble snapshot: {bubble_count} albums/tracks")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Snapshot saved with {bubble_count} search bubbles',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving search bubble snapshot: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
return _save_source_bubble_snapshot("bubbles", "No bubble data provided", "search_bubbles", "search bubbles", "search bubble snapshot", "albums/tracks")
|
||||
|
||||
@app.route('/api/search_bubbles/hydrate', methods=['GET'])
|
||||
def hydrate_search_bubbles():
|
||||
|
|
@ -24003,35 +23933,7 @@ def hydrate_search_bubbles():
|
|||
@app.route('/api/beatport_bubbles/snapshot', methods=['POST'])
|
||||
def save_beatport_bubble_snapshot():
|
||||
"""Saves a snapshot of current Beatport download bubble state for persistence."""
|
||||
try:
|
||||
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']
|
||||
|
||||
db = get_database()
|
||||
db.save_bubble_snapshot('beatport_bubbles', bubbles, profile_id=get_current_profile_id())
|
||||
|
||||
bubble_count = len(bubbles)
|
||||
logger.info(f"Saved Beatport bubble snapshot: {bubble_count} charts")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Snapshot saved with {bubble_count} Beatport bubbles',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving Beatport bubble snapshot: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
return _save_source_bubble_snapshot("bubbles", "No bubble data provided", "beatport_bubbles", "Beatport bubbles", "Beatport bubble snapshot", "charts")
|
||||
|
||||
@app.route('/api/beatport_bubbles/hydrate', methods=['GET'])
|
||||
def hydrate_beatport_bubbles():
|
||||
|
|
|
|||
Loading…
Reference in a new issue