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:
|
except Exception as e:
|
||||||
logger.error(f"Error resetting {label} playlist: {e}")
|
logger.error(f"Error resetting {label} playlist: {e}")
|
||||||
return {"error": str(e)}, 500
|
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}}
|
states = {'pl': {'discovery_future': fut}}
|
||||||
body, code = reset_playlist(states, 'pl', label='Qobuz', not_found_message='nf')
|
body, code = reset_playlist(states, 'pl', label='Qobuz', not_found_message='nf')
|
||||||
assert code == 500 and "error" in body
|
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'])
|
@app.route('/api/tidal/playlists/states', methods=['GET'])
|
||||||
def get_tidal_playlist_states():
|
def get_tidal_playlist_states():
|
||||||
"""Get all stored Tidal playlist discovery states for frontend hydration (similar to YouTube playlists)"""
|
"""Get all stored Tidal playlist discovery states for frontend hydration (similar to YouTube playlists)"""
|
||||||
try:
|
return _get_source_playlist_states(tidal_discovery_states, "Tidal", "Tidal")
|
||||||
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
|
|
||||||
|
|
||||||
@app.route('/api/tidal/state/<playlist_id>', methods=['GET'])
|
@app.route('/api/tidal/state/<playlist_id>', methods=['GET'])
|
||||||
def get_tidal_playlist_state(playlist_id):
|
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_sync_status as _get_sync_status_core,
|
||||||
get_discovery_status as _get_discovery_status_core,
|
get_discovery_status as _get_discovery_status_core,
|
||||||
reset_playlist as _reset_playlist_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_attr_or_unknown as _pl_name_attr_or_unknown,
|
||||||
playlist_name_strict as _pl_name_strict,
|
playlist_name_strict as _pl_name_strict,
|
||||||
playlist_name_safe as _pl_name_safe,
|
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
|
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():
|
def _build_tidal_discovery_deps():
|
||||||
"""Build the TidalDiscoveryDeps bundle from web_server.py globals on each call."""
|
"""Build the TidalDiscoveryDeps bundle from web_server.py globals on each call."""
|
||||||
return _discovery_tidal.TidalDiscoveryDeps(
|
return _discovery_tidal.TidalDiscoveryDeps(
|
||||||
|
|
@ -21506,33 +21488,7 @@ def update_deezer_discovery_match():
|
||||||
@app.route('/api/deezer/playlists/states', methods=['GET'])
|
@app.route('/api/deezer/playlists/states', methods=['GET'])
|
||||||
def get_deezer_playlist_states():
|
def get_deezer_playlist_states():
|
||||||
"""Get all stored Deezer playlist discovery states for frontend hydration"""
|
"""Get all stored Deezer playlist discovery states for frontend hydration"""
|
||||||
try:
|
return _get_source_playlist_states(deezer_discovery_states, "Deezer", "Deezer")
|
||||||
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
|
|
||||||
|
|
||||||
@app.route('/api/deezer/state/<playlist_id>', methods=['GET'])
|
@app.route('/api/deezer/state/<playlist_id>', methods=['GET'])
|
||||||
def get_deezer_playlist_state(playlist_id):
|
def get_deezer_playlist_state(playlist_id):
|
||||||
|
|
@ -22011,33 +21967,7 @@ def update_qobuz_discovery_match():
|
||||||
@app.route('/api/qobuz/playlists/states', methods=['GET'])
|
@app.route('/api/qobuz/playlists/states', methods=['GET'])
|
||||||
def get_qobuz_playlist_states():
|
def get_qobuz_playlist_states():
|
||||||
"""Get all stored Qobuz playlist discovery states for frontend hydration."""
|
"""Get all stored Qobuz playlist discovery states for frontend hydration."""
|
||||||
try:
|
return _get_source_playlist_states(qobuz_discovery_states, "Qobuz", "Qobuz")
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/qobuz/state/<playlist_id>', methods=['GET'])
|
@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'])
|
@app.route('/api/spotify-public/playlists/states', methods=['GET'])
|
||||||
def get_spotify_public_playlist_states():
|
def get_spotify_public_playlist_states():
|
||||||
"""Get all stored Spotify Public playlist discovery states for frontend hydration"""
|
"""Get all stored Spotify Public playlist discovery states for frontend hydration"""
|
||||||
try:
|
return _get_source_playlist_states(spotify_public_discovery_states, "Spotify Public", "Spotify Public")
|
||||||
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
|
|
||||||
|
|
||||||
@app.route('/api/spotify-public/state/<url_hash>', methods=['GET'])
|
@app.route('/api/spotify-public/state/<url_hash>', methods=['GET'])
|
||||||
def get_spotify_public_playlist_state(url_hash):
|
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'])
|
@app.route('/api/itunes-link/playlists/states', methods=['GET'])
|
||||||
def get_itunes_link_playlist_states():
|
def get_itunes_link_playlist_states():
|
||||||
try:
|
return _get_source_playlist_states(itunes_link_discovery_states, "iTunes Link")
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/itunes-link/state/<url_hash>', methods=['GET'])
|
@app.route('/api/itunes-link/state/<url_hash>', methods=['GET'])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue