Discovery lift (5/N): reset_*_playlist -> shared helper
Fifth cluster: reset_<source>_playlist for the four sources with byte-
identical bodies (Tidal, Deezer, Qobuz, Spotify-Public) ->
core.discovery.endpoints.reset_playlist(states, key, *, label,
not_found_message), wired via _reset_source_playlist. Resets phase/status to
'fresh', clears discovery/sync fields, cancels any discovery_future, and
preserves the original playlist payload.
Left with their own bodies (genuinely divergent):
- YouTube: status -> 'parsed' (not 'fresh'), no download_process_id, logs the
playlist name, "reset to fresh state".
- ListenBrainz: status -> 'cached', logs playlist title, returns
{"success": True, "phase": "fresh"} (different payload), _lb_state_key.
- iTunes-Link: state.update(...), no info log, "iTunes Link reset to fresh
phase".
Tests: +4 (404, full clear + playlist preserved + future cancelled, no-future
path, exception -> 500). Full discovery suite: 179 passed.
web_server.py: -100 lines.
This commit is contained in:
parent
8a9ed677ab
commit
44b032b6c0
3 changed files with 120 additions and 114 deletions
|
|
@ -288,3 +288,49 @@ def get_discovery_status(
|
|||
except Exception as e:
|
||||
logger.error(f"Error getting {error_label} discovery status: {e}")
|
||||
return {"error": str(e)}, 500
|
||||
|
||||
|
||||
def reset_playlist(
|
||||
states: Dict[str, Any],
|
||||
key: str,
|
||||
*,
|
||||
label: str,
|
||||
not_found_message: str,
|
||||
) -> Tuple[Dict[str, Any], int]:
|
||||
"""Reset a discovery playlist back to the 'fresh' phase, clearing all
|
||||
discovery/sync data while preserving the original playlist payload.
|
||||
|
||||
1:1 lift of the byte-identical ``reset_<source>_playlist`` bodies
|
||||
(Tidal, Deezer, Qobuz, Spotify-Public). Returns ``(payload, status_code)``.
|
||||
|
||||
NOT folded in (genuinely divergent): YouTube (status -> 'parsed', no
|
||||
download_process_id, logs the playlist name, "reset to fresh state"),
|
||||
ListenBrainz (status -> 'cached', logs playlist title, returns
|
||||
{"phase": "fresh"}), iTunes-Link (uses state.update, no info log, distinct
|
||||
message). Those keep their own bodies.
|
||||
"""
|
||||
try:
|
||||
if key not in states:
|
||||
return {"error": not_found_message}, 404
|
||||
|
||||
state = states[key]
|
||||
if 'discovery_future' in state and state['discovery_future']:
|
||||
state['discovery_future'].cancel()
|
||||
|
||||
state['phase'] = 'fresh'
|
||||
state['status'] = 'fresh'
|
||||
state['discovery_results'] = []
|
||||
state['discovery_progress'] = 0
|
||||
state['spotify_matches'] = 0
|
||||
state['sync_playlist_id'] = None
|
||||
state['converted_spotify_playlist_id'] = None
|
||||
state['download_process_id'] = None
|
||||
state['sync_progress'] = {}
|
||||
state['discovery_future'] = None
|
||||
state['last_accessed'] = time.time()
|
||||
|
||||
logger.info(f"Reset {label} playlist to fresh: {key}")
|
||||
return {"success": True, "message": "Playlist reset to fresh phase"}, 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting {label} playlist: {e}")
|
||||
return {"error": str(e)}, 500
|
||||
|
|
|
|||
|
|
@ -445,3 +445,63 @@ def test_discovery_status_missing_field_raises_500():
|
|||
body, code = get_discovery_status(states, 'pl',
|
||||
not_found_message='nf', error_label='Beatport')
|
||||
assert code == 500 and "error" in body
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reset_playlist
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_reset_not_found():
|
||||
from core.discovery.endpoints import reset_playlist
|
||||
body, code = reset_playlist({}, 'missing', label='Tidal',
|
||||
not_found_message='Tidal playlist not found')
|
||||
assert code == 404 and body == {"error": "Tidal playlist not found"}
|
||||
|
||||
|
||||
def test_reset_clears_state_preserving_playlist_and_cancels_future():
|
||||
from core.discovery.endpoints import reset_playlist
|
||||
fut = _FakeFuture()
|
||||
state = {
|
||||
'playlist': {'name': 'keep me'},
|
||||
'phase': 'discovered', 'status': 'done',
|
||||
'discovery_results': [{'x': 1}], 'discovery_progress': 100,
|
||||
'spotify_matches': 5, 'sync_playlist_id': 'sp', 'last_accessed': 0,
|
||||
'converted_spotify_playlist_id': 'cv', 'download_process_id': 'dp',
|
||||
'sync_progress': {'n': 1}, 'discovery_future': fut,
|
||||
}
|
||||
states = {'pl': state}
|
||||
body, code = reset_playlist(states, 'pl', label='Tidal', not_found_message='nf')
|
||||
|
||||
assert code == 200
|
||||
assert body == {"success": True, "message": "Playlist reset to fresh phase"}
|
||||
assert fut.cancelled is True
|
||||
# cleared
|
||||
assert state['phase'] == 'fresh'
|
||||
assert state['status'] == 'fresh'
|
||||
assert state['discovery_results'] == []
|
||||
assert state['discovery_progress'] == 0
|
||||
assert state['spotify_matches'] == 0
|
||||
assert state['sync_playlist_id'] is None
|
||||
assert state['converted_spotify_playlist_id'] is None
|
||||
assert state['download_process_id'] is None
|
||||
assert state['sync_progress'] == {}
|
||||
assert state['discovery_future'] is None
|
||||
assert state['last_accessed'] != 0
|
||||
# original playlist payload preserved
|
||||
assert state['playlist'] == {'name': 'keep me'}
|
||||
|
||||
|
||||
def test_reset_without_discovery_future():
|
||||
from core.discovery.endpoints import reset_playlist
|
||||
state = {'phase': 'discovered'} # no discovery_future key
|
||||
body, code = reset_playlist({'pl': state}, 'pl', label='Deezer', not_found_message='nf')
|
||||
assert code == 200
|
||||
assert state['phase'] == 'fresh'
|
||||
|
||||
|
||||
def test_reset_exception_returns_500():
|
||||
from core.discovery.endpoints import reset_playlist
|
||||
fut = object() # .cancel missing -> AttributeError in try
|
||||
states = {'pl': {'discovery_future': fut}}
|
||||
body, code = reset_playlist(states, 'pl', label='Qobuz', not_found_message='nf')
|
||||
assert code == 500 and "error" in body
|
||||
|
|
|
|||
128
web_server.py
128
web_server.py
|
|
@ -20748,35 +20748,7 @@ def get_tidal_playlist_state(playlist_id):
|
|||
@app.route('/api/tidal/reset/<playlist_id>', methods=['POST'])
|
||||
def reset_tidal_playlist(playlist_id):
|
||||
"""Reset Tidal playlist to fresh phase (clear discovery/sync data)"""
|
||||
try:
|
||||
if playlist_id not in tidal_discovery_states:
|
||||
return jsonify({"error": "Tidal playlist not found"}), 404
|
||||
|
||||
state = tidal_discovery_states[playlist_id]
|
||||
|
||||
# Stop any active discovery
|
||||
if 'discovery_future' in state and state['discovery_future']:
|
||||
state['discovery_future'].cancel()
|
||||
|
||||
# Reset state to fresh (preserve original playlist data)
|
||||
state['phase'] = 'fresh'
|
||||
state['status'] = 'fresh'
|
||||
state['discovery_results'] = []
|
||||
state['discovery_progress'] = 0
|
||||
state['spotify_matches'] = 0
|
||||
state['sync_playlist_id'] = None
|
||||
state['converted_spotify_playlist_id'] = None
|
||||
state['download_process_id'] = None
|
||||
state['sync_progress'] = {}
|
||||
state['discovery_future'] = None
|
||||
state['last_accessed'] = time.time()
|
||||
|
||||
logger.info(f"Reset Tidal playlist to fresh: {playlist_id}")
|
||||
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting Tidal playlist: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _reset_source_playlist(tidal_discovery_states, playlist_id, "Tidal", "Tidal playlist not found")
|
||||
|
||||
@app.route('/api/tidal/delete/<playlist_id>', methods=['POST'])
|
||||
def delete_tidal_playlist(playlist_id):
|
||||
|
|
@ -21041,6 +21013,7 @@ from core.discovery.endpoints import (
|
|||
delete_playlist_state as _delete_playlist_state_core,
|
||||
get_sync_status as _get_sync_status_core,
|
||||
get_discovery_status as _get_discovery_status_core,
|
||||
reset_playlist as _reset_playlist_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,
|
||||
|
|
@ -21087,6 +21060,15 @@ def _get_source_discovery_status(states, key, not_found_message, error_label):
|
|||
return jsonify(body), code
|
||||
|
||||
|
||||
def _reset_source_playlist(states, key, label, not_found_message):
|
||||
"""Thin glue for the per-source reset routes that share the identical
|
||||
reset body (Tidal/Deezer/Qobuz/Spotify-Public)."""
|
||||
body, code = _reset_playlist_core(
|
||||
states, key, label=label, not_found_message=not_found_message,
|
||||
)
|
||||
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(
|
||||
|
|
@ -21588,35 +21570,7 @@ def get_deezer_playlist_state(playlist_id):
|
|||
@app.route('/api/deezer/reset/<playlist_id>', methods=['POST'])
|
||||
def reset_deezer_playlist(playlist_id):
|
||||
"""Reset Deezer playlist to fresh phase (clear discovery/sync data)"""
|
||||
try:
|
||||
if playlist_id not in deezer_discovery_states:
|
||||
return jsonify({"error": "Deezer playlist not found"}), 404
|
||||
|
||||
state = deezer_discovery_states[playlist_id]
|
||||
|
||||
# Stop any active discovery
|
||||
if 'discovery_future' in state and state['discovery_future']:
|
||||
state['discovery_future'].cancel()
|
||||
|
||||
# Reset state to fresh (preserve original playlist data)
|
||||
state['phase'] = 'fresh'
|
||||
state['status'] = 'fresh'
|
||||
state['discovery_results'] = []
|
||||
state['discovery_progress'] = 0
|
||||
state['spotify_matches'] = 0
|
||||
state['sync_playlist_id'] = None
|
||||
state['converted_spotify_playlist_id'] = None
|
||||
state['download_process_id'] = None
|
||||
state['sync_progress'] = {}
|
||||
state['discovery_future'] = None
|
||||
state['last_accessed'] = time.time()
|
||||
|
||||
logger.info(f"Reset Deezer playlist to fresh: {playlist_id}")
|
||||
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting Deezer playlist: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _reset_source_playlist(deezer_discovery_states, playlist_id, "Deezer", "Deezer playlist not found")
|
||||
|
||||
@app.route('/api/deezer/delete/<playlist_id>', methods=['POST'])
|
||||
def delete_deezer_playlist(playlist_id):
|
||||
|
|
@ -22122,33 +22076,7 @@ def get_qobuz_playlist_state(playlist_id):
|
|||
@app.route('/api/qobuz/reset/<playlist_id>', methods=['POST'])
|
||||
def reset_qobuz_playlist(playlist_id):
|
||||
"""Reset Qobuz playlist to fresh phase (clear discovery/sync data)."""
|
||||
try:
|
||||
if playlist_id not in qobuz_discovery_states:
|
||||
return jsonify({"error": "Qobuz playlist not found"}), 404
|
||||
|
||||
state = qobuz_discovery_states[playlist_id]
|
||||
|
||||
if 'discovery_future' in state and state['discovery_future']:
|
||||
state['discovery_future'].cancel()
|
||||
|
||||
state['phase'] = 'fresh'
|
||||
state['status'] = 'fresh'
|
||||
state['discovery_results'] = []
|
||||
state['discovery_progress'] = 0
|
||||
state['spotify_matches'] = 0
|
||||
state['sync_playlist_id'] = None
|
||||
state['converted_spotify_playlist_id'] = None
|
||||
state['download_process_id'] = None
|
||||
state['sync_progress'] = {}
|
||||
state['discovery_future'] = None
|
||||
state['last_accessed'] = time.time()
|
||||
|
||||
logger.info(f"Reset Qobuz playlist to fresh: {playlist_id}")
|
||||
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting Qobuz playlist: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _reset_source_playlist(qobuz_discovery_states, playlist_id, "Qobuz", "Qobuz playlist not found")
|
||||
|
||||
|
||||
@app.route('/api/qobuz/delete/<playlist_id>', methods=['POST'])
|
||||
|
|
@ -22930,35 +22858,7 @@ def get_spotify_public_playlist_state(url_hash):
|
|||
@app.route('/api/spotify-public/reset/<url_hash>', methods=['POST'])
|
||||
def reset_spotify_public_playlist(url_hash):
|
||||
"""Reset Spotify Public playlist to fresh phase (clear discovery/sync data)"""
|
||||
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]
|
||||
|
||||
# Stop any active discovery
|
||||
if 'discovery_future' in state and state['discovery_future']:
|
||||
state['discovery_future'].cancel()
|
||||
|
||||
# Reset state to fresh (preserve original playlist data)
|
||||
state['phase'] = 'fresh'
|
||||
state['status'] = 'fresh'
|
||||
state['discovery_results'] = []
|
||||
state['discovery_progress'] = 0
|
||||
state['spotify_matches'] = 0
|
||||
state['sync_playlist_id'] = None
|
||||
state['converted_spotify_playlist_id'] = None
|
||||
state['download_process_id'] = None
|
||||
state['sync_progress'] = {}
|
||||
state['discovery_future'] = None
|
||||
state['last_accessed'] = time.time()
|
||||
|
||||
logger.info(f"Reset Spotify Public playlist to fresh: {url_hash}")
|
||||
return jsonify({"success": True, "message": "Playlist reset to fresh phase"})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting Spotify Public playlist: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return _reset_source_playlist(spotify_public_discovery_states, url_hash, "Spotify Public", "Spotify Public playlist not found")
|
||||
|
||||
@app.route('/api/spotify-public/delete/<url_hash>', methods=['POST'])
|
||||
def delete_spotify_public_playlist(url_hash):
|
||||
|
|
|
|||
Loading…
Reference in a new issue