Discovery lift (6/N): get_*_playlist_states -> shared helper
Sixth cluster: the bulk-hydration get_<source>_playlist_states endpoints for
the five sources that build the identical per-entry dict + {"states": [...]}
shape (Tidal, Deezer, Qobuz, Spotify-Public, iTunes-Link) ->
core.discovery.endpoints.get_playlist_states(states, *, error_label,
info_log_label=None), wired via _get_source_playlist_states.
iTunes-Link is the only one of the five without the "Returning N stored ..."
info log, so info_log_label is optional (iTunes passes None to suppress it).
NOT folded in: the YouTube/ListenBrainz get_all_*_playlists endpoints. They
return {"playlists": [...]} (different key) with a different field set
(url / created_at / playlist, no discovery_results) and filter out
mirrored_/profile-scoped entries — genuinely divergent, kept as-is.
Tests: +4 (list build + last_accessed bump + exact shape, empty, optional ids
default None, missing-required-field -> 500). Full discovery suite: 183 passed.
web_server.py: -116 lines.
This commit is contained in:
parent
44b032b6c0
commit
7b6615b65a
3 changed files with 104 additions and 131 deletions
|
|
@ -334,3 +334,48 @@ def reset_playlist(
|
|||
except Exception as e:
|
||||
logger.error(f"Error resetting {label} playlist: {e}")
|
||||
return {"error": str(e)}, 500
|
||||
|
||||
|
||||
def get_playlist_states(
|
||||
states: Dict[str, Any],
|
||||
*,
|
||||
error_label: str,
|
||||
info_log_label: str = None,
|
||||
) -> Tuple[Dict[str, Any], int]:
|
||||
"""Return all stored discovery states for a source as a list for frontend
|
||||
card hydration (``{"states": [...]}``).
|
||||
|
||||
1:1 lift of the ``get_<source>_playlist_states`` bodies (Tidal, Deezer,
|
||||
Qobuz, Spotify-Public, iTunes-Link), which build the same per-entry dict.
|
||||
iTunes-Link is the only one without the "Returning N ..." info log, so
|
||||
``info_log_label`` is optional (pass None to suppress it, as iTunes did).
|
||||
|
||||
NOT folded in: the YouTube/ListenBrainz ``get_all_*_playlists`` endpoints —
|
||||
they return ``{"playlists": [...]}`` (different key + fields: url/created_at,
|
||||
no discovery_results) and filter mirrored/profile-scoped entries.
|
||||
"""
|
||||
try:
|
||||
result = []
|
||||
current_time = time.time()
|
||||
|
||||
for key, state in states.items():
|
||||
state['last_accessed'] = current_time
|
||||
result.append({
|
||||
'playlist_id': key,
|
||||
'phase': state['phase'],
|
||||
'status': state['status'],
|
||||
'discovery_progress': state['discovery_progress'],
|
||||
'spotify_matches': state['spotify_matches'],
|
||||
'spotify_total': state['spotify_total'],
|
||||
'discovery_results': state['discovery_results'],
|
||||
'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'),
|
||||
'download_process_id': state.get('download_process_id'),
|
||||
'last_accessed': state['last_accessed'],
|
||||
})
|
||||
|
||||
if info_log_label:
|
||||
logger.info(f"Returning {len(result)} stored {info_log_label} playlist states for hydration")
|
||||
return {"states": result}, 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting {error_label} playlist states: {e}")
|
||||
return {"error": str(e)}, 500
|
||||
|
|
|
|||
|
|
@ -505,3 +505,47 @@ def test_reset_exception_returns_500():
|
|||
states = {'pl': {'discovery_future': fut}}
|
||||
body, code = reset_playlist(states, 'pl', label='Qobuz', not_found_message='nf')
|
||||
assert code == 500 and "error" in body
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_playlist_states (bulk hydration list)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_playlist_states_builds_list_and_bumps_access():
|
||||
from core.discovery.endpoints import get_playlist_states
|
||||
s1 = {'phase': 'discovered', 'status': 'done', 'discovery_progress': 100,
|
||||
'spotify_matches': 3, 'spotify_total': 5, 'discovery_results': [{'a': 1}],
|
||||
'converted_spotify_playlist_id': 'cv', 'download_process_id': 'dp',
|
||||
'last_accessed': 0}
|
||||
states = {'k1': s1}
|
||||
body, code = get_playlist_states(states, error_label='Tidal', info_log_label='Tidal')
|
||||
assert code == 200
|
||||
assert body == {"states": [{
|
||||
'playlist_id': 'k1', 'phase': 'discovered', 'status': 'done',
|
||||
'discovery_progress': 100, 'spotify_matches': 3, 'spotify_total': 5,
|
||||
'discovery_results': [{'a': 1}], 'converted_spotify_playlist_id': 'cv',
|
||||
'download_process_id': 'dp', 'last_accessed': s1['last_accessed'],
|
||||
}]}
|
||||
assert s1['last_accessed'] != 0 # bumped
|
||||
|
||||
|
||||
def test_playlist_states_empty():
|
||||
from core.discovery.endpoints import get_playlist_states
|
||||
body, code = get_playlist_states({}, error_label='Deezer', info_log_label='Deezer')
|
||||
assert code == 200 and body == {"states": []}
|
||||
|
||||
|
||||
def test_playlist_states_optional_ids_default_none():
|
||||
from core.discovery.endpoints import get_playlist_states
|
||||
state = {'phase': 'fresh', 'status': 'fresh', 'discovery_progress': 0,
|
||||
'spotify_matches': 0, 'spotify_total': 0, 'discovery_results': []}
|
||||
body, _ = get_playlist_states({'k': state}, error_label='iTunes Link')
|
||||
assert body['states'][0]['converted_spotify_playlist_id'] is None
|
||||
assert body['states'][0]['download_process_id'] is None
|
||||
|
||||
|
||||
def test_playlist_states_missing_required_field_raises_500():
|
||||
from core.discovery.endpoints import get_playlist_states
|
||||
# state missing 'phase' -> strict access raises -> 500
|
||||
body, code = get_playlist_states({'k': {'status': 'x'}}, error_label='Qobuz')
|
||||
assert code == 500 and "error" in body
|
||||
|
|
|
|||
146
web_server.py
146
web_server.py
|
|
@ -20682,35 +20682,7 @@ def update_tidal_discovery_match():
|
|||
@app.route('/api/tidal/playlists/states', methods=['GET'])
|
||||
def get_tidal_playlist_states():
|
||||
"""Get all stored Tidal playlist discovery states for frontend hydration (similar to YouTube playlists)"""
|
||||
try:
|
||||
states = []
|
||||
current_time = time.time()
|
||||
|
||||
for playlist_id, state in tidal_discovery_states.items():
|
||||
# Update access time when requested
|
||||
state['last_accessed'] = current_time
|
||||
|
||||
# Return essential data for card state recreation
|
||||
state_info = {
|
||||
'playlist_id': playlist_id,
|
||||
'phase': state['phase'],
|
||||
'status': state['status'],
|
||||
'discovery_progress': state['discovery_progress'],
|
||||
'spotify_matches': state['spotify_matches'],
|
||||
'spotify_total': state['spotify_total'],
|
||||
'discovery_results': state['discovery_results'],
|
||||
'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'),
|
||||
'download_process_id': state.get('download_process_id'),
|
||||
'last_accessed': state['last_accessed']
|
||||
}
|
||||
states.append(state_info)
|
||||
|
||||
logger.info(f"Returning {len(states)} stored Tidal playlist states for hydration")
|
||||
return jsonify({"states": states})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Tidal playlist states: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _get_source_playlist_states(tidal_discovery_states, "Tidal", "Tidal")
|
||||
|
||||
@app.route('/api/tidal/state/<playlist_id>', methods=['GET'])
|
||||
def get_tidal_playlist_state(playlist_id):
|
||||
|
|
@ -21014,6 +20986,7 @@ from core.discovery.endpoints import (
|
|||
get_sync_status as _get_sync_status_core,
|
||||
get_discovery_status as _get_discovery_status_core,
|
||||
reset_playlist as _reset_playlist_core,
|
||||
get_playlist_states as _get_playlist_states_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,
|
||||
|
|
@ -21069,6 +21042,15 @@ def _reset_source_playlist(states, key, label, not_found_message):
|
|||
return jsonify(body), code
|
||||
|
||||
|
||||
def _get_source_playlist_states(states, error_label, info_log_label=None):
|
||||
"""Thin glue for the per-source get_*_playlist_states bulk-hydration routes
|
||||
(Tidal/Deezer/Qobuz/Spotify-Public/iTunes-Link)."""
|
||||
body, code = _get_playlist_states_core(
|
||||
states, error_label=error_label, info_log_label=info_log_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(
|
||||
|
|
@ -21506,33 +21488,7 @@ def update_deezer_discovery_match():
|
|||
@app.route('/api/deezer/playlists/states', methods=['GET'])
|
||||
def get_deezer_playlist_states():
|
||||
"""Get all stored Deezer playlist discovery states for frontend hydration"""
|
||||
try:
|
||||
states = []
|
||||
current_time = time.time()
|
||||
|
||||
for playlist_id, state in deezer_discovery_states.items():
|
||||
state['last_accessed'] = current_time
|
||||
|
||||
state_info = {
|
||||
'playlist_id': playlist_id,
|
||||
'phase': state['phase'],
|
||||
'status': state['status'],
|
||||
'discovery_progress': state['discovery_progress'],
|
||||
'spotify_matches': state['spotify_matches'],
|
||||
'spotify_total': state['spotify_total'],
|
||||
'discovery_results': state['discovery_results'],
|
||||
'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'),
|
||||
'download_process_id': state.get('download_process_id'),
|
||||
'last_accessed': state['last_accessed']
|
||||
}
|
||||
states.append(state_info)
|
||||
|
||||
logger.info(f"Returning {len(states)} stored Deezer playlist states for hydration")
|
||||
return jsonify({"states": states})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Deezer playlist states: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _get_source_playlist_states(deezer_discovery_states, "Deezer", "Deezer")
|
||||
|
||||
@app.route('/api/deezer/state/<playlist_id>', methods=['GET'])
|
||||
def get_deezer_playlist_state(playlist_id):
|
||||
|
|
@ -22011,33 +21967,7 @@ def update_qobuz_discovery_match():
|
|||
@app.route('/api/qobuz/playlists/states', methods=['GET'])
|
||||
def get_qobuz_playlist_states():
|
||||
"""Get all stored Qobuz playlist discovery states for frontend hydration."""
|
||||
try:
|
||||
states = []
|
||||
current_time = time.time()
|
||||
|
||||
for playlist_id, state in qobuz_discovery_states.items():
|
||||
state['last_accessed'] = current_time
|
||||
|
||||
state_info = {
|
||||
'playlist_id': playlist_id,
|
||||
'phase': state['phase'],
|
||||
'status': state['status'],
|
||||
'discovery_progress': state['discovery_progress'],
|
||||
'spotify_matches': state['spotify_matches'],
|
||||
'spotify_total': state['spotify_total'],
|
||||
'discovery_results': state['discovery_results'],
|
||||
'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'),
|
||||
'download_process_id': state.get('download_process_id'),
|
||||
'last_accessed': state['last_accessed']
|
||||
}
|
||||
states.append(state_info)
|
||||
|
||||
logger.info(f"Returning {len(states)} stored Qobuz playlist states for hydration")
|
||||
return jsonify({"states": states})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Qobuz playlist states: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _get_source_playlist_states(qobuz_discovery_states, "Qobuz", "Qobuz")
|
||||
|
||||
|
||||
@app.route('/api/qobuz/state/<playlist_id>', methods=['GET'])
|
||||
|
|
@ -22795,33 +22725,7 @@ def update_spotify_public_discovery_match():
|
|||
@app.route('/api/spotify-public/playlists/states', methods=['GET'])
|
||||
def get_spotify_public_playlist_states():
|
||||
"""Get all stored Spotify Public playlist discovery states for frontend hydration"""
|
||||
try:
|
||||
states = []
|
||||
current_time = time.time()
|
||||
|
||||
for url_hash, state in spotify_public_discovery_states.items():
|
||||
state['last_accessed'] = current_time
|
||||
|
||||
state_info = {
|
||||
'playlist_id': url_hash,
|
||||
'phase': state['phase'],
|
||||
'status': state['status'],
|
||||
'discovery_progress': state['discovery_progress'],
|
||||
'spotify_matches': state['spotify_matches'],
|
||||
'spotify_total': state['spotify_total'],
|
||||
'discovery_results': state['discovery_results'],
|
||||
'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'),
|
||||
'download_process_id': state.get('download_process_id'),
|
||||
'last_accessed': state['last_accessed']
|
||||
}
|
||||
states.append(state_info)
|
||||
|
||||
logger.info(f"Returning {len(states)} stored Spotify Public playlist states for hydration")
|
||||
return jsonify({"states": states})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Spotify Public playlist states: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _get_source_playlist_states(spotify_public_discovery_states, "Spotify Public", "Spotify Public")
|
||||
|
||||
@app.route('/api/spotify-public/state/<url_hash>', methods=['GET'])
|
||||
def get_spotify_public_playlist_state(url_hash):
|
||||
|
|
@ -23205,27 +23109,7 @@ def update_itunes_link_discovery_match():
|
|||
|
||||
@app.route('/api/itunes-link/playlists/states', methods=['GET'])
|
||||
def get_itunes_link_playlist_states():
|
||||
try:
|
||||
current_time = time.time()
|
||||
states = []
|
||||
for url_hash, state in itunes_link_discovery_states.items():
|
||||
state['last_accessed'] = current_time
|
||||
states.append({
|
||||
'playlist_id': url_hash,
|
||||
'phase': state['phase'],
|
||||
'status': state['status'],
|
||||
'discovery_progress': state['discovery_progress'],
|
||||
'spotify_matches': state['spotify_matches'],
|
||||
'spotify_total': state['spotify_total'],
|
||||
'discovery_results': state['discovery_results'],
|
||||
'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'),
|
||||
'download_process_id': state.get('download_process_id'),
|
||||
'last_accessed': state['last_accessed']
|
||||
})
|
||||
return jsonify({"states": states})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting iTunes Link playlist states: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _get_source_playlist_states(itunes_link_discovery_states, "iTunes Link")
|
||||
|
||||
|
||||
@app.route('/api/itunes-link/state/<url_hash>', methods=['GET'])
|
||||
|
|
|
|||
Loading…
Reference in a new issue