Discovery lift (2/N): cancel_*_sync + delete_*_playlist -> shared helpers
Second cluster. Two more sets of byte-identical per-source bodies: cancel_<source>_sync (Tidal, Deezer, Qobuz, Spotify-Public, iTunes-Link, YouTube, ListenBrainz) -> core.discovery.endpoints.cancel_sync(states, key, *, label, not_found_message, sync_lock, sync_states, active_sync_workers). Returns (payload, status_code); a thin web_server glue (_cancel_source_sync) wires the sync-infra globals + jsonify. Caller passes the resolved key (ListenBrainz transforms via _lb_state_key) and the exact 404 string (iTunes-Link uses "iTunes Link not found"). delete_<source>_playlist (Tidal, Deezer, Qobuz, Spotify-Public) -> delete_playlist_state(states, key, *, label, not_found_message), wired via _delete_source_playlist. Intentionally left with their own bodies (genuinely divergent, not 1:1): - Beatport cancel (cancels a stored sync_future, no message, warning log). - iTunes-Link / YouTube / ListenBrainz / Beatport deletes (different success messages, info-log wording, playlist-name extraction, /remove route, chart key). Tests: +11 in tests/discovery/test_discovery_endpoints.py covering cancel (404, active-worker cancel + state revert, worker-absent, no-sync-in-progress, label in message, exception->500) and delete (404, future cancel + removal, no/falsy future, exception->500 leaves state). Full discovery suite: 162 passed. web_server.py: -216 lines.
This commit is contained in:
parent
628395eda5
commit
2d76a7c061
3 changed files with 255 additions and 254 deletions
|
|
@ -14,7 +14,8 @@ shape) is intentionally NOT routed through here and stays in its own function.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Dict, List
|
import time
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
|
@ -74,3 +75,85 @@ def convert_results_to_spotify_tracks(
|
||||||
|
|
||||||
logger.info(f"Converted {len(spotify_tracks)} {source_label} matches to Spotify tracks for sync")
|
logger.info(f"Converted {len(spotify_tracks)} {source_label} matches to Spotify tracks for sync")
|
||||||
return spotify_tracks
|
return spotify_tracks
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_sync(
|
||||||
|
states: Dict[str, Any],
|
||||||
|
key: str,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
not_found_message: str,
|
||||||
|
sync_lock: Any,
|
||||||
|
sync_states: Dict[str, Any],
|
||||||
|
active_sync_workers: Dict[str, Any],
|
||||||
|
) -> Tuple[Dict[str, Any], int]:
|
||||||
|
"""Cancel an in-progress sync for one discovery playlist.
|
||||||
|
|
||||||
|
1:1 lift of the byte-identical ``cancel_<source>_sync`` bodies (Tidal,
|
||||||
|
Deezer, Qobuz, Spotify-Public, iTunes-Link, YouTube, ListenBrainz). The
|
||||||
|
caller passes the already-resolved state key (ListenBrainz transforms it
|
||||||
|
via ``_lb_state_key`` first), the source ``label``, the exact 404 message
|
||||||
|
(iTunes-Link uses "iTunes Link not found", not "... playlist not found"),
|
||||||
|
and the shared sync infrastructure (so this stays free of web_server
|
||||||
|
globals / Flask).
|
||||||
|
|
||||||
|
Returns ``(payload_dict, status_code)``; the caller wraps in ``jsonify``.
|
||||||
|
|
||||||
|
Beatport is NOT routed here — it cancels a stored ``sync_future`` and
|
||||||
|
returns a different payload.
|
||||||
|
"""
|
||||||
|
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 sync_playlist_id:
|
||||||
|
with sync_lock:
|
||||||
|
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
||||||
|
if sync_playlist_id in active_sync_workers:
|
||||||
|
del active_sync_workers[sync_playlist_id]
|
||||||
|
|
||||||
|
state['phase'] = 'discovered'
|
||||||
|
state['sync_playlist_id'] = None
|
||||||
|
state['sync_progress'] = {}
|
||||||
|
|
||||||
|
return {"success": True, "message": f"{label} sync cancelled"}, 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error cancelling {label} sync: {e}")
|
||||||
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
|
|
||||||
|
def delete_playlist_state(
|
||||||
|
states: Dict[str, Any],
|
||||||
|
key: str,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
not_found_message: str,
|
||||||
|
) -> Tuple[Dict[str, Any], int]:
|
||||||
|
"""Delete a discovery playlist's state entry, cancelling any active
|
||||||
|
discovery first.
|
||||||
|
|
||||||
|
1:1 lift of the byte-identical ``delete_<source>_playlist`` bodies
|
||||||
|
(Tidal, Deezer, Qobuz, Spotify-Public). Returns ``(payload, status_code)``.
|
||||||
|
|
||||||
|
The iTunes-Link / YouTube / ListenBrainz / Beatport deletes intentionally
|
||||||
|
keep their own bodies — they differ in success message, info-log wording,
|
||||||
|
name extraction, and/or key transform.
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
del states[key]
|
||||||
|
|
||||||
|
logger.info(f"Deleted {label} playlist state: {key}")
|
||||||
|
return {"success": True, "message": "Playlist deleted"}, 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting {label} playlist: {e}")
|
||||||
|
return {"error": str(e)}, 500
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,30 @@ the originals handled is exercised here.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from core.discovery.endpoints import convert_results_to_spotify_tracks
|
import threading
|
||||||
|
|
||||||
|
from core.discovery.endpoints import (
|
||||||
|
convert_results_to_spotify_tracks,
|
||||||
|
cancel_sync,
|
||||||
|
delete_playlist_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeFuture:
|
||||||
|
def __init__(self):
|
||||||
|
self.cancelled = False
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.cancelled = True
|
||||||
|
|
||||||
|
|
||||||
|
def _cancel_infra():
|
||||||
|
"""Fresh sync infra (lock + the two shared dicts) for cancel_sync tests."""
|
||||||
|
return {
|
||||||
|
'sync_lock': threading.Lock(),
|
||||||
|
'sync_states': {},
|
||||||
|
'active_sync_workers': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -125,3 +148,114 @@ def test_spotify_data_takes_precedence_over_auto_fields():
|
||||||
}]
|
}]
|
||||||
out = convert_results_to_spotify_tracks(results, 'Tidal')
|
out = convert_results_to_spotify_tracks(results, 'Tidal')
|
||||||
assert out[0]['id'] == 'D'
|
assert out[0]['id'] == 'D'
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# cancel_sync
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_cancel_sync_not_found_returns_404():
|
||||||
|
body, code = cancel_sync({}, 'missing', label='Tidal',
|
||||||
|
not_found_message='Tidal playlist not found', **_cancel_infra())
|
||||||
|
assert code == 404
|
||||||
|
assert body == {"error": "Tidal playlist not found"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_sync_cancels_active_worker_and_reverts_state():
|
||||||
|
infra = _cancel_infra()
|
||||||
|
infra['sync_states']['sp1'] = {"status": "running"}
|
||||||
|
infra['active_sync_workers']['sp1'] = _FakeFuture()
|
||||||
|
states = {'pl': {'phase': 'syncing', 'sync_playlist_id': 'sp1',
|
||||||
|
'sync_progress': {'done': 1}, 'last_accessed': 0}}
|
||||||
|
|
||||||
|
body, code = cancel_sync(states, 'pl', label='Tidal',
|
||||||
|
not_found_message='nf', **infra)
|
||||||
|
|
||||||
|
assert code == 200
|
||||||
|
assert body == {"success": True, "message": "Tidal sync cancelled"}
|
||||||
|
# sync marked cancelled, worker removed
|
||||||
|
assert infra['sync_states']['sp1'] == {"status": "cancelled"}
|
||||||
|
assert 'sp1' not in infra['active_sync_workers']
|
||||||
|
# state reverted
|
||||||
|
assert states['pl']['phase'] == 'discovered'
|
||||||
|
assert states['pl']['sync_playlist_id'] is None
|
||||||
|
assert states['pl']['sync_progress'] == {}
|
||||||
|
assert states['pl']['last_accessed'] != 0 # touched
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_sync_worker_absent_from_active_map_is_safe():
|
||||||
|
infra = _cancel_infra() # active_sync_workers empty
|
||||||
|
states = {'pl': {'phase': 'syncing', 'sync_playlist_id': 'sp1'}}
|
||||||
|
body, code = cancel_sync(states, 'pl', label='Deezer', not_found_message='nf', **infra)
|
||||||
|
assert code == 200
|
||||||
|
assert infra['sync_states']['sp1'] == {"status": "cancelled"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_sync_no_sync_in_progress_still_reverts():
|
||||||
|
infra = _cancel_infra()
|
||||||
|
states = {'pl': {'phase': 'discovered'}} # no sync_playlist_id
|
||||||
|
body, code = cancel_sync(states, 'pl', label='Qobuz', not_found_message='nf', **infra)
|
||||||
|
assert code == 200
|
||||||
|
assert states['pl']['sync_playlist_id'] is None
|
||||||
|
assert states['pl']['sync_progress'] == {}
|
||||||
|
assert infra['sync_states'] == {} # nothing cancelled
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_sync_label_in_message():
|
||||||
|
infra = _cancel_infra()
|
||||||
|
states = {'pl': {}}
|
||||||
|
body, _ = cancel_sync(states, 'pl', label='iTunes Link', not_found_message='nf', **infra)
|
||||||
|
assert body["message"] == "iTunes Link sync cancelled"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_sync_exception_returns_500():
|
||||||
|
infra = _cancel_infra()
|
||||||
|
states = {'pl': object()} # not subscriptable -> raises inside try
|
||||||
|
body, code = cancel_sync(states, 'pl', label='Tidal', not_found_message='nf', **infra)
|
||||||
|
assert code == 500
|
||||||
|
assert "error" in body
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# delete_playlist_state
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_delete_not_found_returns_404():
|
||||||
|
body, code = delete_playlist_state({}, 'missing', label='Tidal',
|
||||||
|
not_found_message='Tidal playlist not found')
|
||||||
|
assert code == 404
|
||||||
|
assert body == {"error": "Tidal playlist not found"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_cancels_discovery_future_and_removes_state():
|
||||||
|
fut = _FakeFuture()
|
||||||
|
states = {'pl': {'discovery_future': fut}}
|
||||||
|
body, code = delete_playlist_state(states, 'pl', label='Tidal', not_found_message='nf')
|
||||||
|
assert code == 200
|
||||||
|
assert body == {"success": True, "message": "Playlist deleted"}
|
||||||
|
assert fut.cancelled is True
|
||||||
|
assert 'pl' not in states
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_without_discovery_future_still_removes():
|
||||||
|
states = {'pl': {'phase': 'discovered'}} # no discovery_future
|
||||||
|
body, code = delete_playlist_state(states, 'pl', label='Deezer', not_found_message='nf')
|
||||||
|
assert code == 200
|
||||||
|
assert 'pl' not in states
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_falsy_discovery_future_not_cancelled():
|
||||||
|
states = {'pl': {'discovery_future': None}}
|
||||||
|
body, code = delete_playlist_state(states, 'pl', label='Qobuz', not_found_message='nf')
|
||||||
|
assert code == 200
|
||||||
|
assert 'pl' not in states
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_exception_returns_500():
|
||||||
|
fut = object() # no .cancel() -> AttributeError inside try
|
||||||
|
states = {'pl': {'discovery_future': fut}}
|
||||||
|
body, code = delete_playlist_state(states, 'pl', label='Tidal', not_found_message='nf')
|
||||||
|
assert code == 500
|
||||||
|
assert "error" in body
|
||||||
|
# state NOT deleted because the exception fired before del
|
||||||
|
assert 'pl' in states
|
||||||
|
|
|
||||||
288
web_server.py
288
web_server.py
|
|
@ -20802,25 +20802,7 @@ def reset_tidal_playlist(playlist_id):
|
||||||
@app.route('/api/tidal/delete/<playlist_id>', methods=['POST'])
|
@app.route('/api/tidal/delete/<playlist_id>', methods=['POST'])
|
||||||
def delete_tidal_playlist(playlist_id):
|
def delete_tidal_playlist(playlist_id):
|
||||||
"""Delete Tidal playlist state completely"""
|
"""Delete Tidal playlist state completely"""
|
||||||
try:
|
return _delete_source_playlist(tidal_discovery_states, playlist_id, "Tidal", "Tidal playlist not found")
|
||||||
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()
|
|
||||||
|
|
||||||
# Remove from state dictionary
|
|
||||||
del tidal_discovery_states[playlist_id]
|
|
||||||
|
|
||||||
logger.info(f"Deleted Tidal playlist state: {playlist_id}")
|
|
||||||
return jsonify({"success": True, "message": "Playlist deleted"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting Tidal playlist: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/tidal/update_phase/<playlist_id>', methods=['POST'])
|
@app.route('/api/tidal/update_phase/<playlist_id>', methods=['POST'])
|
||||||
def update_tidal_playlist_phase(playlist_id):
|
def update_tidal_playlist_phase(playlist_id):
|
||||||
|
|
@ -21074,7 +21056,31 @@ from core.discovery.scoring import (
|
||||||
# Tidal discovery worker logic lives in core/discovery/tidal.py.
|
# Tidal discovery worker logic lives in core/discovery/tidal.py.
|
||||||
from core.discovery import tidal as _discovery_tidal
|
from core.discovery import tidal as _discovery_tidal
|
||||||
# Source-agnostic discovery route helpers (lifted from the per-source copies).
|
# Source-agnostic discovery route helpers (lifted from the per-source copies).
|
||||||
from core.discovery.endpoints import convert_results_to_spotify_tracks
|
from core.discovery.endpoints import (
|
||||||
|
convert_results_to_spotify_tracks,
|
||||||
|
cancel_sync as _cancel_sync_core,
|
||||||
|
delete_playlist_state as _delete_playlist_state_core,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cancel_source_sync(states, key, label, not_found_message):
|
||||||
|
"""Thin glue: wire web_server's sync infra into the lifted cancel_sync
|
||||||
|
helper and jsonify the result. Used by the per-source cancel routes."""
|
||||||
|
body, code = _cancel_sync_core(
|
||||||
|
states, key, label=label, not_found_message=not_found_message,
|
||||||
|
sync_lock=sync_lock, sync_states=sync_states,
|
||||||
|
active_sync_workers=active_sync_workers,
|
||||||
|
)
|
||||||
|
return jsonify(body), code
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_source_playlist(states, key, label, not_found_message):
|
||||||
|
"""Thin glue for the per-source delete routes that share the identical
|
||||||
|
delete body (Tidal/Deezer/Qobuz/Spotify-Public)."""
|
||||||
|
body, code = _delete_playlist_state_core(
|
||||||
|
states, key, label=label, not_found_message=not_found_message,
|
||||||
|
)
|
||||||
|
return jsonify(body), code
|
||||||
|
|
||||||
|
|
||||||
def _build_tidal_discovery_deps():
|
def _build_tidal_discovery_deps():
|
||||||
|
|
@ -21214,33 +21220,7 @@ def get_tidal_sync_status(playlist_id):
|
||||||
@app.route('/api/tidal/sync/cancel/<playlist_id>', methods=['POST'])
|
@app.route('/api/tidal/sync/cancel/<playlist_id>', methods=['POST'])
|
||||||
def cancel_tidal_sync(playlist_id):
|
def cancel_tidal_sync(playlist_id):
|
||||||
"""Cancel sync for a Tidal playlist"""
|
"""Cancel sync for a Tidal playlist"""
|
||||||
try:
|
return _cancel_source_sync(tidal_discovery_states, playlist_id, "Tidal", "Tidal playlist not found")
|
||||||
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 sync_playlist_id:
|
|
||||||
# Cancel the sync using existing sync infrastructure
|
|
||||||
with sync_lock:
|
|
||||||
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
|
||||||
|
|
||||||
# Clean up sync worker
|
|
||||||
if sync_playlist_id in active_sync_workers:
|
|
||||||
del active_sync_workers[sync_playlist_id]
|
|
||||||
|
|
||||||
# Revert Tidal state
|
|
||||||
state['phase'] = 'discovered'
|
|
||||||
state['sync_playlist_id'] = None
|
|
||||||
state['sync_progress'] = {}
|
|
||||||
|
|
||||||
return jsonify({"success": True, "message": "Tidal sync cancelled"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cancelling Tidal sync: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
@ -21699,25 +21679,7 @@ def reset_deezer_playlist(playlist_id):
|
||||||
@app.route('/api/deezer/delete/<playlist_id>', methods=['POST'])
|
@app.route('/api/deezer/delete/<playlist_id>', methods=['POST'])
|
||||||
def delete_deezer_playlist(playlist_id):
|
def delete_deezer_playlist(playlist_id):
|
||||||
"""Delete Deezer playlist state completely"""
|
"""Delete Deezer playlist state completely"""
|
||||||
try:
|
return _delete_source_playlist(deezer_discovery_states, playlist_id, "Deezer", "Deezer playlist not found")
|
||||||
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()
|
|
||||||
|
|
||||||
# Remove from state dictionary
|
|
||||||
del deezer_discovery_states[playlist_id]
|
|
||||||
|
|
||||||
logger.info(f"Deleted Deezer playlist state: {playlist_id}")
|
|
||||||
return jsonify({"success": True, "message": "Playlist deleted"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting Deezer playlist: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/deezer/update_phase/<playlist_id>', methods=['POST'])
|
@app.route('/api/deezer/update_phase/<playlist_id>', methods=['POST'])
|
||||||
def update_deezer_playlist_phase(playlist_id):
|
def update_deezer_playlist_phase(playlist_id):
|
||||||
|
|
@ -21893,33 +21855,7 @@ def get_deezer_sync_status(playlist_id):
|
||||||
@app.route('/api/deezer/sync/cancel/<playlist_id>', methods=['POST'])
|
@app.route('/api/deezer/sync/cancel/<playlist_id>', methods=['POST'])
|
||||||
def cancel_deezer_sync(playlist_id):
|
def cancel_deezer_sync(playlist_id):
|
||||||
"""Cancel sync for a Deezer playlist"""
|
"""Cancel sync for a Deezer playlist"""
|
||||||
try:
|
return _cancel_source_sync(deezer_discovery_states, playlist_id, "Deezer", "Deezer playlist not found")
|
||||||
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 sync_playlist_id:
|
|
||||||
# Cancel the sync using existing sync infrastructure
|
|
||||||
with sync_lock:
|
|
||||||
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
|
||||||
|
|
||||||
# Clean up sync worker
|
|
||||||
if sync_playlist_id in active_sync_workers:
|
|
||||||
del active_sync_workers[sync_playlist_id]
|
|
||||||
|
|
||||||
# Revert Deezer state
|
|
||||||
state['phase'] = 'discovered'
|
|
||||||
state['sync_playlist_id'] = None
|
|
||||||
state['sync_progress'] = {}
|
|
||||||
|
|
||||||
return jsonify({"success": True, "message": "Deezer sync cancelled"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cancelling Deezer sync: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
@ -22335,23 +22271,7 @@ def reset_qobuz_playlist(playlist_id):
|
||||||
@app.route('/api/qobuz/delete/<playlist_id>', methods=['POST'])
|
@app.route('/api/qobuz/delete/<playlist_id>', methods=['POST'])
|
||||||
def delete_qobuz_playlist(playlist_id):
|
def delete_qobuz_playlist(playlist_id):
|
||||||
"""Delete Qobuz playlist state completely."""
|
"""Delete Qobuz playlist state completely."""
|
||||||
try:
|
return _delete_source_playlist(qobuz_discovery_states, playlist_id, "Qobuz", "Qobuz playlist not found")
|
||||||
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()
|
|
||||||
|
|
||||||
del qobuz_discovery_states[playlist_id]
|
|
||||||
|
|
||||||
logger.info(f"Deleted Qobuz playlist state: {playlist_id}")
|
|
||||||
return jsonify({"success": True, "message": "Playlist deleted"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting Qobuz playlist: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/qobuz/update_phase/<playlist_id>', methods=['POST'])
|
@app.route('/api/qobuz/update_phase/<playlist_id>', methods=['POST'])
|
||||||
|
|
@ -22517,29 +22437,7 @@ def get_qobuz_sync_status(playlist_id):
|
||||||
@app.route('/api/qobuz/sync/cancel/<playlist_id>', methods=['POST'])
|
@app.route('/api/qobuz/sync/cancel/<playlist_id>', methods=['POST'])
|
||||||
def cancel_qobuz_sync(playlist_id):
|
def cancel_qobuz_sync(playlist_id):
|
||||||
"""Cancel sync for a Qobuz playlist."""
|
"""Cancel sync for a Qobuz playlist."""
|
||||||
try:
|
return _cancel_source_sync(qobuz_discovery_states, playlist_id, "Qobuz", "Qobuz playlist not found")
|
||||||
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 sync_playlist_id:
|
|
||||||
with sync_lock:
|
|
||||||
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
|
||||||
if sync_playlist_id in active_sync_workers:
|
|
||||||
del active_sync_workers[sync_playlist_id]
|
|
||||||
|
|
||||||
state['phase'] = 'discovered'
|
|
||||||
state['sync_playlist_id'] = None
|
|
||||||
state['sync_progress'] = {}
|
|
||||||
|
|
||||||
return jsonify({"success": True, "message": "Qobuz sync cancelled"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cancelling Qobuz sync: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
@ -23239,25 +23137,7 @@ def reset_spotify_public_playlist(url_hash):
|
||||||
@app.route('/api/spotify-public/delete/<url_hash>', methods=['POST'])
|
@app.route('/api/spotify-public/delete/<url_hash>', methods=['POST'])
|
||||||
def delete_spotify_public_playlist(url_hash):
|
def delete_spotify_public_playlist(url_hash):
|
||||||
"""Delete Spotify Public playlist state completely"""
|
"""Delete Spotify Public playlist state completely"""
|
||||||
try:
|
return _delete_source_playlist(spotify_public_discovery_states, url_hash, "Spotify Public", "Spotify Public playlist not found")
|
||||||
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()
|
|
||||||
|
|
||||||
# Remove from state dictionary
|
|
||||||
del spotify_public_discovery_states[url_hash]
|
|
||||||
|
|
||||||
logger.info(f"Deleted Spotify Public playlist state: {url_hash}")
|
|
||||||
return jsonify({"success": True, "message": "Playlist deleted"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error deleting Spotify Public playlist: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/spotify-public/update_phase/<url_hash>', methods=['POST'])
|
@app.route('/api/spotify-public/update_phase/<url_hash>', methods=['POST'])
|
||||||
def update_spotify_public_playlist_phase(url_hash):
|
def update_spotify_public_playlist_phase(url_hash):
|
||||||
|
|
@ -23434,33 +23314,7 @@ def get_spotify_public_sync_status(url_hash):
|
||||||
@app.route('/api/spotify-public/sync/cancel/<url_hash>', methods=['POST'])
|
@app.route('/api/spotify-public/sync/cancel/<url_hash>', methods=['POST'])
|
||||||
def cancel_spotify_public_sync(url_hash):
|
def cancel_spotify_public_sync(url_hash):
|
||||||
"""Cancel sync for a Spotify Public playlist"""
|
"""Cancel sync for a Spotify Public playlist"""
|
||||||
try:
|
return _cancel_source_sync(spotify_public_discovery_states, url_hash, "Spotify Public", "Spotify Public playlist not found")
|
||||||
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 sync_playlist_id:
|
|
||||||
# Cancel the sync using existing sync infrastructure
|
|
||||||
with sync_lock:
|
|
||||||
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
|
||||||
|
|
||||||
# Clean up sync worker
|
|
||||||
if sync_playlist_id in active_sync_workers:
|
|
||||||
del active_sync_workers[sync_playlist_id]
|
|
||||||
|
|
||||||
# Revert Spotify Public state
|
|
||||||
state['phase'] = 'discovered'
|
|
||||||
state['sync_playlist_id'] = None
|
|
||||||
state['sync_progress'] = {}
|
|
||||||
|
|
||||||
return jsonify({"success": True, "message": "Spotify Public sync cancelled"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cancelling Spotify Public sync: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
@ -23890,24 +23744,7 @@ def get_itunes_link_sync_status(url_hash):
|
||||||
|
|
||||||
@app.route('/api/itunes-link/sync/cancel/<url_hash>', methods=['POST'])
|
@app.route('/api/itunes-link/sync/cancel/<url_hash>', methods=['POST'])
|
||||||
def cancel_itunes_link_sync(url_hash):
|
def cancel_itunes_link_sync(url_hash):
|
||||||
try:
|
return _cancel_source_sync(itunes_link_discovery_states, url_hash, "iTunes Link", "iTunes Link not found")
|
||||||
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 sync_playlist_id:
|
|
||||||
with sync_lock:
|
|
||||||
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
|
||||||
if sync_playlist_id in active_sync_workers:
|
|
||||||
del active_sync_workers[sync_playlist_id]
|
|
||||||
state['phase'] = 'discovered'
|
|
||||||
state['sync_playlist_id'] = None
|
|
||||||
state['sync_progress'] = {}
|
|
||||||
return jsonify({"success": True, "message": "iTunes Link sync cancelled"})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cancelling iTunes Link sync: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
@ -24555,33 +24392,7 @@ def get_youtube_sync_status(url_hash):
|
||||||
@app.route('/api/youtube/sync/cancel/<url_hash>', methods=['POST'])
|
@app.route('/api/youtube/sync/cancel/<url_hash>', methods=['POST'])
|
||||||
def cancel_youtube_sync(url_hash):
|
def cancel_youtube_sync(url_hash):
|
||||||
"""Cancel sync for a YouTube playlist"""
|
"""Cancel sync for a YouTube playlist"""
|
||||||
try:
|
return _cancel_source_sync(youtube_playlist_states, url_hash, "YouTube", "YouTube playlist not found")
|
||||||
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 sync_playlist_id:
|
|
||||||
# Cancel the sync using existing sync infrastructure
|
|
||||||
with sync_lock:
|
|
||||||
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
|
||||||
|
|
||||||
# Clean up sync worker
|
|
||||||
if sync_playlist_id in active_sync_workers:
|
|
||||||
del active_sync_workers[sync_playlist_id]
|
|
||||||
|
|
||||||
# Revert YouTube state
|
|
||||||
state['phase'] = 'discovered'
|
|
||||||
state['sync_playlist_id'] = None
|
|
||||||
state['sync_progress'] = {}
|
|
||||||
|
|
||||||
return jsonify({"success": True, "message": "YouTube sync cancelled"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cancelling YouTube sync: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
# New YouTube Playlist Management Endpoints (for persistent state)
|
# New YouTube Playlist Management Endpoints (for persistent state)
|
||||||
|
|
||||||
|
|
@ -30690,34 +30501,7 @@ def get_listenbrainz_sync_status(playlist_mbid):
|
||||||
@app.route('/api/listenbrainz/sync/cancel/<playlist_mbid>', methods=['POST'])
|
@app.route('/api/listenbrainz/sync/cancel/<playlist_mbid>', methods=['POST'])
|
||||||
def cancel_listenbrainz_sync(playlist_mbid):
|
def cancel_listenbrainz_sync(playlist_mbid):
|
||||||
"""Cancel sync for a ListenBrainz playlist"""
|
"""Cancel sync for a ListenBrainz playlist"""
|
||||||
try:
|
return _cancel_source_sync(listenbrainz_playlist_states, _lb_state_key(playlist_mbid), "ListenBrainz", "ListenBrainz playlist not found")
|
||||||
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 sync_playlist_id:
|
|
||||||
# Cancel the sync using existing sync infrastructure
|
|
||||||
with sync_lock:
|
|
||||||
sync_states[sync_playlist_id] = {"status": "cancelled"}
|
|
||||||
|
|
||||||
# Clean up sync worker
|
|
||||||
if sync_playlist_id in active_sync_workers:
|
|
||||||
del active_sync_workers[sync_playlist_id]
|
|
||||||
|
|
||||||
# Revert ListenBrainz state
|
|
||||||
state['phase'] = 'discovered'
|
|
||||||
state['sync_playlist_id'] = None
|
|
||||||
state['sync_progress'] = {}
|
|
||||||
|
|
||||||
return jsonify({"success": True, "message": "ListenBrainz sync cancelled"})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error cancelling ListenBrainz sync: {e}")
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/metadata/start', methods=['POST'])
|
@app.route('/api/metadata/start', methods=['POST'])
|
||||||
def start_metadata_update():
|
def start_metadata_update():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue