From 8a9ed677ab668415c664102eec75513b6625f62a Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Thu, 28 May 2026 17:00:20 -0700 Subject: [PATCH] Discovery lift (4/N): get_*_discovery_status -> shared helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth cluster: get__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. --- core/discovery/endpoints.py | 39 ++++ tests/discovery/test_discovery_endpoints.py | 51 ++++++ web_server.py | 189 ++------------------ 3 files changed, 107 insertions(+), 172 deletions(-) diff --git a/core/discovery/endpoints.py b/core/discovery/endpoints.py index f9ca30b5..81723df5 100644 --- a/core/discovery/endpoints.py +++ b/core/discovery/endpoints.py @@ -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__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 diff --git a/tests/discovery/test_discovery_endpoints.py b/tests/discovery/test_discovery_endpoints.py index 968d79cf..846802f5 100644 --- a/tests/discovery/test_discovery_endpoints.py +++ b/tests/discovery/test_discovery_endpoints.py @@ -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 diff --git a/web_server.py b/web_server.py index ce7f9dec..9ad29b8c 100644 --- a/web_server.py +++ b/web_server.py @@ -20560,28 +20560,7 @@ def start_tidal_discovery(playlist_id): @app.route('/api/tidal/discovery/status/', 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/', 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/', 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/', 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/', 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/', 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/', 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/', methods=['POST']) def update_listenbrainz_phase(playlist_mbid): @@ -32018,28 +31884,7 @@ def start_beatport_discovery(url_hash): @app.route('/api/beatport/discovery/status/', 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'])