From aad1d2b8f0e2debb9073c4c99c11d95b4bf694ff Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Thu, 28 May 2026 16:51:16 -0700 Subject: [PATCH] Discovery lift (3/N): get_*_sync_status -> shared helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third cluster: the get__sync_status routes (Tidal, Deezer, Qobuz, Spotify-Public, iTunes-Link, YouTube, ListenBrainz) -> core.discovery. endpoints.get_sync_status(...), wired via _get_source_sync_status glue. This cluster carries the real per-source quirks, all captured 1:1 as params: - not_found_message (iTunes-Link uses "iTunes Link not found"). - error_label vs activity_subject — these DIFFER for Spotify-Public: the activity feed says "Spotify Link playlist ..." while the except log says "Error getting Spotify Public sync status". - playlist-name accessor, three styles lifted verbatim as named helpers: playlist_name_attr_or_unknown (Tidal: object .name), playlist_name_strict (Deezer/Qobuz/Spotify-Public/iTunes: state['playlist']['name'], can raise), playlist_name_safe (YouTube/ListenBrainz: .get default). The strict getter preserves the original's behavior of raising -> 500 AFTER phase/sync_progress were already mutated. - ListenBrainz key via _lb_state_key (caller-resolved). Beatport stays separate (different payload: status not sync_status, sync_id, no lock, chart key). Tests: +9 (3 name accessors incl. raise/fallback semantics; status 404s, running-no-mutation, finished+activity, error+revert+activity, and strict- getter-missing -> 500 after partial mutation). Full discovery suite: 171 passed. web_server.py: -244 lines. --- core/discovery/endpoints.py | 92 +++++++ tests/discovery/test_discovery_endpoints.py | 135 +++++++++ web_server.py | 290 ++------------------ 3 files changed, 250 insertions(+), 267 deletions(-) diff --git a/core/discovery/endpoints.py b/core/discovery/endpoints.py index 61d8cc0b..f9ca30b5 100644 --- a/core/discovery/endpoints.py +++ b/core/discovery/endpoints.py @@ -157,3 +157,95 @@ def delete_playlist_state( except Exception as e: logger.error(f"Error deleting {label} playlist: {e}") return {"error": str(e)}, 500 + + +# --- playlist-name accessors ------------------------------------------------- +# The per-source sync-status handlers read the display name three different +# ways. Each is reproduced verbatim so the 1:1 behavior (including which ones +# raise vs. fall back to 'Unknown Playlist') is preserved. + +def playlist_name_attr_or_unknown(state: Dict[str, Any]) -> str: + """Tidal: playlist is an object — use ``.name`` or 'Unknown Playlist'.""" + pl = state.get('playlist') + return pl.name if pl and hasattr(pl, 'name') else 'Unknown Playlist' + + +def playlist_name_strict(state: Dict[str, Any]) -> str: + """Deezer / Qobuz / Spotify-Public / iTunes-Link: strict dict access — + raises (→ 500) if 'playlist' is missing, exactly like the originals.""" + return state['playlist']['name'] + + +def playlist_name_safe(state: Dict[str, Any]) -> str: + """YouTube / ListenBrainz: safe dict access, defaulting to 'Unknown + Playlist'.""" + return state.get('playlist', {}).get('name', 'Unknown Playlist') + + +def get_sync_status( + states: Dict[str, Any], + key: str, + *, + not_found_message: str, + error_label: str, + activity_subject: str, + playlist_name_getter, + sync_lock: Any, + sync_states: Dict[str, Any], + add_activity_item, +) -> Tuple[Dict[str, Any], int]: + """Report sync status for one discovery playlist, posting an activity-feed + item when the sync finishes or errors. + + 1:1 lift of the ``get__sync_status`` bodies (Tidal, Deezer, Qobuz, + Spotify-Public, iTunes-Link, YouTube, ListenBrainz). Per-source variation + is captured by the parameters: + + - ``not_found_message`` — the 404 string (iTunes-Link drops "playlist"). + - ``error_label`` — used in the except log ("Error getting sync status"). + - ``activity_subject`` — the activity-feed prefix; note Spotify-Public uses + "Spotify Link playlist" while its error_label is "Spotify Public". + - ``playlist_name_getter`` — one of the accessors above (attr/strict/safe); + the strict one can raise, matching the originals (→ 500). The state's + phase/sync_progress are mutated BEFORE the name is read, so a raising + getter leaves the same partial mutation the original did. + + Beatport is NOT routed here — it returns a different payload (``status`` + not ``sync_status``, includes ``sync_id``, no lock, ``chart`` key). + """ + try: + if key not in states: + return {"error": not_found_message}, 404 + + state = states[key] + state['last_accessed'] = time.time() + sync_playlist_id = state.get('sync_playlist_id') + + if not sync_playlist_id: + return {"error": "No sync in progress"}, 404 + + with sync_lock: + sync_state = sync_states.get(sync_playlist_id, {}) + + response = { + 'phase': state['phase'], + 'sync_status': sync_state.get('status', 'unknown'), + 'progress': sync_state.get('progress', {}), + 'complete': sync_state.get('status') == 'finished', + 'error': sync_state.get('error'), + } + + if sync_state.get('status') == 'finished': + state['phase'] = 'sync_complete' + state['sync_progress'] = sync_state.get('progress', {}) + playlist_name = playlist_name_getter(state) + add_activity_item("", "Sync Complete", f"{activity_subject} '{playlist_name}' synced successfully", "Now") + elif sync_state.get('status') == 'error': + state['phase'] = 'discovered' + playlist_name = playlist_name_getter(state) + add_activity_item("", "Sync Failed", f"{activity_subject} '{playlist_name}' sync failed", "Now") + + return response, 200 + except Exception as e: + logger.error(f"Error getting {error_label} sync status: {e}") + return {"error": str(e)}, 500 diff --git a/tests/discovery/test_discovery_endpoints.py b/tests/discovery/test_discovery_endpoints.py index 2e346f84..968d79cf 100644 --- a/tests/discovery/test_discovery_endpoints.py +++ b/tests/discovery/test_discovery_endpoints.py @@ -14,6 +14,9 @@ from core.discovery.endpoints import ( convert_results_to_spotify_tracks, cancel_sync, delete_playlist_state, + get_sync_status, + playlist_name_strict as _pl_name_strict, + playlist_name_safe as _pl_name_safe, ) @@ -259,3 +262,135 @@ def test_delete_exception_returns_500(): assert "error" in body # state NOT deleted because the exception fired before del assert 'pl' in states + + +# --------------------------------------------------------------------------- +# playlist-name accessors +# --------------------------------------------------------------------------- + +class _Obj: + def __init__(self, name): + self.name = name + + +def test_name_attr_or_unknown(): + from core.discovery.endpoints import playlist_name_attr_or_unknown as g + assert g({'playlist': _Obj('My PL')}) == 'My PL' + assert g({'playlist': {'name': 'dict'}}) == 'Unknown Playlist' # dict has no .name attr + assert g({}) == 'Unknown Playlist' + assert g({'playlist': None}) == 'Unknown Playlist' + + +def test_name_strict(): + from core.discovery.endpoints import playlist_name_strict as g + assert g({'playlist': {'name': 'X'}}) == 'X' + import pytest + with pytest.raises(KeyError): + g({}) # strict -> raises, matching originals + + +def test_name_safe(): + from core.discovery.endpoints import playlist_name_safe as g + assert g({'playlist': {'name': 'X'}}) == 'X' + assert g({}) == 'Unknown Playlist' + assert g({'playlist': {}}) == 'Unknown Playlist' + + +# --------------------------------------------------------------------------- +# get_sync_status +# --------------------------------------------------------------------------- + +def _activity_recorder(): + calls = [] + + def add_activity_item(user, action, desc, when): + calls.append((user, action, desc, when)) + + return calls, add_activity_item + + +def _status_kwargs(infra, add_activity_item, *, not_found_message='nf', + error_label='Tidal', activity_subject='Tidal playlist', + name_getter=None): + return dict( + not_found_message=not_found_message, error_label=error_label, + activity_subject=activity_subject, + playlist_name_getter=name_getter or _pl_name_safe, + add_activity_item=add_activity_item, + sync_lock=infra['sync_lock'], sync_states=infra['sync_states'], + ) + + +def test_status_not_found(): + infra = _cancel_infra() + calls, add = _activity_recorder() + body, code = get_sync_status({}, 'missing', + **_status_kwargs(infra, add, not_found_message='Tidal playlist not found')) + assert code == 404 and body == {"error": "Tidal playlist not found"} + assert calls == [] + + +def test_status_no_sync_in_progress(): + infra = _cancel_infra() + calls, add = _activity_recorder() + states = {'pl': {'phase': 'discovered'}} # no sync_playlist_id + body, code = get_sync_status(states, 'pl', **_status_kwargs(infra, add)) + assert code == 404 and body == {"error": "No sync in progress"} + + +def test_status_running_returns_shape_without_mutation_or_activity(): + infra = _cancel_infra() + infra['sync_states']['sp1'] = {"status": "running", "progress": {"n": 2}} + calls, add = _activity_recorder() + states = {'pl': {'phase': 'syncing', 'sync_playlist_id': 'sp1'}} + body, code = get_sync_status(states, 'pl', **_status_kwargs(infra, add)) + assert code == 200 + assert body == { + 'phase': 'syncing', 'sync_status': 'running', + 'progress': {"n": 2}, 'complete': False, 'error': None, + } + assert states['pl']['phase'] == 'syncing' # unchanged + assert calls == [] + + +def test_status_finished_sets_complete_and_posts_activity(): + infra = _cancel_infra() + infra['sync_states']['sp1'] = {"status": "finished", "progress": {"done": 9}} + calls, add = _activity_recorder() + states = {'pl': {'phase': 'syncing', 'sync_playlist_id': 'sp1', + 'playlist': {'name': 'Mix'}}} + body, code = get_sync_status(states, 'pl', **_status_kwargs( + infra, add, activity_subject='Spotify Link playlist', + name_getter=_pl_name_strict)) + assert code == 200 + assert body['complete'] is True + assert states['pl']['phase'] == 'sync_complete' + assert states['pl']['sync_progress'] == {"done": 9} + assert calls == [("", "Sync Complete", "Spotify Link playlist 'Mix' synced successfully", "Now")] + + +def test_status_error_reverts_and_posts_failed_activity(): + infra = _cancel_infra() + infra['sync_states']['sp1'] = {"status": "error", "error": "boom"} + calls, add = _activity_recorder() + states = {'pl': {'phase': 'syncing', 'sync_playlist_id': 'sp1', + 'playlist': {'name': 'Mix'}}} + body, code = get_sync_status(states, 'pl', **_status_kwargs( + infra, add, activity_subject='YouTube playlist', name_getter=_pl_name_safe)) + assert code == 200 + assert body['error'] == "boom" + assert states['pl']['phase'] == 'discovered' + assert calls == [("", "Sync Failed", "YouTube playlist 'Mix' sync failed", "Now")] + + +def test_status_strict_getter_missing_playlist_raises_500_after_partial_mutation(): + infra = _cancel_infra() + infra['sync_states']['sp1'] = {"status": "finished", "progress": {}} + calls, add = _activity_recorder() + states = {'pl': {'phase': 'syncing', 'sync_playlist_id': 'sp1'}} # no 'playlist' + body, code = get_sync_status(states, 'pl', **_status_kwargs( + infra, add, error_label='Deezer', name_getter=_pl_name_strict)) + assert code == 500 and "error" in body + # phase was set to sync_complete BEFORE the strict getter raised (1:1). + assert states['pl']['phase'] == 'sync_complete' + assert calls == [] # activity never posted diff --git a/web_server.py b/web_server.py index 54d0a247..ce7f9dec 100644 --- a/web_server.py +++ b/web_server.py @@ -21060,6 +21060,10 @@ from core.discovery.endpoints import ( convert_results_to_spotify_tracks, cancel_sync as _cancel_sync_core, delete_playlist_state as _delete_playlist_state_core, + get_sync_status as _get_sync_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, ) @@ -21083,6 +21087,18 @@ def _delete_source_playlist(states, key, label, not_found_message): return jsonify(body), code +def _get_source_sync_status(states, key, not_found_message, error_label, + activity_subject, name_getter): + """Thin glue for the per-source get_*_sync_status routes — wires the sync + infra + add_activity_item into the lifted helper and jsonifies.""" + body, code = _get_sync_status_core( + states, key, not_found_message=not_found_message, error_label=error_label, + activity_subject=activity_subject, playlist_name_getter=name_getter, + sync_lock=sync_lock, sync_states=sync_states, add_activity_item=add_activity_item, + ) + 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( @@ -21174,48 +21190,7 @@ def start_tidal_sync(playlist_id): @app.route('/api/tidal/sync/status/', methods=['GET']) def get_tidal_sync_status(playlist_id): """Get sync status for a Tidal playlist""" - try: - if playlist_id not in tidal_discovery_states: - return jsonify({"error": "Tidal playlist not found"}), 404 - - state = tidal_discovery_states[playlist_id] - state['last_accessed'] = time.time() # Update access time - sync_playlist_id = state.get('sync_playlist_id') - - if not sync_playlist_id: - return jsonify({"error": "No sync in progress"}), 404 - - # Get sync status from existing sync infrastructure - with sync_lock: - sync_state = sync_states.get(sync_playlist_id, {}) - - response = { - 'phase': state['phase'], - 'sync_status': sync_state.get('status', 'unknown'), - 'progress': sync_state.get('progress', {}), - 'complete': sync_state.get('status') == 'finished', - 'error': sync_state.get('error') - } - - # Update Tidal state if sync completed - if sync_state.get('status') == 'finished': - state['phase'] = 'sync_complete' - state['sync_progress'] = sync_state.get('progress', {}) - # Add activity for sync completion - playlist = state.get('playlist') - playlist_name = playlist.name if playlist and hasattr(playlist, 'name') else 'Unknown Playlist' - add_activity_item("", "Sync Complete", f"Tidal playlist '{playlist_name}' synced successfully", "Now") - elif sync_state.get('status') == 'error': - state['phase'] = 'discovered' # Revert on error - playlist = state.get('playlist') - playlist_name = playlist.name if playlist and hasattr(playlist, 'name') else 'Unknown Playlist' - add_activity_item("", "Sync Failed", f"Tidal playlist '{playlist_name}' sync failed", "Now") - - return jsonify(response) - - except Exception as e: - logger.error(f"Error getting Tidal sync status: {e}") - return jsonify({"error": str(e)}), 500 + return _get_source_sync_status(tidal_discovery_states, playlist_id, "Tidal playlist not found", "Tidal", "Tidal playlist", _pl_name_attr_or_unknown) @app.route('/api/tidal/sync/cancel/', methods=['POST']) def cancel_tidal_sync(playlist_id): @@ -21812,45 +21787,7 @@ def start_deezer_sync(playlist_id): @app.route('/api/deezer/sync/status/', methods=['GET']) def get_deezer_sync_status(playlist_id): """Get sync status for a Deezer playlist""" - try: - if playlist_id not in deezer_discovery_states: - return jsonify({"error": "Deezer playlist not found"}), 404 - - state = deezer_discovery_states[playlist_id] - state['last_accessed'] = time.time() - sync_playlist_id = state.get('sync_playlist_id') - - if not sync_playlist_id: - return jsonify({"error": "No sync in progress"}), 404 - - # Get sync status from existing sync infrastructure - with sync_lock: - sync_state = sync_states.get(sync_playlist_id, {}) - - response = { - 'phase': state['phase'], - 'sync_status': sync_state.get('status', 'unknown'), - 'progress': sync_state.get('progress', {}), - 'complete': sync_state.get('status') == 'finished', - 'error': sync_state.get('error') - } - - # Update Deezer state if sync completed - if sync_state.get('status') == 'finished': - state['phase'] = 'sync_complete' - state['sync_progress'] = sync_state.get('progress', {}) - playlist_name = state['playlist']['name'] - add_activity_item("", "Sync Complete", f"Deezer playlist '{playlist_name}' synced successfully", "Now") - elif sync_state.get('status') == 'error': - state['phase'] = 'discovered' # Revert on error - playlist_name = state['playlist']['name'] - add_activity_item("", "Sync Failed", f"Deezer playlist '{playlist_name}' sync failed", "Now") - - return jsonify(response) - - except Exception as e: - logger.error(f"Error getting Deezer sync status: {e}") - return jsonify({"error": str(e)}), 500 + return _get_source_sync_status(deezer_discovery_states, playlist_id, "Deezer playlist not found", "Deezer", "Deezer playlist", _pl_name_strict) @app.route('/api/deezer/sync/cancel/', methods=['POST']) def cancel_deezer_sync(playlist_id): @@ -22395,43 +22332,7 @@ def start_qobuz_sync(playlist_id): @app.route('/api/qobuz/sync/status/', methods=['GET']) def get_qobuz_sync_status(playlist_id): """Get sync status for a Qobuz playlist.""" - try: - if playlist_id not in qobuz_discovery_states: - return jsonify({"error": "Qobuz playlist not found"}), 404 - - state = qobuz_discovery_states[playlist_id] - state['last_accessed'] = time.time() - sync_playlist_id = state.get('sync_playlist_id') - - if not sync_playlist_id: - return jsonify({"error": "No sync in progress"}), 404 - - with sync_lock: - sync_state = sync_states.get(sync_playlist_id, {}) - - response = { - 'phase': state['phase'], - 'sync_status': sync_state.get('status', 'unknown'), - 'progress': sync_state.get('progress', {}), - 'complete': sync_state.get('status') == 'finished', - 'error': sync_state.get('error') - } - - if sync_state.get('status') == 'finished': - state['phase'] = 'sync_complete' - state['sync_progress'] = sync_state.get('progress', {}) - playlist_name = state['playlist']['name'] - add_activity_item("", "Sync Complete", f"Qobuz playlist '{playlist_name}' synced successfully", "Now") - elif sync_state.get('status') == 'error': - state['phase'] = 'discovered' - playlist_name = state['playlist']['name'] - add_activity_item("", "Sync Failed", f"Qobuz playlist '{playlist_name}' sync failed", "Now") - - return jsonify(response) - - except Exception as e: - logger.error(f"Error getting Qobuz sync status: {e}") - return jsonify({"error": str(e)}), 500 + return _get_source_sync_status(qobuz_discovery_states, playlist_id, "Qobuz playlist not found", "Qobuz", "Qobuz playlist", _pl_name_strict) @app.route('/api/qobuz/sync/cancel/', methods=['POST']) @@ -23271,45 +23172,7 @@ def start_spotify_public_sync(url_hash): @app.route('/api/spotify-public/sync/status/', methods=['GET']) def get_spotify_public_sync_status(url_hash): """Get sync status for a Spotify Public playlist""" - try: - if url_hash not in spotify_public_discovery_states: - return jsonify({"error": "Spotify Public playlist not found"}), 404 - - state = spotify_public_discovery_states[url_hash] - state['last_accessed'] = time.time() - sync_playlist_id = state.get('sync_playlist_id') - - if not sync_playlist_id: - return jsonify({"error": "No sync in progress"}), 404 - - # Get sync status from existing sync infrastructure - with sync_lock: - sync_state = sync_states.get(sync_playlist_id, {}) - - response = { - 'phase': state['phase'], - 'sync_status': sync_state.get('status', 'unknown'), - 'progress': sync_state.get('progress', {}), - 'complete': sync_state.get('status') == 'finished', - 'error': sync_state.get('error') - } - - # Update Spotify Public state if sync completed - if sync_state.get('status') == 'finished': - state['phase'] = 'sync_complete' - state['sync_progress'] = sync_state.get('progress', {}) - playlist_name = state['playlist']['name'] - add_activity_item("", "Sync Complete", f"Spotify Link playlist '{playlist_name}' synced successfully", "Now") - elif sync_state.get('status') == 'error': - state['phase'] = 'discovered' # Revert on error - playlist_name = state['playlist']['name'] - add_activity_item("", "Sync Failed", f"Spotify Link playlist '{playlist_name}' sync failed", "Now") - - return jsonify(response) - - except Exception as e: - logger.error(f"Error getting Spotify Public sync status: {e}") - return jsonify({"error": str(e)}), 500 + return _get_source_sync_status(spotify_public_discovery_states, url_hash, "Spotify Public playlist not found", "Spotify Public", "Spotify Link playlist", _pl_name_strict) @app.route('/api/spotify-public/sync/cancel/', methods=['POST']) def cancel_spotify_public_sync(url_hash): @@ -23711,35 +23574,7 @@ def start_itunes_link_sync(url_hash): @app.route('/api/itunes-link/sync/status/', methods=['GET']) def get_itunes_link_sync_status(url_hash): - try: - if url_hash not in itunes_link_discovery_states: - return jsonify({"error": "iTunes Link not found"}), 404 - state = itunes_link_discovery_states[url_hash] - state['last_accessed'] = time.time() - sync_playlist_id = state.get('sync_playlist_id') - if not sync_playlist_id: - return jsonify({"error": "No sync in progress"}), 404 - - with sync_lock: - sync_state = sync_states.get(sync_playlist_id, {}) - response = { - 'phase': state['phase'], - 'sync_status': sync_state.get('status', 'unknown'), - 'progress': sync_state.get('progress', {}), - 'complete': sync_state.get('status') == 'finished', - 'error': sync_state.get('error') - } - if sync_state.get('status') == 'finished': - state['phase'] = 'sync_complete' - state['sync_progress'] = sync_state.get('progress', {}) - add_activity_item("", "Sync Complete", f"iTunes Link '{state['playlist']['name']}' synced successfully", "Now") - elif sync_state.get('status') == 'error': - state['phase'] = 'discovered' - add_activity_item("", "Sync Failed", f"iTunes Link '{state['playlist']['name']}' sync failed", "Now") - return jsonify(response) - except Exception as e: - logger.error(f"Error getting iTunes Link sync status: {e}") - return jsonify({"error": str(e)}), 500 + return _get_source_sync_status(itunes_link_discovery_states, url_hash, "iTunes Link not found", "iTunes Link", "iTunes Link", _pl_name_strict) @app.route('/api/itunes-link/sync/cancel/', methods=['POST']) @@ -24348,46 +24183,7 @@ def start_youtube_sync(url_hash): @app.route('/api/youtube/sync/status/', methods=['GET']) def get_youtube_sync_status(url_hash): """Get sync 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 - sync_playlist_id = state.get('sync_playlist_id') - - if not sync_playlist_id: - return jsonify({"error": "No sync in progress"}), 404 - - # Get sync status from existing sync infrastructure - with sync_lock: - sync_state = sync_states.get(sync_playlist_id, {}) - - response = { - 'phase': state['phase'], - 'sync_status': sync_state.get('status', 'unknown'), - 'progress': sync_state.get('progress', {}), - 'complete': sync_state.get('status') == 'finished', - 'error': sync_state.get('error') - } - - # Update YouTube state if sync completed - if sync_state.get('status') == 'finished': - state['phase'] = 'sync_complete' - state['sync_progress'] = sync_state.get('progress', {}) - # Add activity for sync completion - playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("", "Sync Complete", f"YouTube playlist '{playlist_name}' synced successfully", "Now") - elif sync_state.get('status') == 'error': - state['phase'] = 'discovered' # Revert on error - playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("", "Sync Failed", f"YouTube playlist '{playlist_name}' sync failed", "Now") - - return jsonify(response) - - except Exception as e: - logger.error(f"Error getting YouTube sync status: {e}") - return jsonify({"error": str(e)}), 500 + return _get_source_sync_status(youtube_playlist_states, url_hash, "YouTube playlist not found", "YouTube", "YouTube playlist", _pl_name_safe) @app.route('/api/youtube/sync/cancel/', methods=['POST']) def cancel_youtube_sync(url_hash): @@ -30456,47 +30252,7 @@ def start_listenbrainz_sync(playlist_mbid): @app.route('/api/listenbrainz/sync/status/', methods=['GET']) def get_listenbrainz_sync_status(playlist_mbid): """Get sync 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() # Update access time - sync_playlist_id = state.get('sync_playlist_id') - - if not sync_playlist_id: - return jsonify({"error": "No sync in progress"}), 404 - - # Get sync status from existing sync infrastructure - with sync_lock: - sync_state = sync_states.get(sync_playlist_id, {}) - - response = { - 'phase': state['phase'], - 'sync_status': sync_state.get('status', 'unknown'), - 'progress': sync_state.get('progress', {}), - 'complete': sync_state.get('status') == 'finished', - 'error': sync_state.get('error') - } - - # Update ListenBrainz state if sync completed - if sync_state.get('status') == 'finished': - state['phase'] = 'sync_complete' - state['sync_progress'] = sync_state.get('progress', {}) - # Add activity for sync completion - playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("", "Sync Complete", f"ListenBrainz playlist '{playlist_name}' synced successfully", "Now") - elif sync_state.get('status') == 'error': - state['phase'] = 'discovered' # Revert on error - playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist') - add_activity_item("", "Sync Failed", f"ListenBrainz playlist '{playlist_name}' sync failed", "Now") - - return jsonify(response) - - except Exception as e: - logger.error(f"Error getting ListenBrainz sync status: {e}") - return jsonify({"error": str(e)}), 500 + return _get_source_sync_status(listenbrainz_playlist_states, _lb_state_key(playlist_mbid), "ListenBrainz playlist not found", "ListenBrainz", "ListenBrainz playlist", _pl_name_safe) @app.route('/api/listenbrainz/sync/cancel/', methods=['POST']) def cancel_listenbrainz_sync(playlist_mbid):