Discovery lift (3/N): get_*_sync_status -> shared helper

Third cluster: the get_<source>_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.
This commit is contained in:
BoulderBadgeDad 2026-05-28 16:51:16 -07:00
parent 2d76a7c061
commit aad1d2b8f0
3 changed files with 250 additions and 267 deletions

View file

@ -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_<source>_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 <X> 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

View file

@ -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

View file

@ -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/<playlist_id>', 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/<playlist_id>', methods=['POST'])
def cancel_tidal_sync(playlist_id):
@ -21812,45 +21787,7 @@ def start_deezer_sync(playlist_id):
@app.route('/api/deezer/sync/status/<playlist_id>', 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/<playlist_id>', methods=['POST'])
def cancel_deezer_sync(playlist_id):
@ -22395,43 +22332,7 @@ def start_qobuz_sync(playlist_id):
@app.route('/api/qobuz/sync/status/<playlist_id>', 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/<playlist_id>', methods=['POST'])
@ -23271,45 +23172,7 @@ def start_spotify_public_sync(url_hash):
@app.route('/api/spotify-public/sync/status/<url_hash>', 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/<url_hash>', 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/<url_hash>', 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/<url_hash>', methods=['POST'])
@ -24348,46 +24183,7 @@ def start_youtube_sync(url_hash):
@app.route('/api/youtube/sync/status/<url_hash>', 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/<url_hash>', methods=['POST'])
def cancel_youtube_sync(url_hash):
@ -30456,47 +30252,7 @@ def start_listenbrainz_sync(playlist_mbid):
@app.route('/api/listenbrainz/sync/status/<playlist_mbid>', 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/<playlist_mbid>', methods=['POST'])
def cancel_listenbrainz_sync(playlist_mbid):