Discovery lift (4/N): get_*_discovery_status -> shared helper

Fourth cluster: get_<source>_discovery_status (all eight sources, Beatport
included) -> core.discovery.endpoints.get_discovery_status(states, key, *,
not_found_message, error_label), wired via _get_source_discovery_status.

Unlike sync-status, the discovery-status response shape is byte-identical
across every source (phase/status/progress/spotify_matches/spotify_total/
results/complete), so Beatport folds in here too. Only the 404 string
("... discovery not found" vs "... playlist not found" vs "Beatport chart
not found") and the except-log label vary. ListenBrainz key via _lb_state_key.

NOT touched this cluster: get_*_playlist_state (the sibling endpoints).
Those genuinely diverge per source — different id-key name (playlist_id /
url_hash / playlist_mbid), presence of url / created_at / download_process_id,
Tidal's playlist.__dict__ serialization, and YouTube's strict (non-.get)
field access. Folding them would need a flag pile that wouldn't be a clean
1:1, so they keep their own bodies.

Tests: +4 (404, full response + last_accessed bump, complete=False when not
'discovered', missing-field -> 500). Full discovery suite: 175 passed.

web_server.py: -155 lines.
This commit is contained in:
BoulderBadgeDad 2026-05-28 17:00:20 -07:00
parent aad1d2b8f0
commit 8a9ed677ab
3 changed files with 107 additions and 172 deletions

View file

@ -249,3 +249,42 @@ def get_sync_status(
except Exception as e:
logger.error(f"Error getting {error_label} sync status: {e}")
return {"error": str(e)}, 500
def get_discovery_status(
states: Dict[str, Any],
key: str,
*,
not_found_message: str,
error_label: str,
) -> Tuple[Dict[str, Any], int]:
"""Report real-time discovery progress/results for one playlist.
1:1 lift of the byte-identical ``get_<source>_discovery_status`` bodies.
Unlike sync-status, this shape is identical for ALL eight sources
Beatport included so it folds in too. Only the 404 message
(".../discovery not found" vs ".../playlist not found" vs "Beatport chart
not found") and the except-log label vary, both passed in. The caller
resolves the key (ListenBrainz via ``_lb_state_key``).
Returns ``(payload, status_code)``.
"""
try:
if key not in states:
return {"error": not_found_message}, 404
state = states[key]
state['last_accessed'] = time.time()
return {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered',
}, 200
except Exception as e:
logger.error(f"Error getting {error_label} discovery status: {e}")
return {"error": str(e)}, 500

View file

@ -394,3 +394,54 @@ def test_status_strict_getter_missing_playlist_raises_500_after_partial_mutation
# phase was set to sync_complete BEFORE the strict getter raised (1:1).
assert states['pl']['phase'] == 'sync_complete'
assert calls == [] # activity never posted
# ---------------------------------------------------------------------------
# get_discovery_status
# ---------------------------------------------------------------------------
def test_discovery_status_not_found():
from core.discovery.endpoints import get_discovery_status
body, code = get_discovery_status({}, 'missing',
not_found_message='Tidal discovery not found',
error_label='Tidal')
assert code == 404 and body == {"error": "Tidal discovery not found"}
def test_discovery_status_builds_response_and_bumps_access():
from core.discovery.endpoints import get_discovery_status
state = {
'phase': 'discovered', 'status': 'done', 'discovery_progress': 100,
'spotify_matches': 8, 'spotify_total': 10,
'discovery_results': [{'x': 1}], 'last_accessed': 0,
}
states = {'pl': state}
body, code = get_discovery_status(states, 'pl',
not_found_message='nf', error_label='Tidal')
assert code == 200
assert body == {
'phase': 'discovered', 'status': 'done', 'progress': 100,
'spotify_matches': 8, 'spotify_total': 10,
'results': [{'x': 1}], 'complete': True,
}
assert state['last_accessed'] != 0
def test_discovery_status_complete_false_when_not_discovered():
from core.discovery.endpoints import get_discovery_status
state = {
'phase': 'discovering', 'status': 'running', 'discovery_progress': 40,
'spotify_matches': 2, 'spotify_total': 10, 'discovery_results': [],
}
body, code = get_discovery_status({'pl': state}, 'pl',
not_found_message='nf', error_label='Deezer')
assert code == 200 and body['complete'] is False
def test_discovery_status_missing_field_raises_500():
from core.discovery.endpoints import get_discovery_status
# state missing 'status' -> strict access raises -> 500 (matches original)
states = {'pl': {'phase': 'x'}}
body, code = get_discovery_status(states, 'pl',
not_found_message='nf', error_label='Beatport')
assert code == 500 and "error" in body

View file

@ -20560,28 +20560,7 @@ def start_tidal_discovery(playlist_id):
@app.route('/api/tidal/discovery/status/<playlist_id>', methods=['GET'])
def get_tidal_discovery_status(playlist_id):
"""Get real-time discovery status for a Tidal playlist"""
try:
if playlist_id not in tidal_discovery_states:
return jsonify({"error": "Tidal discovery not found"}), 404
state = tidal_discovery_states[playlist_id]
state['last_accessed'] = time.time() # Update access time
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"Error getting Tidal discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(tidal_discovery_states, playlist_id, "Tidal discovery not found", "Tidal")
@app.route('/api/tidal/discovery/update_match', methods=['POST'])
@ -21061,6 +21040,7 @@ from core.discovery.endpoints import (
cancel_sync as _cancel_sync_core,
delete_playlist_state as _delete_playlist_state_core,
get_sync_status as _get_sync_status_core,
get_discovery_status as _get_discovery_status_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,
@ -21099,6 +21079,14 @@ def _get_source_sync_status(states, key, not_found_message, error_label,
return jsonify(body), code
def _get_source_discovery_status(states, key, not_found_message, error_label):
"""Thin glue for the per-source get_*_discovery_status routes."""
body, code = _get_discovery_status_core(
states, key, not_found_message=not_found_message, error_label=error_label,
)
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(
@ -21418,28 +21406,7 @@ def start_deezer_discovery(playlist_id):
@app.route('/api/deezer/discovery/status/<playlist_id>', methods=['GET'])
def get_deezer_discovery_status(playlist_id):
"""Get real-time discovery status for a Deezer playlist"""
try:
if playlist_id not in deezer_discovery_states:
return jsonify({"error": "Deezer discovery not found"}), 404
state = deezer_discovery_states[playlist_id]
state['last_accessed'] = time.time()
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"Error getting Deezer discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(deezer_discovery_states, playlist_id, "Deezer discovery not found", "Deezer")
@app.route('/api/deezer/discovery/update_match', methods=['POST'])
def update_deezer_discovery_match():
@ -21985,28 +21952,7 @@ def start_qobuz_discovery(playlist_id):
@app.route('/api/qobuz/discovery/status/<playlist_id>', methods=['GET'])
def get_qobuz_discovery_status(playlist_id):
"""Get real-time discovery status for a Qobuz playlist."""
try:
if playlist_id not in qobuz_discovery_states:
return jsonify({"error": "Qobuz discovery not found"}), 404
state = qobuz_discovery_states[playlist_id]
state['last_accessed'] = time.time()
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"Error getting Qobuz discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(qobuz_discovery_states, playlist_id, "Qobuz discovery not found", "Qobuz")
@app.route('/api/qobuz/discovery/update_match', methods=['POST'])
@ -22803,28 +22749,7 @@ def start_spotify_public_discovery(url_hash):
@app.route('/api/spotify-public/discovery/status/<url_hash>', methods=['GET'])
def get_spotify_public_discovery_status(url_hash):
"""Get real-time discovery status for a Spotify Public playlist"""
try:
if url_hash not in spotify_public_discovery_states:
return jsonify({"error": "Spotify Public discovery not found"}), 404
state = spotify_public_discovery_states[url_hash]
state['last_accessed'] = time.time()
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"Error getting Spotify Public discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(spotify_public_discovery_states, url_hash, "Spotify Public discovery not found", "Spotify Public")
@app.route('/api/spotify-public/discovery/update_match', methods=['POST'])
def update_spotify_public_discovery_match():
@ -23311,23 +23236,7 @@ def start_itunes_link_discovery(url_hash):
@app.route('/api/itunes-link/discovery/status/<url_hash>', methods=['GET'])
def get_itunes_link_discovery_status(url_hash):
try:
if url_hash not in itunes_link_discovery_states:
return jsonify({"error": "iTunes Link discovery not found"}), 404
state = itunes_link_discovery_states[url_hash]
state['last_accessed'] = time.time()
return jsonify({
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
})
except Exception as e:
logger.error(f"Error getting iTunes Link discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(itunes_link_discovery_states, url_hash, "iTunes Link discovery not found", "iTunes Link")
def _update_itunes_link_discovery_result(identifier, track_index, spotify_track):
@ -23727,28 +23636,7 @@ def start_youtube_discovery(url_hash):
@app.route('/api/youtube/discovery/status/<url_hash>', methods=['GET'])
def get_youtube_discovery_status(url_hash):
"""Get real-time discovery status for a YouTube playlist"""
try:
if url_hash not in youtube_playlist_states:
return jsonify({"error": "YouTube playlist not found"}), 404
state = youtube_playlist_states[url_hash]
state['last_accessed'] = time.time() # Update access time
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"Error getting YouTube discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(youtube_playlist_states, url_hash, "YouTube playlist not found", "YouTube")
@app.route('/api/youtube/discovery/unmatch', methods=['POST'])
@ -30002,29 +29890,7 @@ def start_listenbrainz_discovery(playlist_mbid):
@app.route('/api/listenbrainz/discovery/status/<playlist_mbid>', methods=['GET'])
def get_listenbrainz_discovery_status(playlist_mbid):
"""Get real-time discovery status for a ListenBrainz playlist"""
try:
state_key = _lb_state_key(playlist_mbid)
if state_key not in listenbrainz_playlist_states:
return jsonify({"error": "ListenBrainz playlist not found"}), 404
state = listenbrainz_playlist_states[state_key]
state['last_accessed'] = time.time()
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"Error getting ListenBrainz discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(listenbrainz_playlist_states, _lb_state_key(playlist_mbid), "ListenBrainz playlist not found", "ListenBrainz")
@app.route('/api/listenbrainz/update-phase/<playlist_mbid>', methods=['POST'])
def update_listenbrainz_phase(playlist_mbid):
@ -32018,28 +31884,7 @@ def start_beatport_discovery(url_hash):
@app.route('/api/beatport/discovery/status/<url_hash>', methods=['GET'])
def get_beatport_discovery_status(url_hash):
"""Get real-time discovery status for a Beatport chart"""
try:
if url_hash not in beatport_chart_states:
return jsonify({"error": "Beatport chart not found"}), 404
state = beatport_chart_states[url_hash]
state['last_accessed'] = time.time()
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"Error getting Beatport discovery status: {e}")
return jsonify({"error": str(e)}), 500
return _get_source_discovery_status(beatport_chart_states, url_hash, "Beatport chart not found", "Beatport")
@app.route('/api/beatport/discovery/update_match', methods=['POST'])