Cin-6: Rename soulseek_client global → download_orchestrator
The global handle in web_server.py was named soulseek_client for historical reasons but the type has long been DownloadOrchestrator, not SoulseekClient. Renamed the global plus every parameter/attribute that carried the legacy name. - web_server.py: global var renamed; all 99 references updated. - api/, core/downloads/*, core/search/*, core/streaming/*, services/sync_service.py: parameter names, dataclass fields, and init() arg names renamed. - Test fixtures (CandidatesDeps, MasterDeps, SearchDeps, etc.) and the _build_deps helpers updated accordingly. The core.soulseek_client module path and SoulseekClient class name (the actual soulseek-only client) are unchanged — only the orchestrator handle renamed. Module imports of TrackResult/AlbumResult/DownloadStatus from core.soulseek_client preserved.
This commit is contained in:
parent
7519c3d50c
commit
61ba3a15de
28 changed files with 212 additions and 212 deletions
|
|
@ -121,7 +121,7 @@ def register_routes(bp):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from utils.async_helpers import run_async
|
from utils.async_helpers import run_async
|
||||||
soulseek = current_app.soulsync.get("soulseek_client")
|
soulseek = current_app.soulsync.get("download_orchestrator")
|
||||||
if not soulseek:
|
if not soulseek:
|
||||||
return api_error("NOT_AVAILABLE", "Soulseek client not configured.", 503)
|
return api_error("NOT_AVAILABLE", "Soulseek client not configured.", 503)
|
||||||
|
|
||||||
|
|
@ -138,7 +138,7 @@ def register_routes(bp):
|
||||||
"""Cancel all active downloads and clear completed ones."""
|
"""Cancel all active downloads and clear completed ones."""
|
||||||
try:
|
try:
|
||||||
from utils.async_helpers import run_async
|
from utils.async_helpers import run_async
|
||||||
soulseek = current_app.soulsync.get("soulseek_client")
|
soulseek = current_app.soulsync.get("download_orchestrator")
|
||||||
if not soulseek:
|
if not soulseek:
|
||||||
return api_error("NOT_AVAILABLE", "Soulseek client not configured.", 503)
|
return api_error("NOT_AVAILABLE", "Soulseek client not configured.", 503)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ def _run_search_and_download(request_id, query, notify_url):
|
||||||
if request_id in _pending_requests:
|
if request_id in _pending_requests:
|
||||||
_pending_requests[request_id]['status'] = 'searching'
|
_pending_requests[request_id]['status'] = 'searching'
|
||||||
|
|
||||||
soulseek = current_app._get_current_object().soulsync.get('soulseek_client')
|
soulseek = current_app._get_current_object().soulsync.get('download_orchestrator')
|
||||||
if not soulseek:
|
if not soulseek:
|
||||||
with _requests_lock:
|
with _requests_lock:
|
||||||
if request_id in _pending_requests:
|
if request_id in _pending_requests:
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ def register_routes(bp):
|
||||||
spotify = ctx.get("spotify_client")
|
spotify = ctx.get("spotify_client")
|
||||||
spotify_ok = bool(spotify and spotify.is_authenticated())
|
spotify_ok = bool(spotify and spotify.is_authenticated())
|
||||||
|
|
||||||
soulseek = ctx.get("soulseek_client")
|
soulseek = ctx.get("download_orchestrator")
|
||||||
soulseek_ok = bool(soulseek)
|
soulseek_ok = bool(soulseek)
|
||||||
|
|
||||||
hydrabase = ctx.get("hydrabase_client")
|
hydrabase = ctx.get("hydrabase_client")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""Service connection test — lifted from web_server.py.
|
"""Service connection test — lifted from web_server.py.
|
||||||
|
|
||||||
The function body is byte-identical to the original. soulseek_client,
|
The function body is byte-identical to the original. download_orchestrator,
|
||||||
qobuz_enrichment_worker, hydrabase_client, docker_resolve_url, and
|
qobuz_enrichment_worker, hydrabase_client, docker_resolve_url, and
|
||||||
docker_resolve_path are injected at runtime because they live in
|
docker_resolve_path are injected at runtime because they live in
|
||||||
web_server.py and are constructed there.
|
web_server.py and are constructed there.
|
||||||
|
|
@ -27,7 +27,7 @@ def _get_metadata_fallback_source():
|
||||||
|
|
||||||
|
|
||||||
# Injected at runtime via init().
|
# Injected at runtime via init().
|
||||||
soulseek_client = None
|
download_orchestrator = None
|
||||||
qobuz_enrichment_worker = None
|
qobuz_enrichment_worker = None
|
||||||
hydrabase_client = None
|
hydrabase_client = None
|
||||||
docker_resolve_url = None
|
docker_resolve_url = None
|
||||||
|
|
@ -35,16 +35,16 @@ docker_resolve_path = None
|
||||||
|
|
||||||
|
|
||||||
def init(
|
def init(
|
||||||
soulseek_client_obj,
|
download_orchestrator_obj,
|
||||||
qobuz_worker,
|
qobuz_worker,
|
||||||
hydrabase_client_obj,
|
hydrabase_client_obj,
|
||||||
docker_resolve_url_fn,
|
docker_resolve_url_fn,
|
||||||
docker_resolve_path_fn,
|
docker_resolve_path_fn,
|
||||||
):
|
):
|
||||||
"""Bind web_server-side helpers/globals so the lifted body can resolve them."""
|
"""Bind web_server-side helpers/globals so the lifted body can resolve them."""
|
||||||
global soulseek_client, qobuz_enrichment_worker, hydrabase_client
|
global download_orchestrator, qobuz_enrichment_worker, hydrabase_client
|
||||||
global docker_resolve_url, docker_resolve_path
|
global docker_resolve_url, docker_resolve_path
|
||||||
soulseek_client = soulseek_client_obj
|
download_orchestrator = download_orchestrator_obj
|
||||||
qobuz_enrichment_worker = qobuz_worker
|
qobuz_enrichment_worker = qobuz_worker
|
||||||
hydrabase_client = hydrabase_client_obj
|
hydrabase_client = hydrabase_client_obj
|
||||||
docker_resolve_url = docker_resolve_url_fn
|
docker_resolve_url = docker_resolve_url_fn
|
||||||
|
|
@ -177,13 +177,13 @@ def run_service_test(service, test_config):
|
||||||
else:
|
else:
|
||||||
return False, f"Output folder not found: {transfer_path}"
|
return False, f"Output folder not found: {transfer_path}"
|
||||||
elif service == "soulseek":
|
elif service == "soulseek":
|
||||||
if soulseek_client is None:
|
if download_orchestrator is None:
|
||||||
return False, "Download orchestrator failed to initialize. Check server logs for startup errors."
|
return False, "Download orchestrator failed to initialize. Check server logs for startup errors."
|
||||||
|
|
||||||
# Test the orchestrator's configured download source (not just Soulseek)
|
# Test the orchestrator's configured download source (not just Soulseek)
|
||||||
download_mode = config_manager.get('download_source.mode', 'hybrid')
|
download_mode = config_manager.get('download_source.mode', 'hybrid')
|
||||||
|
|
||||||
if run_async(soulseek_client.check_connection()):
|
if run_async(download_orchestrator.check_connection()):
|
||||||
# Success message based on active mode
|
# Success message based on active mode
|
||||||
mode_messages = {
|
mode_messages = {
|
||||||
'soulseek': "Successfully connected to Soulseek network via slskd.",
|
'soulseek': "Successfully connected to Soulseek network via slskd.",
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ download_batches = None
|
||||||
sync_states = None
|
sync_states = None
|
||||||
youtube_playlist_states = None
|
youtube_playlist_states = None
|
||||||
tidal_discovery_states = None
|
tidal_discovery_states = None
|
||||||
soulseek_client = None
|
download_orchestrator = None
|
||||||
_log_path = None
|
_log_path = None
|
||||||
_log_dir = None
|
_log_dir = None
|
||||||
app = None
|
app = None
|
||||||
|
|
@ -80,7 +80,7 @@ def init(
|
||||||
sync_states_dict,
|
sync_states_dict,
|
||||||
youtube_playlist_states_dict,
|
youtube_playlist_states_dict,
|
||||||
tidal_discovery_states_dict,
|
tidal_discovery_states_dict,
|
||||||
soulseek_client_obj,
|
download_orchestrator_obj,
|
||||||
log_path,
|
log_path,
|
||||||
log_dir,
|
log_dir,
|
||||||
flask_app,
|
flask_app,
|
||||||
|
|
@ -90,7 +90,7 @@ def init(
|
||||||
"""Bind shared state/helpers from web_server."""
|
"""Bind shared state/helpers from web_server."""
|
||||||
global SOULSYNC_VERSION, _DIRECT_RUN, _status_cache, qobuz_enrichment_worker
|
global SOULSYNC_VERSION, _DIRECT_RUN, _status_cache, qobuz_enrichment_worker
|
||||||
global download_batches, sync_states, youtube_playlist_states
|
global download_batches, sync_states, youtube_playlist_states
|
||||||
global tidal_discovery_states, soulseek_client, _log_path, _log_dir
|
global tidal_discovery_states, download_orchestrator, _log_path, _log_dir
|
||||||
global app, get_database, _get_tidal_client
|
global app, get_database, _get_tidal_client
|
||||||
SOULSYNC_VERSION = soulsync_version
|
SOULSYNC_VERSION = soulsync_version
|
||||||
_DIRECT_RUN = direct_run
|
_DIRECT_RUN = direct_run
|
||||||
|
|
@ -100,7 +100,7 @@ def init(
|
||||||
sync_states = sync_states_dict
|
sync_states = sync_states_dict
|
||||||
youtube_playlist_states = youtube_playlist_states_dict
|
youtube_playlist_states = youtube_playlist_states_dict
|
||||||
tidal_discovery_states = tidal_discovery_states_dict
|
tidal_discovery_states = tidal_discovery_states_dict
|
||||||
soulseek_client = soulseek_client_obj
|
download_orchestrator = download_orchestrator_obj
|
||||||
_log_path = log_path
|
_log_path = log_path
|
||||||
_log_dir = log_dir
|
_log_dir = log_dir
|
||||||
app = flask_app
|
app = flask_app
|
||||||
|
|
@ -292,9 +292,9 @@ def get_debug_info():
|
||||||
|
|
||||||
# Download client init failures
|
# Download client init failures
|
||||||
info['download_client_failures'] = []
|
info['download_client_failures'] = []
|
||||||
if soulseek_client and hasattr(soulseek_client, '_init_failures'):
|
if download_orchestrator and hasattr(download_orchestrator, '_init_failures'):
|
||||||
info['download_client_failures'] = soulseek_client._init_failures
|
info['download_client_failures'] = download_orchestrator._init_failures
|
||||||
elif not soulseek_client:
|
elif not download_orchestrator:
|
||||||
info['download_client_failures'] = ['ALL (orchestrator failed to initialize)']
|
info['download_client_failures'] = ['ALL (orchestrator failed to initialize)']
|
||||||
|
|
||||||
# API rate monitor — current calls/min, 24h totals, peaks, rate limit events
|
# API rate monitor — current calls/min, 24h totals, peaks, rate limit events
|
||||||
|
|
|
||||||
|
|
@ -547,10 +547,8 @@ class DownloadOrchestrator:
|
||||||
# Singleton accessor — mirrors Cin's metadata engine pattern
|
# Singleton accessor — mirrors Cin's metadata engine pattern
|
||||||
# (``get_metadata_engine()``). Callers that don't need a custom
|
# (``get_metadata_engine()``). Callers that don't need a custom
|
||||||
# registry use this instead of instantiating DownloadOrchestrator
|
# registry use this instead of instantiating DownloadOrchestrator
|
||||||
# directly. Currently web_server.py constructs the singleton at
|
# directly. web_server.py constructs the singleton at startup and
|
||||||
# startup and exposes it via the legacy ``soulseek_client`` global;
|
# exposes it via the ``download_orchestrator`` global.
|
||||||
# this factory exists for new callers + future migration of that
|
|
||||||
# global to a more honestly-named ``download_orchestrator``.
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_default_orchestrator: Optional['DownloadOrchestrator'] = None
|
_default_orchestrator: Optional['DownloadOrchestrator'] = None
|
||||||
|
|
|
||||||
|
|
@ -42,31 +42,31 @@ _TERMINAL_STATUSES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def cancel_single_download(soulseek_client, run_async: Callable,
|
def cancel_single_download(download_orchestrator, run_async: Callable,
|
||||||
download_id: str, username: str) -> bool:
|
download_id: str, username: str) -> bool:
|
||||||
"""Cancel one specific slskd download (with `remove=True`)."""
|
"""Cancel one specific slskd download (with `remove=True`)."""
|
||||||
return run_async(soulseek_client.cancel_download(download_id, username, remove=True))
|
return run_async(download_orchestrator.cancel_download(download_id, username, remove=True))
|
||||||
|
|
||||||
|
|
||||||
def cancel_all_active(soulseek_client, run_async: Callable,
|
def cancel_all_active(download_orchestrator, run_async: Callable,
|
||||||
sweep_callback: Callable[[], None]) -> tuple[bool, str]:
|
sweep_callback: Callable[[], None]) -> tuple[bool, str]:
|
||||||
"""Cancel every active slskd download, clear the resulting ones, sweep dirs.
|
"""Cancel every active slskd download, clear the resulting ones, sweep dirs.
|
||||||
|
|
||||||
Returns `(success, message)` so the route can map to the right HTTP shape.
|
Returns `(success, message)` so the route can map to the right HTTP shape.
|
||||||
"""
|
"""
|
||||||
cancel_success = run_async(soulseek_client.cancel_all_downloads())
|
cancel_success = run_async(download_orchestrator.cancel_all_downloads())
|
||||||
if not cancel_success:
|
if not cancel_success:
|
||||||
return False, "Failed to cancel active downloads."
|
return False, "Failed to cancel active downloads."
|
||||||
|
|
||||||
run_async(soulseek_client.clear_all_completed_downloads())
|
run_async(download_orchestrator.clear_all_completed_downloads())
|
||||||
sweep_callback()
|
sweep_callback()
|
||||||
return True, "All downloads cancelled and cleared."
|
return True, "All downloads cancelled and cleared."
|
||||||
|
|
||||||
|
|
||||||
def clear_finished_active(soulseek_client, run_async: Callable,
|
def clear_finished_active(download_orchestrator, run_async: Callable,
|
||||||
sweep_callback: Callable[[], None]) -> bool:
|
sweep_callback: Callable[[], None]) -> bool:
|
||||||
"""Clear all terminal transfers from slskd, sweep dirs on success."""
|
"""Clear all terminal transfers from slskd, sweep dirs on success."""
|
||||||
success = run_async(soulseek_client.clear_all_completed_downloads())
|
success = run_async(download_orchestrator.clear_all_completed_downloads())
|
||||||
if success:
|
if success:
|
||||||
sweep_callback()
|
sweep_callback()
|
||||||
return success
|
return success
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ and notifies the lifecycle via `on_download_completed(success=False)` so the
|
||||||
worker slot frees up.
|
worker slot frees up.
|
||||||
|
|
||||||
Lifted verbatim from web_server.py. Wide dependency surface
|
Lifted verbatim from web_server.py. Wide dependency surface
|
||||||
(soulseek_client, spotify_client, lifecycle callback, context-key helper,
|
(download_orchestrator, spotify_client, lifecycle callback, context-key helper,
|
||||||
status updater, DB) all injected via `CandidatesDeps`.
|
status updater, DB) all injected via `CandidatesDeps`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ logger = logging.getLogger(__name__)
|
||||||
@dataclass
|
@dataclass
|
||||||
class CandidatesDeps:
|
class CandidatesDeps:
|
||||||
"""Bundle of cross-cutting deps the candidate-fallback logic needs."""
|
"""Bundle of cross-cutting deps the candidate-fallback logic needs."""
|
||||||
soulseek_client: Any
|
download_orchestrator: Any
|
||||||
spotify_client: Any
|
spotify_client: Any
|
||||||
run_async: Callable[..., Any]
|
run_async: Callable[..., Any]
|
||||||
get_database: Callable[[], Any]
|
get_database: Callable[[], Any]
|
||||||
|
|
@ -209,7 +209,7 @@ def attempt_download_with_candidates(task_id, candidates, track, batch_id=None,
|
||||||
|
|
||||||
# Initiate download
|
# Initiate download
|
||||||
logger.info(f"[Modal Worker] Starting download: {username} / {os.path.basename(filename)}")
|
logger.info(f"[Modal Worker] Starting download: {username} / {os.path.basename(filename)}")
|
||||||
download_id = deps.run_async(deps.soulseek_client.download(username, filename, size))
|
download_id = deps.run_async(deps.download_orchestrator.download(username, filename, size))
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
# Store context for post-processing with complete Spotify metadata (GUI PARITY)
|
# Store context for post-processing with complete Spotify metadata (GUI PARITY)
|
||||||
|
|
@ -330,7 +330,7 @@ def attempt_download_with_candidates(task_id, candidates, track, batch_id=None,
|
||||||
logger.warning(f"[Modal Worker] Task {task_id} cancelled after download {download_id} started - attempting to cancel download")
|
logger.warning(f"[Modal Worker] Task {task_id} cancelled after download {download_id} started - attempting to cancel download")
|
||||||
# Try to cancel the download immediately
|
# Try to cancel the download immediately
|
||||||
try:
|
try:
|
||||||
deps.run_async(deps.soulseek_client.cancel_download(download_id, username, remove=True))
|
deps.run_async(deps.download_orchestrator.cancel_download(download_id, username, remove=True))
|
||||||
logger.warning(f"Successfully cancelled active download {download_id}")
|
logger.warning(f"Successfully cancelled active download {download_id}")
|
||||||
except Exception as cancel_error:
|
except Exception as cancel_error:
|
||||||
logger.error(f"Failed to cancel active download {download_id}: {cancel_error}")
|
logger.error(f"Failed to cancel active download {download_id}: {cancel_error}")
|
||||||
|
|
@ -340,8 +340,8 @@ def attempt_download_with_candidates(task_id, candidates, track, batch_id=None,
|
||||||
deps.on_download_completed(batch_id, task_id, success=False)
|
deps.on_download_completed(batch_id, task_id, success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Store download information - use real download ID from soulseek_client
|
# Store download information - use real download ID from download_orchestrator
|
||||||
# CRITICAL FIX: Trust the download ID returned by soulseek_client.download()
|
# CRITICAL FIX: Trust the download ID returned by download_orchestrator.download()
|
||||||
download_tasks[task_id]['download_id'] = download_id
|
download_tasks[task_id]['download_id'] = download_id
|
||||||
|
|
||||||
download_tasks[task_id]['username'] = username
|
download_tasks[task_id]['username'] = username
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||||
class MasterDeps:
|
class MasterDeps:
|
||||||
"""Bundle of cross-cutting deps the master worker needs."""
|
"""Bundle of cross-cutting deps the master worker needs."""
|
||||||
config_manager: Any
|
config_manager: Any
|
||||||
soulseek_client: Any
|
download_orchestrator: Any
|
||||||
run_async: Callable[..., Any]
|
run_async: Callable[..., Any]
|
||||||
mb_worker: Any
|
mb_worker: Any
|
||||||
mb_release_cache: dict
|
mb_release_cache: dict
|
||||||
|
|
@ -372,7 +372,7 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma
|
||||||
_sr.info(f"[Album Pre-flight] Searching for '{artist_name} {album_name}'")
|
_sr.info(f"[Album Pre-flight] Searching for '{artist_name} {album_name}'")
|
||||||
logger.info(f"[Album Pre-flight] Searching Soulseek for complete album: '{artist_name} - {album_name}'")
|
logger.info(f"[Album Pre-flight] Searching Soulseek for complete album: '{artist_name} - {album_name}'")
|
||||||
|
|
||||||
slsk = deps.soulseek_client.client('soulseek') if hasattr(deps.soulseek_client, 'client') else deps.soulseek_client
|
slsk = deps.download_orchestrator.client('soulseek') if hasattr(deps.download_orchestrator, 'client') else deps.download_orchestrator
|
||||||
|
|
||||||
# Try multiple query variations (banned keywords in artist/album name can return 0 results)
|
# Try multiple query variations (banned keywords in artist/album name can return 0 results)
|
||||||
album_queries = [f"{artist_name} {album_name}"]
|
album_queries = [f"{artist_name} {album_name}"]
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ _run_post_processing_worker = None
|
||||||
_start_next_batch_of_downloads = None
|
_start_next_batch_of_downloads = None
|
||||||
_orphaned_download_keys = None
|
_orphaned_download_keys = None
|
||||||
missing_download_executor = None
|
missing_download_executor = None
|
||||||
soulseek_client = None
|
download_orchestrator = None
|
||||||
|
|
||||||
|
|
||||||
def init(
|
def init(
|
||||||
|
|
@ -44,12 +44,12 @@ def init(
|
||||||
start_next_batch_of_downloads,
|
start_next_batch_of_downloads,
|
||||||
orphaned_download_keys,
|
orphaned_download_keys,
|
||||||
missing_download_executor_obj,
|
missing_download_executor_obj,
|
||||||
soulseek_client_obj,
|
download_orchestrator_obj,
|
||||||
):
|
):
|
||||||
"""Bind web_server-side helpers/globals so the class body can resolve them."""
|
"""Bind web_server-side helpers/globals so the class body can resolve them."""
|
||||||
global _make_context_key, _on_download_completed, _download_track_worker
|
global _make_context_key, _on_download_completed, _download_track_worker
|
||||||
global _run_post_processing_worker, _start_next_batch_of_downloads
|
global _run_post_processing_worker, _start_next_batch_of_downloads
|
||||||
global _orphaned_download_keys, missing_download_executor, soulseek_client
|
global _orphaned_download_keys, missing_download_executor, download_orchestrator
|
||||||
_make_context_key = make_context_key
|
_make_context_key = make_context_key
|
||||||
_on_download_completed = on_download_completed
|
_on_download_completed = on_download_completed
|
||||||
_download_track_worker = download_track_worker
|
_download_track_worker = download_track_worker
|
||||||
|
|
@ -57,7 +57,7 @@ def init(
|
||||||
_start_next_batch_of_downloads = start_next_batch_of_downloads
|
_start_next_batch_of_downloads = start_next_batch_of_downloads
|
||||||
_orphaned_download_keys = orphaned_download_keys
|
_orphaned_download_keys = orphaned_download_keys
|
||||||
missing_download_executor = missing_download_executor_obj
|
missing_download_executor = missing_download_executor_obj
|
||||||
soulseek_client = soulseek_client_obj
|
download_orchestrator = download_orchestrator_obj
|
||||||
|
|
||||||
|
|
||||||
class WebUIDownloadMonitor:
|
class WebUIDownloadMonitor:
|
||||||
|
|
@ -199,7 +199,7 @@ class WebUIDownloadMonitor:
|
||||||
if op[0] == 'cancel_download':
|
if op[0] == 'cancel_download':
|
||||||
_, download_id, username = op
|
_, download_id, username = op
|
||||||
logger.debug(f"[Deferred] Cancelling download: {download_id} from {username}")
|
logger.debug(f"[Deferred] Cancelling download: {download_id} from {username}")
|
||||||
run_async(soulseek_client.cancel_download(download_id, username, remove=True))
|
run_async(download_orchestrator.cancel_download(download_id, username, remove=True))
|
||||||
logger.debug(f"[Deferred] Successfully cancelled download {download_id}")
|
logger.debug(f"[Deferred] Successfully cancelled download {download_id}")
|
||||||
elif op[0] == 'cleanup_orphan':
|
elif op[0] == 'cleanup_orphan':
|
||||||
_, context_key = op
|
_, context_key = op
|
||||||
|
|
@ -254,9 +254,9 @@ class WebUIDownloadMonitor:
|
||||||
|
|
||||||
# Get Soulseek downloads from API
|
# Get Soulseek downloads from API
|
||||||
transfers_data = None
|
transfers_data = None
|
||||||
_slsk = soulseek_client.client('soulseek') if soulseek_client and hasattr(soulseek_client, 'client') else None
|
_slsk = download_orchestrator.client('soulseek') if download_orchestrator and hasattr(download_orchestrator, 'client') else None
|
||||||
if soulseek_active and _slsk and _slsk.base_url:
|
if soulseek_active and _slsk and _slsk.base_url:
|
||||||
transfers_data = run_async(soulseek_client._make_request('GET', 'transfers/downloads'))
|
transfers_data = run_async(download_orchestrator._make_request('GET', 'transfers/downloads'))
|
||||||
if transfers_data:
|
if transfers_data:
|
||||||
for user_data in transfers_data:
|
for user_data in transfers_data:
|
||||||
username = user_data.get('username', 'Unknown')
|
username = user_data.get('username', 'Unknown')
|
||||||
|
|
@ -271,9 +271,9 @@ class WebUIDownloadMonitor:
|
||||||
# cross-source aggregation, no per-source iteration.
|
# cross-source aggregation, no per-source iteration.
|
||||||
try:
|
try:
|
||||||
all_downloads = []
|
all_downloads = []
|
||||||
if soulseek_client and hasattr(soulseek_client, 'engine'):
|
if download_orchestrator and hasattr(download_orchestrator, 'engine'):
|
||||||
try:
|
try:
|
||||||
all_downloads = run_async(soulseek_client.engine.get_all_downloads())
|
all_downloads = run_async(download_orchestrator.engine.get_all_downloads())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
for download in all_downloads:
|
for download in all_downloads:
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class PostProcessDeps:
|
||||||
always live (no caching of pre-init Spotify clients etc).
|
always live (no caching of pre-init Spotify clients etc).
|
||||||
"""
|
"""
|
||||||
config_manager: Any
|
config_manager: Any
|
||||||
soulseek_client: Any
|
download_orchestrator: Any
|
||||||
run_async: Callable
|
run_async: Callable
|
||||||
docker_resolve_path: Callable[[str], str]
|
docker_resolve_path: Callable[[str], str]
|
||||||
extract_filename: Callable[[str], str]
|
extract_filename: Callable[[str], str]
|
||||||
|
|
@ -196,7 +196,7 @@ def run_post_processing_worker(task_id: str, batch_id: str, deps: PostProcessDep
|
||||||
# Query the download orchestrator for the status which contains the real file path
|
# Query the download orchestrator for the status which contains the real file path
|
||||||
# CRITICAL FIX: Use the actual download_id designated by the client, not the internal task_id
|
# CRITICAL FIX: Use the actual download_id designated by the client, not the internal task_id
|
||||||
actual_download_id = task.get('download_id') or task_id
|
actual_download_id = task.get('download_id') or task_id
|
||||||
status = deps.run_async(deps.soulseek_client.get_download_status(actual_download_id))
|
status = deps.run_async(deps.download_orchestrator.get_download_status(actual_download_id))
|
||||||
if status and status.file_path:
|
if status and status.file_path:
|
||||||
real_path = status.file_path
|
real_path = status.file_path
|
||||||
if os.path.exists(real_path):
|
if os.path.exists(real_path):
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ logger = logging.getLogger(__name__)
|
||||||
@dataclass
|
@dataclass
|
||||||
class TaskWorkerDeps:
|
class TaskWorkerDeps:
|
||||||
"""Bundle of cross-cutting deps the per-task download worker needs."""
|
"""Bundle of cross-cutting deps the per-task download worker needs."""
|
||||||
soulseek_client: Any
|
download_orchestrator: Any
|
||||||
matching_engine: Any
|
matching_engine: Any
|
||||||
run_async: Callable
|
run_async: Callable
|
||||||
try_source_reuse: Callable # (task_id, batch_id, track) -> bool
|
try_source_reuse: Callable # (task_id, batch_id, track) -> bool
|
||||||
|
|
@ -210,7 +210,7 @@ def download_track_worker(task_id: str, batch_id: Optional[str], deps: TaskWorke
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Perform search with timeout
|
# Perform search with timeout
|
||||||
tracks_result, _ = deps.run_async(deps.soulseek_client.search(query, timeout=30))
|
tracks_result, _ = deps.run_async(deps.download_orchestrator.search(query, timeout=30))
|
||||||
logger.debug(f"Search completed for task {task_id}, got {len(tracks_result) if tracks_result else 0} results")
|
logger.debug(f"Search completed for task {task_id}, got {len(tracks_result) if tracks_result else 0} results")
|
||||||
|
|
||||||
# CRITICAL: Check cancellation immediately after search returns
|
# CRITICAL: Check cancellation immediately after search returns
|
||||||
|
|
@ -272,9 +272,9 @@ def download_track_worker(task_id: str, batch_id: Optional[str], deps: TaskWorke
|
||||||
# === HYBRID FALLBACK: If primary source failed, try remaining sources directly ===
|
# === HYBRID FALLBACK: If primary source failed, try remaining sources directly ===
|
||||||
# The orchestrator's hybrid search stops at the first source with results, even if
|
# The orchestrator's hybrid search stops at the first source with results, even if
|
||||||
# those results all fail quality filtering. Try remaining sources individually.
|
# those results all fail quality filtering. Try remaining sources individually.
|
||||||
if getattr(deps.soulseek_client, 'mode', '') == 'hybrid':
|
if getattr(deps.download_orchestrator, 'mode', '') == 'hybrid':
|
||||||
try:
|
try:
|
||||||
orch = deps.soulseek_client
|
orch = deps.download_orchestrator
|
||||||
hybrid_order = getattr(orch, 'hybrid_order', None) or []
|
hybrid_order = getattr(orch, 'hybrid_order', None) or []
|
||||||
if not hybrid_order:
|
if not hybrid_order:
|
||||||
primary = getattr(orch, 'hybrid_primary', 'soulseek')
|
primary = getattr(orch, 'hybrid_primary', 'soulseek')
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Soulseek/streaming candidate validation — lifted from web_server.py.
|
"""Soulseek/streaming candidate validation — lifted from web_server.py.
|
||||||
|
|
||||||
Body is byte-identical to the original. ``matching_engine`` and
|
Body is byte-identical to the original. ``matching_engine`` and
|
||||||
``soulseek_client`` are injected via init() because both are
|
``download_orchestrator`` are injected via init() because both are
|
||||||
constructed in web_server.py and referenced by name throughout
|
constructed in web_server.py and referenced by name throughout
|
||||||
the body.
|
the body.
|
||||||
"""
|
"""
|
||||||
|
|
@ -14,14 +14,14 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Injected at runtime via init().
|
# Injected at runtime via init().
|
||||||
matching_engine = None
|
matching_engine = None
|
||||||
soulseek_client = None
|
download_orchestrator = None
|
||||||
|
|
||||||
|
|
||||||
def init(matching_engine_obj, soulseek_client_obj):
|
def init(matching_engine_obj, download_orchestrator_obj):
|
||||||
"""Bind the matching engine and download orchestrator from web_server."""
|
"""Bind the matching engine and download orchestrator from web_server."""
|
||||||
global matching_engine, soulseek_client
|
global matching_engine, download_orchestrator
|
||||||
matching_engine = matching_engine_obj
|
matching_engine = matching_engine_obj
|
||||||
soulseek_client = soulseek_client_obj
|
download_orchestrator = download_orchestrator_obj
|
||||||
|
|
||||||
|
|
||||||
def get_valid_candidates(results, spotify_track, query):
|
def get_valid_candidates(results, spotify_track, query):
|
||||||
|
|
@ -151,8 +151,8 @@ def get_valid_candidates(results, spotify_track, query):
|
||||||
quality_filtered_candidates = initial_candidates
|
quality_filtered_candidates = initial_candidates
|
||||||
else:
|
else:
|
||||||
# Filter by user's quality profile before artist verification (Soulseek only)
|
# Filter by user's quality profile before artist verification (Soulseek only)
|
||||||
# Use existing soulseek_client to avoid re-initializing (which accesses download_path filesystem)
|
# Use existing download_orchestrator to avoid re-initializing (which accesses download_path filesystem)
|
||||||
quality_filtered_candidates = soulseek_client.client('soulseek').filter_results_by_quality_preference(initial_candidates)
|
quality_filtered_candidates = download_orchestrator.client('soulseek').filter_results_by_quality_preference(initial_candidates)
|
||||||
|
|
||||||
# IMPORTANT: Respect empty results from quality filter
|
# IMPORTANT: Respect empty results from quality filter
|
||||||
# If user has strict quality requirements (e.g., FLAC-only with fallback disabled),
|
# If user has strict quality requirements (e.g., FLAC-only with fallback disabled),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Body is byte-identical to the original. Wishlist helpers are
|
Body is byte-identical to the original. Wishlist helpers are
|
||||||
direct imports from core.wishlist.*; runtime state comes from
|
direct imports from core.wishlist.*; runtime state comes from
|
||||||
core.runtime_state; automation_engine, soulseek_client, and the
|
core.runtime_state; automation_engine, download_orchestrator, and the
|
||||||
sweep helper are injected via init() because they are constructed
|
sweep helper are injected via init() because they are constructed
|
||||||
in web_server.py.
|
in web_server.py.
|
||||||
"""
|
"""
|
||||||
|
|
@ -29,15 +29,15 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Injected at runtime via init().
|
# Injected at runtime via init().
|
||||||
automation_engine = None
|
automation_engine = None
|
||||||
soulseek_client = None
|
download_orchestrator = None
|
||||||
_sweep_empty_download_directories = None
|
_sweep_empty_download_directories = None
|
||||||
|
|
||||||
|
|
||||||
def init(engine, soulseek_client_obj, sweep_fn):
|
def init(engine, download_orchestrator_obj, sweep_fn):
|
||||||
"""Bind shared singletons + the sweep helper from web_server."""
|
"""Bind shared singletons + the sweep helper from web_server."""
|
||||||
global automation_engine, soulseek_client, _sweep_empty_download_directories
|
global automation_engine, download_orchestrator, _sweep_empty_download_directories
|
||||||
automation_engine = engine
|
automation_engine = engine
|
||||||
soulseek_client = soulseek_client_obj
|
download_orchestrator = download_orchestrator_obj
|
||||||
_sweep_empty_download_directories = sweep_fn
|
_sweep_empty_download_directories = sweep_fn
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -185,7 +185,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
|
||||||
# Auto-cleanup: Clear completed downloads from slskd
|
# Auto-cleanup: Clear completed downloads from slskd
|
||||||
try:
|
try:
|
||||||
logger.info(f"[Auto-Cleanup] Clearing completed downloads from slskd after batch {batch_id}")
|
logger.info(f"[Auto-Cleanup] Clearing completed downloads from slskd after batch {batch_id}")
|
||||||
run_async(soulseek_client.clear_all_completed_downloads())
|
run_async(download_orchestrator.clear_all_completed_downloads())
|
||||||
logger.info("[Auto-Cleanup] Completed downloads cleared from slskd")
|
logger.info("[Auto-Cleanup] Completed downloads cleared from slskd")
|
||||||
except Exception as cleanup_error:
|
except Exception as cleanup_error:
|
||||||
logger.warning(f"[Auto-Cleanup] Failed to clear completed downloads: {cleanup_error}")
|
logger.warning(f"[Auto-Cleanup] Failed to clear completed downloads: {cleanup_error}")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def run_basic_soulseek_search(
|
def run_basic_soulseek_search(
|
||||||
query: str,
|
query: str,
|
||||||
soulseek_client,
|
download_orchestrator,
|
||||||
run_async: Callable,
|
run_async: Callable,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Search Soulseek for `query`, normalize albums + tracks to one sorted list.
|
"""Search Soulseek for `query`, normalize albums + tracks to one sorted list.
|
||||||
|
|
@ -22,7 +22,7 @@ def run_basic_soulseek_search(
|
||||||
Returns dicts with `result_type` set to "album" or "track" and sorted by
|
Returns dicts with `result_type` set to "album" or "track" and sorted by
|
||||||
`quality_score` descending. Empty list on any failure (caller logs).
|
`quality_score` descending. Empty list on any failure (caller logs).
|
||||||
"""
|
"""
|
||||||
tracks, albums = run_async(soulseek_client.search(query))
|
tracks, albums = run_async(download_orchestrator.search(query))
|
||||||
|
|
||||||
processed_albums = []
|
processed_albums = []
|
||||||
for album in albums:
|
for album in albums:
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class SearchDeps:
|
||||||
spotify_client: Any
|
spotify_client: Any
|
||||||
hydrabase_client: Any
|
hydrabase_client: Any
|
||||||
hydrabase_worker: Any
|
hydrabase_worker: Any
|
||||||
soulseek_client: Any
|
download_orchestrator: Any
|
||||||
fix_artist_image_url: Callable[[Optional[str]], Optional[str]]
|
fix_artist_image_url: Callable[[Optional[str]], Optional[str]]
|
||||||
is_hydrabase_active: Callable[[], bool]
|
is_hydrabase_active: Callable[[], bool]
|
||||||
get_metadata_fallback_source: Callable[[], str]
|
get_metadata_fallback_source: Callable[[], str]
|
||||||
|
|
@ -291,9 +291,9 @@ def run_enhanced_search(query: str, requested_source: str, deps: SearchDeps) ->
|
||||||
def resolve_youtube_videos_client(deps: SearchDeps):
|
def resolve_youtube_videos_client(deps: SearchDeps):
|
||||||
"""Return the YouTube download client (used for music-video search)
|
"""Return the YouTube download client (used for music-video search)
|
||||||
via the orchestrator's generic accessor, or None when unavailable."""
|
via the orchestrator's generic accessor, or None when unavailable."""
|
||||||
if not deps.soulseek_client or not hasattr(deps.soulseek_client, 'client'):
|
if not deps.download_orchestrator or not hasattr(deps.download_orchestrator, 'client'):
|
||||||
return None
|
return None
|
||||||
return deps.soulseek_client.client('youtube')
|
return deps.download_orchestrator.client('youtube')
|
||||||
|
|
||||||
|
|
||||||
def stream_youtube_videos(query: str, youtube_client, run_async: Callable) -> Iterator[str]:
|
def stream_youtube_videos(query: str, youtube_client, run_async: Callable) -> Iterator[str]:
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ def stream_search_track(
|
||||||
album_name: Optional[str],
|
album_name: Optional[str],
|
||||||
duration_ms: int,
|
duration_ms: int,
|
||||||
config_manager,
|
config_manager,
|
||||||
soulseek_client,
|
download_orchestrator,
|
||||||
matching_engine,
|
matching_engine,
|
||||||
run_async: Callable,
|
run_async: Callable,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
|
|
@ -120,7 +120,7 @@ def stream_search_track(
|
||||||
|
|
||||||
# Map mode name to canonical registry name (legacy 'deezer_dl'
|
# Map mode name to canonical registry name (legacy 'deezer_dl'
|
||||||
# alias resolves to 'deezer' via the orchestrator's registry).
|
# alias resolves to 'deezer' via the orchestrator's registry).
|
||||||
stream_client = soulseek_client.client(effective_mode)
|
stream_client = download_orchestrator.client(effective_mode)
|
||||||
use_direct_client = stream_client is not None
|
use_direct_client = stream_client is not None
|
||||||
|
|
||||||
max_peer_queue = config_manager.get('soulseek.max_peer_queue', 0) or 0
|
max_peer_queue = config_manager.get('soulseek.max_peer_queue', 0) or 0
|
||||||
|
|
@ -131,7 +131,7 @@ def stream_search_track(
|
||||||
if use_direct_client:
|
if use_direct_client:
|
||||||
tracks_result, _ = run_async(stream_client.search(query, timeout=15))
|
tracks_result, _ = run_async(stream_client.search(query, timeout=15))
|
||||||
else:
|
else:
|
||||||
tracks_result, _ = run_async(soulseek_client.search(query, timeout=15))
|
tracks_result, _ = run_async(download_orchestrator.search(query, timeout=15))
|
||||||
|
|
||||||
if not tracks_result:
|
if not tracks_result:
|
||||||
logger.info(f"No results for query '{query}', trying next...")
|
logger.info(f"No results for query '{query}', trying next...")
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ it in the local Stream/ folder for the browser audio player.
|
||||||
1. Reset stream state to 'loading' with the new track info.
|
1. Reset stream state to 'loading' with the new track info.
|
||||||
2. Clear any prior file from the Stream/ folder (only one stream lives
|
2. Clear any prior file from the Stream/ folder (only one stream lives
|
||||||
there at a time).
|
there at a time).
|
||||||
3. Spin up a fresh asyncio event loop and `soulseek_client.download()`
|
3. Spin up a fresh asyncio event loop and `download_orchestrator.download()`
|
||||||
the track.
|
the track.
|
||||||
4. Poll `soulseek_client.get_all_downloads()` every 1.5 s to track
|
4. Poll `download_orchestrator.get_all_downloads()` every 1.5 s to track
|
||||||
progress, with separate handling for queued vs actively downloading
|
progress, with separate handling for queued vs actively downloading
|
||||||
states. Queue timeout = 15 s; overall timeout = 60 s.
|
states. Queue timeout = 15 s; overall timeout = 60 s.
|
||||||
5. On completion (state ~ 'succeeded' or progress >= 100% AND bytes
|
5. On completion (state ~ 'succeeded' or progress >= 100% AND bytes
|
||||||
|
|
@ -45,7 +45,7 @@ logger = logging.getLogger(__name__)
|
||||||
class PrepareStreamDeps:
|
class PrepareStreamDeps:
|
||||||
"""Bundle of cross-cutting deps the stream-prep worker needs."""
|
"""Bundle of cross-cutting deps the stream-prep worker needs."""
|
||||||
config_manager: Any
|
config_manager: Any
|
||||||
soulseek_client: Any
|
download_orchestrator: Any
|
||||||
stream_lock: Any # threading.Lock
|
stream_lock: Any # threading.Lock
|
||||||
project_root: str # absolute path to web_server.py's directory
|
project_root: str # absolute path to web_server.py's directory
|
||||||
docker_resolve_path: Callable[[str], str]
|
docker_resolve_path: Callable[[str], str]
|
||||||
|
|
@ -112,7 +112,7 @@ def prepare_stream_task(track_data, deps: PrepareStreamDeps):
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
download_result = loop.run_until_complete(deps.soulseek_client.download(
|
download_result = loop.run_until_complete(deps.download_orchestrator.download(
|
||||||
track_data.get('username'),
|
track_data.get('username'),
|
||||||
track_data.get('filename'),
|
track_data.get('filename'),
|
||||||
track_data.get('size', 0)
|
track_data.get('size', 0)
|
||||||
|
|
@ -144,7 +144,7 @@ def prepare_stream_task(track_data, deps: PrepareStreamDeps):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use orchestrator's get_all_downloads() which works for both sources
|
# Use orchestrator's get_all_downloads() which works for both sources
|
||||||
all_downloads = loop.run_until_complete(deps.soulseek_client.get_all_downloads())
|
all_downloads = loop.run_until_complete(deps.download_orchestrator.get_all_downloads())
|
||||||
download_status = deps.find_streaming_download_in_all_downloads(all_downloads, track_data)
|
download_status = deps.find_streaming_download_in_all_downloads(all_downloads, track_data)
|
||||||
|
|
||||||
if download_status:
|
if download_status:
|
||||||
|
|
@ -252,7 +252,7 @@ def prepare_stream_task(track_data, deps: PrepareStreamDeps):
|
||||||
download_id = download_status.get('id', '')
|
download_id = download_status.get('id', '')
|
||||||
if download_id and track_data.get('username'):
|
if download_id and track_data.get('username'):
|
||||||
success = loop.run_until_complete(
|
success = loop.run_until_complete(
|
||||||
deps.soulseek_client.signal_download_completion(
|
deps.download_orchestrator.signal_download_completion(
|
||||||
download_id, track_data.get('username'), remove=True)
|
download_id, track_data.get('username'), remove=True)
|
||||||
)
|
)
|
||||||
if success:
|
if success:
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,12 @@ class SyncProgress:
|
||||||
failed_tracks: int = 0
|
failed_tracks: int = 0
|
||||||
|
|
||||||
class PlaylistSyncService:
|
class PlaylistSyncService:
|
||||||
def __init__(self, spotify_client: SpotifyClient, plex_client: PlexClient, soulseek_client: SoulseekClient, jellyfin_client: JellyfinClient = None, navidrome_client = None):
|
def __init__(self, spotify_client: SpotifyClient, plex_client: PlexClient, download_orchestrator: SoulseekClient, jellyfin_client: JellyfinClient = None, navidrome_client = None):
|
||||||
self.spotify_client = spotify_client
|
self.spotify_client = spotify_client
|
||||||
self.plex_client = plex_client
|
self.plex_client = plex_client
|
||||||
self.jellyfin_client = jellyfin_client
|
self.jellyfin_client = jellyfin_client
|
||||||
self.navidrome_client = navidrome_client
|
self.navidrome_client = navidrome_client
|
||||||
self.soulseek_client = soulseek_client
|
self.download_orchestrator = download_orchestrator
|
||||||
self.progress_callbacks = {} # Playlist-specific progress callbacks
|
self.progress_callbacks = {} # Playlist-specific progress callbacks
|
||||||
self.syncing_playlists = set() # Track multiple syncing playlists
|
self.syncing_playlists = set() # Track multiple syncing playlists
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
|
|
@ -635,7 +635,7 @@ class PlaylistSyncService:
|
||||||
query = self.matching_engine.generate_download_query(match_result.spotify_track)
|
query = self.matching_engine.generate_download_query(match_result.spotify_track)
|
||||||
logger.info(f"Attempting to download: {query}")
|
logger.info(f"Attempting to download: {query}")
|
||||||
|
|
||||||
download_id = await self.soulseek_client.search_and_download_best(query, expected_track=match_result.spotify_track)
|
download_id = await self.download_orchestrator.search_and_download_best(query, expected_track=match_result.spotify_track)
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
downloaded_count += 1
|
downloaded_count += 1
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ def _build_deps(
|
||||||
on_complete=None,
|
on_complete=None,
|
||||||
):
|
):
|
||||||
deps = dc.CandidatesDeps(
|
deps = dc.CandidatesDeps(
|
||||||
soulseek_client=soulseek or _FakeSoulseek(),
|
download_orchestrator=soulseek or _FakeSoulseek(),
|
||||||
spotify_client=spotify or _FakeSpotify(),
|
spotify_client=spotify or _FakeSpotify(),
|
||||||
run_async=_run_async,
|
run_async=_run_async,
|
||||||
get_database=lambda: db or _FakeDB(),
|
get_database=lambda: db or _FakeDB(),
|
||||||
|
|
@ -136,7 +136,7 @@ def test_first_candidate_starts_download_and_returns_true():
|
||||||
result = dc.attempt_download_with_candidates("t1", candidates, track, batch_id="b1", deps=deps)
|
result = dc.attempt_download_with_candidates("t1", candidates, track, batch_id="b1", deps=deps)
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
assert deps.soulseek_client.download_calls == [("user1", "best.flac", 1000)]
|
assert deps.download_orchestrator.download_calls == [("user1", "best.flac", 1000)]
|
||||||
assert download_tasks["t1"]["download_id"] == "dl-1"
|
assert download_tasks["t1"]["download_id"] == "dl-1"
|
||||||
assert "user1::best.flac" in matched_downloads_context
|
assert "user1::best.flac" in matched_downloads_context
|
||||||
|
|
||||||
|
|
@ -155,7 +155,7 @@ def test_candidates_tried_in_confidence_order():
|
||||||
dc.attempt_download_with_candidates("t2", candidates, track, batch_id=None, deps=deps)
|
dc.attempt_download_with_candidates("t2", candidates, track, batch_id=None, deps=deps)
|
||||||
|
|
||||||
# First call should be the highest-confidence one
|
# First call should be the highest-confidence one
|
||||||
assert deps.soulseek_client.download_calls[0][1] == "high.flac"
|
assert deps.download_orchestrator.download_calls[0][1] == "high.flac"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -175,8 +175,8 @@ def test_already_tried_source_skipped():
|
||||||
dc.attempt_download_with_candidates("t3", candidates, track, batch_id=None, deps=deps)
|
dc.attempt_download_with_candidates("t3", candidates, track, batch_id=None, deps=deps)
|
||||||
|
|
||||||
# First candidate skipped (already used), second one tried
|
# First candidate skipped (already used), second one tried
|
||||||
assert len(deps.soulseek_client.download_calls) == 1
|
assert len(deps.download_orchestrator.download_calls) == 1
|
||||||
assert deps.soulseek_client.download_calls[0][1] == "fresh.flac"
|
assert deps.download_orchestrator.download_calls[0][1] == "fresh.flac"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -196,7 +196,7 @@ def test_blacklisted_source_skipped():
|
||||||
|
|
||||||
dc.attempt_download_with_candidates("t4", candidates, track, batch_id=None, deps=deps)
|
dc.attempt_download_with_candidates("t4", candidates, track, batch_id=None, deps=deps)
|
||||||
|
|
||||||
assert deps.soulseek_client.download_calls[0][1] == "ok.flac"
|
assert deps.download_orchestrator.download_calls[0][1] == "ok.flac"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -213,7 +213,7 @@ def test_cancellation_before_attempt_returns_false():
|
||||||
result = dc.attempt_download_with_candidates("t5", candidates, track, batch_id=None, deps=deps)
|
result = dc.attempt_download_with_candidates("t5", candidates, track, batch_id=None, deps=deps)
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
assert deps.soulseek_client.download_calls == []
|
assert deps.download_orchestrator.download_calls == []
|
||||||
|
|
||||||
|
|
||||||
def test_task_deleted_returns_false():
|
def test_task_deleted_returns_false():
|
||||||
|
|
@ -238,7 +238,7 @@ def test_active_download_id_skips_new_download():
|
||||||
dc.attempt_download_with_candidates("t6", candidates, track, batch_id=None, deps=deps)
|
dc.attempt_download_with_candidates("t6", candidates, track, batch_id=None, deps=deps)
|
||||||
|
|
||||||
# Both candidates skipped (download_id already present)
|
# Both candidates skipped (download_id already present)
|
||||||
assert deps.soulseek_client.download_calls == []
|
assert deps.download_orchestrator.download_calls == []
|
||||||
|
|
||||||
|
|
||||||
def test_cancellation_after_download_starts_calls_cancel_and_lifecycle():
|
def test_cancellation_after_download_starts_calls_cancel_and_lifecycle():
|
||||||
|
|
@ -266,7 +266,7 @@ def test_cancellation_after_download_starts_calls_cancel_and_lifecycle():
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
# cancel_download was called for the in-flight transfer
|
# cancel_download was called for the in-flight transfer
|
||||||
assert deps.soulseek_client.cancel_calls
|
assert deps.download_orchestrator.cancel_calls
|
||||||
# on_download_completed fired with success=False to free the worker slot
|
# on_download_completed fired with success=False to free the worker slot
|
||||||
assert completion_calls == [("b7", "t7", False)]
|
assert completion_calls == [("b7", "t7", False)]
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ def test_cancellation_after_download_starts_calls_cancel_and_lifecycle():
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_all_candidates_failed_returns_false():
|
def test_all_candidates_failed_returns_false():
|
||||||
"""If soulseek_client.download returns None (failure) for all candidates, returns False."""
|
"""If download_orchestrator.download returns None (failure) for all candidates, returns False."""
|
||||||
soulseek = _FakeSoulseek(download_id=None)
|
soulseek = _FakeSoulseek(download_id=None)
|
||||||
deps = _build_deps(soulseek=soulseek)
|
deps = _build_deps(soulseek=soulseek)
|
||||||
_seed_task("t8")
|
_seed_task("t8")
|
||||||
|
|
@ -411,5 +411,5 @@ def test_candidates_with_equal_confidence_both_tried():
|
||||||
dc.attempt_download_with_candidates("t13", candidates, track, batch_id=None, deps=deps)
|
dc.attempt_download_with_candidates("t13", candidates, track, batch_id=None, deps=deps)
|
||||||
|
|
||||||
# First one wins — second never tried because download succeeded
|
# First one wins — second never tried because download succeeded
|
||||||
assert len(deps.soulseek_client.download_calls) == 1
|
assert len(deps.download_orchestrator.download_calls) == 1
|
||||||
assert deps.soulseek_client.download_calls[0][1] == "a.flac"
|
assert deps.download_orchestrator.download_calls[0][1] == "a.flac"
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ def _build_deps(
|
||||||
):
|
):
|
||||||
return mw.MasterDeps(
|
return mw.MasterDeps(
|
||||||
config_manager=config or _FakeConfig(),
|
config_manager=config or _FakeConfig(),
|
||||||
soulseek_client=soulseek or _FakeSoulseekWrapper(_FakeSoulseek()),
|
download_orchestrator=soulseek or _FakeSoulseekWrapper(_FakeSoulseek()),
|
||||||
run_async=run_async or _make_run_async(),
|
run_async=run_async or _make_run_async(),
|
||||||
mb_worker=mb_worker,
|
mb_worker=mb_worker,
|
||||||
mb_release_cache=mb_release_cache if mb_release_cache is not None else {},
|
mb_release_cache=mb_release_cache if mb_release_cache is not None else {},
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class _Recorder:
|
||||||
def _build_deps(
|
def _build_deps(
|
||||||
*,
|
*,
|
||||||
config=None,
|
config=None,
|
||||||
soulseek_client=None,
|
download_orchestrator=None,
|
||||||
run_async=None,
|
run_async=None,
|
||||||
docker_resolve_path=None,
|
docker_resolve_path=None,
|
||||||
extract_filename=None,
|
extract_filename=None,
|
||||||
|
|
@ -64,7 +64,7 @@ def _build_deps(
|
||||||
rec = _Recorder()
|
rec = _Recorder()
|
||||||
return pp.PostProcessDeps(
|
return pp.PostProcessDeps(
|
||||||
config_manager=config or _FakeConfig(),
|
config_manager=config or _FakeConfig(),
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
run_async=run_async or (lambda c: None),
|
run_async=run_async or (lambda c: None),
|
||||||
docker_resolve_path=docker_resolve_path or (lambda p: p),
|
docker_resolve_path=docker_resolve_path or (lambda p: p),
|
||||||
extract_filename=extract_filename or (lambda f: os.path.basename(f) if f else ''),
|
extract_filename=extract_filename or (lambda f: os.path.basename(f) if f else ''),
|
||||||
|
|
@ -315,7 +315,7 @@ def test_youtube_task_uses_get_download_status_to_resolve_path(monkeypatch):
|
||||||
monkeypatch.setattr(pp.os.path, 'exists', lambda p: p == '/downloads/Money.mp3')
|
monkeypatch.setattr(pp.os.path, 'exists', lambda p: p == '/downloads/Money.mp3')
|
||||||
|
|
||||||
deps, rec = _build_deps(
|
deps, rec = _build_deps(
|
||||||
soulseek_client=_FakeYTClient(),
|
download_orchestrator=_FakeYTClient(),
|
||||||
run_async=lambda coro: coro, # not async — direct call
|
run_async=lambda coro: coro, # not async — direct call
|
||||||
)
|
)
|
||||||
pp.run_post_processing_worker('t1', 'b1', deps)
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ def _build_deps(
|
||||||
):
|
):
|
||||||
rec = _Recorder()
|
rec = _Recorder()
|
||||||
return tw.TaskWorkerDeps(
|
return tw.TaskWorkerDeps(
|
||||||
soulseek_client=soulseek or _FakeClient(),
|
download_orchestrator=soulseek or _FakeClient(),
|
||||||
matching_engine=matching or _FakeMatchEngine(),
|
matching_engine=matching or _FakeMatchEngine(),
|
||||||
run_async=_sync_run_async,
|
run_async=_sync_run_async,
|
||||||
try_source_reuse=try_source_reuse,
|
try_source_reuse=try_source_reuse,
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ def _build_deps(**overrides):
|
||||||
spotify_client=None,
|
spotify_client=None,
|
||||||
hydrabase_client=None,
|
hydrabase_client=None,
|
||||||
hydrabase_worker=None,
|
hydrabase_worker=None,
|
||||||
soulseek_client=None,
|
download_orchestrator=None,
|
||||||
fix_artist_image_url=lambda u: f'FIXED::{u}' if u else None,
|
fix_artist_image_url=lambda u: f'FIXED::{u}' if u else None,
|
||||||
is_hydrabase_active=lambda: False,
|
is_hydrabase_active=lambda: False,
|
||||||
get_metadata_fallback_source=lambda: 'spotify',
|
get_metadata_fallback_source=lambda: 'spotify',
|
||||||
|
|
@ -470,19 +470,19 @@ class _FakeSoulseekWithYT:
|
||||||
|
|
||||||
def test_resolve_youtube_videos_returns_subclient():
|
def test_resolve_youtube_videos_returns_subclient():
|
||||||
yt = _FakeYouTube()
|
yt = _FakeYouTube()
|
||||||
deps = _build_deps(soulseek_client=_FakeSoulseekWithYT(yt))
|
deps = _build_deps(download_orchestrator=_FakeSoulseekWithYT(yt))
|
||||||
assert orchestrator.resolve_youtube_videos_client(deps) is yt
|
assert orchestrator.resolve_youtube_videos_client(deps) is yt
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_youtube_videos_no_soulseek_returns_none():
|
def test_resolve_youtube_videos_no_soulseek_returns_none():
|
||||||
deps = _build_deps(soulseek_client=None)
|
deps = _build_deps(download_orchestrator=None)
|
||||||
assert orchestrator.resolve_youtube_videos_client(deps) is None
|
assert orchestrator.resolve_youtube_videos_client(deps) is None
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_youtube_videos_no_youtube_attr_returns_none():
|
def test_resolve_youtube_videos_no_youtube_attr_returns_none():
|
||||||
class _NoYT:
|
class _NoYT:
|
||||||
pass
|
pass
|
||||||
deps = _build_deps(soulseek_client=_NoYT())
|
deps = _build_deps(download_orchestrator=_NoYT())
|
||||||
assert orchestrator.resolve_youtube_videos_client(deps) is None
|
assert orchestrator.resolve_youtube_videos_client(deps) is None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ def test_stream_finds_match_on_first_query():
|
||||||
result = stream.stream_search_track(
|
result = stream.stream_search_track(
|
||||||
track_name='Money', artist_name='Pink Floyd', album_name=None,
|
track_name='Money', artist_name='Pink Floyd', album_name=None,
|
||||||
duration_ms=180000,
|
duration_ms=180000,
|
||||||
config_manager=cfg, soulseek_client=soul, matching_engine=engine,
|
config_manager=cfg, download_orchestrator=soul, matching_engine=engine,
|
||||||
run_async=_run_async,
|
run_async=_run_async,
|
||||||
)
|
)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -190,7 +190,7 @@ def test_stream_walks_to_second_query_on_no_match():
|
||||||
result = stream.stream_search_track(
|
result = stream.stream_search_track(
|
||||||
track_name='Money (Remastered)', artist_name='Pink Floyd', album_name=None,
|
track_name='Money (Remastered)', artist_name='Pink Floyd', album_name=None,
|
||||||
duration_ms=180000,
|
duration_ms=180000,
|
||||||
config_manager=cfg, soulseek_client=soul, matching_engine=engine,
|
config_manager=cfg, download_orchestrator=soul, matching_engine=engine,
|
||||||
run_async=_run_async,
|
run_async=_run_async,
|
||||||
)
|
)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -209,7 +209,7 @@ def test_stream_returns_none_when_no_matches():
|
||||||
result = stream.stream_search_track(
|
result = stream.stream_search_track(
|
||||||
track_name='Money', artist_name='Pink Floyd', album_name=None,
|
track_name='Money', artist_name='Pink Floyd', album_name=None,
|
||||||
duration_ms=180000,
|
duration_ms=180000,
|
||||||
config_manager=cfg, soulseek_client=soul, matching_engine=engine,
|
config_manager=cfg, download_orchestrator=soul, matching_engine=engine,
|
||||||
run_async=_run_async,
|
run_async=_run_async,
|
||||||
)
|
)
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
@ -217,7 +217,7 @@ def test_stream_returns_none_when_no_matches():
|
||||||
|
|
||||||
def test_stream_falls_back_to_default_soulseek_when_no_direct_client():
|
def test_stream_falls_back_to_default_soulseek_when_no_direct_client():
|
||||||
# Effective mode = 'youtube' (coerced from soulseek), but soul.youtube is None
|
# Effective mode = 'youtube' (coerced from soulseek), but soul.youtube is None
|
||||||
# → code falls through to soulseek_client.search directly. Streaming-mode
|
# → code falls through to download_orchestrator.search directly. Streaming-mode
|
||||||
# query gen → "Pink Floyd Money".
|
# query gen → "Pink Floyd Money".
|
||||||
soul = _FakeSoulseek(results_per_query={'Pink Floyd Money': ([object()], [])})
|
soul = _FakeSoulseek(results_per_query={'Pink Floyd Money': ([object()], [])})
|
||||||
cfg = _FakeConfig({
|
cfg = _FakeConfig({
|
||||||
|
|
@ -229,7 +229,7 @@ def test_stream_falls_back_to_default_soulseek_when_no_direct_client():
|
||||||
result = stream.stream_search_track(
|
result = stream.stream_search_track(
|
||||||
track_name='Money', artist_name='Pink Floyd', album_name=None,
|
track_name='Money', artist_name='Pink Floyd', album_name=None,
|
||||||
duration_ms=180000,
|
duration_ms=180000,
|
||||||
config_manager=cfg, soulseek_client=soul, matching_engine=engine,
|
config_manager=cfg, download_orchestrator=soul, matching_engine=engine,
|
||||||
run_async=_run_async,
|
run_async=_run_async,
|
||||||
)
|
)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -257,7 +257,7 @@ def test_stream_continues_past_per_query_exception():
|
||||||
result = stream.stream_search_track(
|
result = stream.stream_search_track(
|
||||||
track_name='Money (Live)', artist_name='Pink Floyd', album_name=None,
|
track_name='Money (Live)', artist_name='Pink Floyd', album_name=None,
|
||||||
duration_ms=180000,
|
duration_ms=180000,
|
||||||
config_manager=cfg, soulseek_client=soul, matching_engine=engine,
|
config_manager=cfg, download_orchestrator=soul, matching_engine=engine,
|
||||||
run_async=_run_async,
|
run_async=_run_async,
|
||||||
)
|
)
|
||||||
# First query raised, second succeeded
|
# First query raised, second succeeded
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from core.streaming import prepare as sp
|
||||||
|
|
||||||
|
|
||||||
class _FakeSoulseek:
|
class _FakeSoulseek:
|
||||||
"""Minimal soulseek_client stub for the stream-prep worker."""
|
"""Minimal download_orchestrator stub for the stream-prep worker."""
|
||||||
|
|
||||||
def __init__(self, *, download_id='dl-1', all_downloads=None):
|
def __init__(self, *, download_id='dl-1', all_downloads=None):
|
||||||
self._download_id = download_id
|
self._download_id = download_id
|
||||||
|
|
@ -43,7 +43,7 @@ def _build_deps(
|
||||||
state = state if state is not None else {}
|
state = state if state is not None else {}
|
||||||
deps = sp.PrepareStreamDeps(
|
deps = sp.PrepareStreamDeps(
|
||||||
config_manager=type('C', (), {'get': lambda self, k, d=None: d})(),
|
config_manager=type('C', (), {'get': lambda self, k, d=None: d})(),
|
||||||
soulseek_client=soulseek or _FakeSoulseek(),
|
download_orchestrator=soulseek or _FakeSoulseek(),
|
||||||
stream_lock=threading.Lock(),
|
stream_lock=threading.Lock(),
|
||||||
project_root=project_root,
|
project_root=project_root,
|
||||||
docker_resolve_path=lambda p: p,
|
docker_resolve_path=lambda p: p,
|
||||||
|
|
@ -107,7 +107,7 @@ def test_stream_folder_cleared_before_download(tmp_path):
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_download_returns_none_marks_error(tmp_path):
|
def test_download_returns_none_marks_error(tmp_path):
|
||||||
"""soulseek_client.download() returning None → state.error."""
|
"""download_orchestrator.download() returning None → state.error."""
|
||||||
sk = _FakeSoulseek(download_id=None)
|
sk = _FakeSoulseek(download_id=None)
|
||||||
deps = _build_deps(soulseek=sk, project_root=str(tmp_path))
|
deps = _build_deps(soulseek=sk, project_root=str(tmp_path))
|
||||||
|
|
||||||
|
|
|
||||||
201
web_server.py
201
web_server.py
|
|
@ -570,7 +570,7 @@ IS_SHUTTING_DOWN = False
|
||||||
# Each client is initialized independently so one failure doesn't take down everything.
|
# Each client is initialized independently so one failure doesn't take down everything.
|
||||||
# Previously, a single exception set ALL clients to None, breaking the entire app.
|
# Previously, a single exception set ALL clients to None, breaking the entire app.
|
||||||
logger.info("Initializing SoulSync services for Web UI...")
|
logger.info("Initializing SoulSync services for Web UI...")
|
||||||
spotify_client = plex_client = jellyfin_client = navidrome_client = soulsync_library_client = soulseek_client = tidal_client = matching_engine = sync_service = web_scan_manager = None
|
spotify_client = plex_client = jellyfin_client = navidrome_client = soulsync_library_client = download_orchestrator = tidal_client = matching_engine = sync_service = web_scan_manager = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
spotify_client = get_spotify_client()
|
spotify_client = get_spotify_client()
|
||||||
|
|
@ -604,7 +604,7 @@ except Exception as e:
|
||||||
logger.error(f" SoulSync library client failed to initialize: {e}")
|
logger.error(f" SoulSync library client failed to initialize: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
soulseek_client = DownloadOrchestrator()
|
download_orchestrator = DownloadOrchestrator()
|
||||||
logger.info(" Download orchestrator initialized")
|
logger.info(" Download orchestrator initialized")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f" Download orchestrator failed to initialize: {e}")
|
logger.error(f" Download orchestrator failed to initialize: {e}")
|
||||||
|
|
@ -622,7 +622,7 @@ except Exception as e:
|
||||||
logger.error(f" Matching engine failed to initialize: {e}")
|
logger.error(f" Matching engine failed to initialize: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sync_service = PlaylistSyncService(spotify_client, plex_client, soulseek_client, jellyfin_client, navidrome_client)
|
sync_service = PlaylistSyncService(spotify_client, plex_client, download_orchestrator, jellyfin_client, navidrome_client)
|
||||||
logger.info(" Playlist sync service initialized")
|
logger.info(" Playlist sync service initialized")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f" Playlist sync service failed to initialize: {e}")
|
logger.error(f" Playlist sync service failed to initialize: {e}")
|
||||||
|
|
@ -630,8 +630,8 @@ except Exception as e:
|
||||||
# Inject shutdown check callback into every download source that
|
# Inject shutdown check callback into every download source that
|
||||||
# accepts one. Generic dispatch via the registry — no per-source
|
# accepts one. Generic dispatch via the registry — no per-source
|
||||||
# attribute reaches needed.
|
# attribute reaches needed.
|
||||||
if soulseek_client and hasattr(soulseek_client, 'registry'):
|
if download_orchestrator and hasattr(download_orchestrator, 'registry'):
|
||||||
for _src_name, _src_client in soulseek_client.registry.all_plugins():
|
for _src_name, _src_client in download_orchestrator.registry.all_plugins():
|
||||||
if _src_client is not None and hasattr(_src_client, 'set_shutdown_check'):
|
if _src_client is not None and hasattr(_src_client, 'set_shutdown_check'):
|
||||||
try:
|
try:
|
||||||
_src_client.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
|
_src_client.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
|
||||||
|
|
@ -1993,9 +1993,9 @@ def _register_automation_handlers():
|
||||||
hybrid_order = config_manager.get('download_source.hybrid_order', ['hifi', 'youtube', 'soulseek'])
|
hybrid_order = config_manager.get('download_source.hybrid_order', ['hifi', 'youtube', 'soulseek'])
|
||||||
soulseek_active = (dl_mode == 'soulseek' or
|
soulseek_active = (dl_mode == 'soulseek' or
|
||||||
(dl_mode == 'hybrid' and 'soulseek' in hybrid_order))
|
(dl_mode == 'hybrid' and 'soulseek' in hybrid_order))
|
||||||
# soulseek_client is a DownloadOrchestrator; the real client lives on
|
# Reach the underlying SoulseekClient via the orchestrator's
|
||||||
# .soulseek. Match the getattr pattern used at the other call sites.
|
# generic accessor.
|
||||||
slskd = getattr(soulseek_client, 'soulseek', None) if soulseek_client else None
|
slskd = download_orchestrator.client('soulseek') if download_orchestrator else None
|
||||||
if not soulseek_active or not slskd or not slskd.base_url:
|
if not soulseek_active or not slskd or not slskd.base_url:
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
log_line='Soulseek not active — skipped', log_type='skip')
|
log_line='Soulseek not active — skipped', log_type='skip')
|
||||||
|
|
@ -2005,7 +2005,7 @@ def _register_automation_handlers():
|
||||||
log_line='Auto-clear disabled in settings', log_type='skip')
|
log_line='Auto-clear disabled in settings', log_type='skip')
|
||||||
return {'status': 'skipped'}
|
return {'status': 'skipped'}
|
||||||
try:
|
try:
|
||||||
success = run_async(soulseek_client.maintain_search_history_with_buffer(
|
success = run_async(download_orchestrator.maintain_search_history_with_buffer(
|
||||||
keep_searches=50, trigger_threshold=200
|
keep_searches=50, trigger_threshold=200
|
||||||
))
|
))
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -2041,7 +2041,7 @@ def _register_automation_handlers():
|
||||||
log_line='Skipped — downloads active', log_type='skip')
|
log_line='Skipped — downloads active', log_type='skip')
|
||||||
return {'status': 'completed'}
|
return {'status': 'completed'}
|
||||||
|
|
||||||
run_async(soulseek_client.clear_all_completed_downloads())
|
run_async(download_orchestrator.clear_all_completed_downloads())
|
||||||
if not has_post_processing:
|
if not has_post_processing:
|
||||||
_sweep_empty_download_directories()
|
_sweep_empty_download_directories()
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
|
|
@ -2096,7 +2096,7 @@ def _register_automation_handlers():
|
||||||
log_line='Download queue: skipped (active batches)', log_type='skip')
|
log_line='Download queue: skipped (active batches)', log_type='skip')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
run_async(soulseek_client.clear_all_completed_downloads())
|
run_async(download_orchestrator.clear_all_completed_downloads())
|
||||||
steps.append('Download queue: cleared')
|
steps.append('Download queue: cleared')
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
log_line='Download queue: cleared', log_type='success')
|
log_line='Download queue: cleared', log_type='success')
|
||||||
|
|
@ -2154,7 +2154,7 @@ def _register_automation_handlers():
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
log_line='Search cleanup: disabled in settings', log_type='skip')
|
log_line='Search cleanup: disabled in settings', log_type='skip')
|
||||||
else:
|
else:
|
||||||
run_async(soulseek_client.maintain_search_history_with_buffer(
|
run_async(download_orchestrator.maintain_search_history_with_buffer(
|
||||||
keep_searches=50, trigger_threshold=200
|
keep_searches=50, trigger_threshold=200
|
||||||
))
|
))
|
||||||
steps.append('Search history: cleaned')
|
steps.append('Search history: cleaned')
|
||||||
|
|
@ -2286,7 +2286,7 @@ def _register_automation_handlers():
|
||||||
if automation_id:
|
if automation_id:
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
phase='Searching', log_line=f'Searching: {query}', log_type='info')
|
phase='Searching', log_line=f'Searching: {query}', log_type='info')
|
||||||
result = run_async(soulseek_client.search_and_download_best(query))
|
result = run_async(download_orchestrator.search_and_download_best(query))
|
||||||
if result:
|
if result:
|
||||||
if automation_id:
|
if automation_id:
|
||||||
_update_automation_progress(automation_id,
|
_update_automation_progress(automation_id,
|
||||||
|
|
@ -2348,7 +2348,7 @@ try:
|
||||||
app.register_blueprint(api_bp, url_prefix='/api/v1')
|
app.register_blueprint(api_bp, url_prefix='/api/v1')
|
||||||
app.soulsync = {
|
app.soulsync = {
|
||||||
'spotify_client': spotify_client,
|
'spotify_client': spotify_client,
|
||||||
'soulseek_client': soulseek_client,
|
'download_orchestrator': download_orchestrator,
|
||||||
'tidal_client': tidal_client,
|
'tidal_client': tidal_client,
|
||||||
'matching_engine': matching_engine,
|
'matching_engine': matching_engine,
|
||||||
'config_manager': config_manager,
|
'config_manager': config_manager,
|
||||||
|
|
@ -2453,8 +2453,9 @@ def get_cached_transfer_data():
|
||||||
|
|
||||||
# First, get Soulseek downloads from API
|
# First, get Soulseek downloads from API
|
||||||
transfers_data = None
|
transfers_data = None
|
||||||
if not soulseek_known_down and soulseek_client and getattr(soulseek_client, 'soulseek', None) and soulseek_client.client("soulseek").base_url:
|
_slsk = download_orchestrator.client("soulseek") if download_orchestrator else None
|
||||||
transfers_data = run_async(soulseek_client._make_request('GET', 'transfers/downloads'))
|
if not soulseek_known_down and _slsk and _slsk.base_url:
|
||||||
|
transfers_data = run_async(download_orchestrator._make_request('GET', 'transfers/downloads'))
|
||||||
if transfers_data:
|
if transfers_data:
|
||||||
all_transfers = []
|
all_transfers = []
|
||||||
for user_data in transfers_data:
|
for user_data in transfers_data:
|
||||||
|
|
@ -2477,9 +2478,9 @@ def get_cached_transfer_data():
|
||||||
all_downloads = []
|
all_downloads = []
|
||||||
# Generic dispatch — engine returns active downloads
|
# Generic dispatch — engine returns active downloads
|
||||||
# across every plugin in one call, no per-source iteration.
|
# across every plugin in one call, no per-source iteration.
|
||||||
if soulseek_client and hasattr(soulseek_client, 'engine'):
|
if download_orchestrator and hasattr(download_orchestrator, 'engine'):
|
||||||
try:
|
try:
|
||||||
all_downloads = run_async(soulseek_client.engine.get_all_downloads())
|
all_downloads = run_async(download_orchestrator.engine.get_all_downloads())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
for download in all_downloads:
|
for download in all_downloads:
|
||||||
|
|
@ -3052,7 +3053,7 @@ def _build_prepare_stream_deps():
|
||||||
|
|
||||||
return _streaming_prepare.PrepareStreamDeps(
|
return _streaming_prepare.PrepareStreamDeps(
|
||||||
config_manager=config_manager,
|
config_manager=config_manager,
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
stream_lock=stream_lock,
|
stream_lock=stream_lock,
|
||||||
project_root=os.path.dirname(os.path.abspath(__file__)),
|
project_root=os.path.dirname(os.path.abspath(__file__)),
|
||||||
docker_resolve_path=docker_resolve_path,
|
docker_resolve_path=docker_resolve_path,
|
||||||
|
|
@ -3499,10 +3500,10 @@ def get_status():
|
||||||
if is_serverless:
|
if is_serverless:
|
||||||
soulseek_status = True
|
soulseek_status = True
|
||||||
soulseek_response_time = 0
|
soulseek_response_time = 0
|
||||||
elif soulseek_relevant and soulseek_client:
|
elif soulseek_relevant and download_orchestrator:
|
||||||
soulseek_start = time.time()
|
soulseek_start = time.time()
|
||||||
try:
|
try:
|
||||||
soulseek_status = run_async(soulseek_client.check_connection())
|
soulseek_status = run_async(download_orchestrator.check_connection())
|
||||||
except Exception:
|
except Exception:
|
||||||
soulseek_status = False
|
soulseek_status = False
|
||||||
soulseek_response_time = (time.time() - soulseek_start) * 1000
|
soulseek_response_time = (time.time() - soulseek_start) * 1000
|
||||||
|
|
@ -3915,7 +3916,7 @@ def _build_system_stats():
|
||||||
|
|
||||||
if soulseek_active and not soulseek_known_down:
|
if soulseek_active and not soulseek_known_down:
|
||||||
try:
|
try:
|
||||||
transfers_data = run_async(soulseek_client._make_request('GET', 'transfers/downloads'))
|
transfers_data = run_async(download_orchestrator._make_request('GET', 'transfers/downloads'))
|
||||||
if transfers_data:
|
if transfers_data:
|
||||||
for user_data in transfers_data:
|
for user_data in transfers_data:
|
||||||
if 'directories' in user_data:
|
if 'directories' in user_data:
|
||||||
|
|
@ -4141,11 +4142,11 @@ def handle_settings():
|
||||||
if navidrome_client:
|
if navidrome_client:
|
||||||
navidrome_client.reload_config()
|
navidrome_client.reload_config()
|
||||||
# Reload orchestrator settings (download source mode, hybrid_primary, etc.)
|
# Reload orchestrator settings (download source mode, hybrid_primary, etc.)
|
||||||
if soulseek_client:
|
if download_orchestrator:
|
||||||
soulseek_client.reload_settings()
|
download_orchestrator.reload_settings()
|
||||||
# Reload YouTube client settings (rate limiting, cookies)
|
# Reload YouTube client settings (rate limiting, cookies)
|
||||||
if hasattr(soulseek_client, 'youtube'):
|
if hasattr(download_orchestrator, 'youtube'):
|
||||||
soulseek_client.client("youtube").reload_settings()
|
download_orchestrator.client("youtube").reload_settings()
|
||||||
# FIX: Re-instantiate the global tidal_client to pick up new settings
|
# FIX: Re-instantiate the global tidal_client to pick up new settings
|
||||||
try:
|
try:
|
||||||
tidal_client = TidalClient()
|
tidal_client = TidalClient()
|
||||||
|
|
@ -4177,7 +4178,7 @@ def handle_settings():
|
||||||
data = dict(config_manager.config_data)
|
data = dict(config_manager.config_data)
|
||||||
# Include which download sources are configured so the UI can auto-disable unconfigured ones
|
# Include which download sources are configured so the UI can auto-disable unconfigured ones
|
||||||
try:
|
try:
|
||||||
data['_source_status'] = soulseek_client.get_source_status()
|
data['_source_status'] = download_orchestrator.get_source_status()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
@ -6728,7 +6729,7 @@ def _build_search_deps():
|
||||||
spotify_client=spotify_client,
|
spotify_client=spotify_client,
|
||||||
hydrabase_client=hydrabase_client,
|
hydrabase_client=hydrabase_client,
|
||||||
hydrabase_worker=hydrabase_worker,
|
hydrabase_worker=hydrabase_worker,
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
fix_artist_image_url=fix_artist_image_url,
|
fix_artist_image_url=fix_artist_image_url,
|
||||||
is_hydrabase_active=_is_hydrabase_active,
|
is_hydrabase_active=_is_hydrabase_active,
|
||||||
get_metadata_fallback_source=_get_metadata_fallback_source,
|
get_metadata_fallback_source=_get_metadata_fallback_source,
|
||||||
|
|
@ -6754,7 +6755,7 @@ def search_music():
|
||||||
add_activity_item("", "Search Started", f"'{query}'", "Now")
|
add_activity_item("", "Search Started", f"'{query}'", "Now")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = _search_basic.run_basic_soulseek_search(query, soulseek_client, run_async)
|
results = _search_basic.run_basic_soulseek_search(query, download_orchestrator, run_async)
|
||||||
add_activity_item("", "Search Complete", f"'{query}' - {len(results)} results", "Now")
|
add_activity_item("", "Search Complete", f"'{query}' - {len(results)} results", "Now")
|
||||||
return jsonify({"results": results})
|
return jsonify({"results": results})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -6809,7 +6810,7 @@ def enhanced_search_source(source_name):
|
||||||
|
|
||||||
When the requested source's client isn't available (Spotify unauthed,
|
When the requested source's client isn't available (Spotify unauthed,
|
||||||
Discogs missing token, Hydrabase disconnected, MusicBrainz import
|
Discogs missing token, Hydrabase disconnected, MusicBrainz import
|
||||||
failure, soulseek_client.client("youtube") missing), returns plain JSON
|
failure, download_orchestrator.client("youtube") missing), returns plain JSON
|
||||||
`{"artists":[],"albums":[],"tracks":[],"available":false}` to match
|
`{"artists":[],"albums":[],"tracks":[],"available":false}` to match
|
||||||
the original endpoint contract.
|
the original endpoint contract.
|
||||||
"""
|
"""
|
||||||
|
|
@ -6894,7 +6895,7 @@ def stream_enhanced_search_track():
|
||||||
album_name=album_name,
|
album_name=album_name,
|
||||||
duration_ms=duration_ms,
|
duration_ms=duration_ms,
|
||||||
config_manager=config_manager,
|
config_manager=config_manager,
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
matching_engine=matching_engine,
|
matching_engine=matching_engine,
|
||||||
run_async=run_async,
|
run_async=run_async,
|
||||||
)
|
)
|
||||||
|
|
@ -7033,7 +7034,7 @@ def download_music_video():
|
||||||
def _progress(pct):
|
def _progress(pct):
|
||||||
_music_video_downloads[video_id]['progress'] = round(pct, 1)
|
_music_video_downloads[video_id]['progress'] = round(pct, 1)
|
||||||
|
|
||||||
final_path = soulseek_client.client("youtube").download_music_video(video_url, output_path, progress_callback=_progress)
|
final_path = download_orchestrator.client("youtube").download_music_video(video_url, output_path, progress_callback=_progress)
|
||||||
|
|
||||||
if final_path and os.path.exists(final_path):
|
if final_path and os.path.exists(final_path):
|
||||||
_music_video_downloads[video_id]['status'] = 'completed'
|
_music_video_downloads[video_id]['status'] = 'completed'
|
||||||
|
|
@ -7092,7 +7093,7 @@ def start_download():
|
||||||
filename = track_data.get('filename')
|
filename = track_data.get('filename')
|
||||||
file_size = track_data.get('size', 0)
|
file_size = track_data.get('size', 0)
|
||||||
|
|
||||||
download_id = run_async(soulseek_client.download(
|
download_id = run_async(download_orchestrator.download(
|
||||||
username,
|
username,
|
||||||
filename,
|
filename,
|
||||||
file_size
|
file_size
|
||||||
|
|
@ -7141,7 +7142,7 @@ def start_download():
|
||||||
if not username or not filename:
|
if not username or not filename:
|
||||||
return jsonify({"error": "Missing username or filename."}), 400
|
return jsonify({"error": "Missing username or filename."}), 400
|
||||||
|
|
||||||
download_id = run_async(soulseek_client.download(username, filename, file_size))
|
download_id = run_async(download_orchestrator.download(username, filename, file_size))
|
||||||
logger.info(f"Download ID returned: {download_id}")
|
logger.info(f"Download ID returned: {download_id}")
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
|
|
@ -7337,7 +7338,7 @@ def get_download_status():
|
||||||
A robust status checker that correctly finds completed files by searching
|
A robust status checker that correctly finds completed files by searching
|
||||||
the entire download directory with fuzzy matching, mirroring the logic from downloads.py.
|
the entire download directory with fuzzy matching, mirroring the logic from downloads.py.
|
||||||
"""
|
"""
|
||||||
if not soulseek_client:
|
if not download_orchestrator:
|
||||||
return jsonify({"transfers": []})
|
return jsonify({"transfers": []})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -7346,7 +7347,7 @@ def get_download_status():
|
||||||
soulseek_known_down = not _status_cache.get('soulseek', {}).get('connected', True)
|
soulseek_known_down = not _status_cache.get('soulseek', {}).get('connected', True)
|
||||||
transfers_data = None
|
transfers_data = None
|
||||||
if not soulseek_known_down:
|
if not soulseek_known_down:
|
||||||
transfers_data = run_async(soulseek_client._make_request('GET', 'transfers/downloads'))
|
transfers_data = run_async(download_orchestrator._make_request('GET', 'transfers/downloads'))
|
||||||
|
|
||||||
# Don't return early if no Soulseek transfers - YouTube/Tidal downloads need to be checked too!
|
# Don't return early if no Soulseek transfers - YouTube/Tidal downloads need to be checked too!
|
||||||
all_transfers = []
|
all_transfers = []
|
||||||
|
|
@ -7414,7 +7415,7 @@ def get_download_status():
|
||||||
transfer_id = file_info.get('id')
|
transfer_id = file_info.get('id')
|
||||||
if transfer_id:
|
if transfer_id:
|
||||||
try:
|
try:
|
||||||
run_async(soulseek_client.cancel_download(str(transfer_id), username, remove=True))
|
run_async(download_orchestrator.cancel_download(str(transfer_id), username, remove=True))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
_orphaned_download_keys.discard(context_key)
|
_orphaned_download_keys.discard(context_key)
|
||||||
|
|
@ -7526,7 +7527,7 @@ def get_download_status():
|
||||||
|
|
||||||
# Also include YouTube/Tidal downloads in the response
|
# Also include YouTube/Tidal downloads in the response
|
||||||
try:
|
try:
|
||||||
all_streaming_downloads = run_async(soulseek_client.get_all_downloads())
|
all_streaming_downloads = run_async(download_orchestrator.get_all_downloads())
|
||||||
|
|
||||||
for download in all_streaming_downloads:
|
for download in all_streaming_downloads:
|
||||||
if download.username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud'):
|
if download.username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud'):
|
||||||
|
|
@ -7658,7 +7659,7 @@ def cancel_download():
|
||||||
return jsonify({"success": False, "error": "Missing download_id or username."}), 400
|
return jsonify({"success": False, "error": "Missing download_id or username."}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = _downloads_cancel.cancel_single_download(soulseek_client, run_async, download_id, username)
|
success = _downloads_cancel.cancel_single_download(download_orchestrator, run_async, download_id, username)
|
||||||
if success:
|
if success:
|
||||||
return jsonify({"success": True, "message": "Download cancelled."})
|
return jsonify({"success": True, "message": "Download cancelled."})
|
||||||
return jsonify({"success": False, "error": "Failed to cancel download via slskd."}), 500
|
return jsonify({"success": False, "error": "Failed to cancel download via slskd."}), 500
|
||||||
|
|
@ -7672,7 +7673,7 @@ def cancel_all_downloads():
|
||||||
"""Cancel all active downloads from slskd, then clear completed ones."""
|
"""Cancel all active downloads from slskd, then clear completed ones."""
|
||||||
try:
|
try:
|
||||||
success, msg = _downloads_cancel.cancel_all_active(
|
success, msg = _downloads_cancel.cancel_all_active(
|
||||||
soulseek_client, run_async, _sweep_empty_download_directories,
|
download_orchestrator, run_async, _sweep_empty_download_directories,
|
||||||
)
|
)
|
||||||
if success:
|
if success:
|
||||||
return jsonify({"success": True, "message": msg})
|
return jsonify({"success": True, "message": msg})
|
||||||
|
|
@ -7687,7 +7688,7 @@ def clear_finished_downloads():
|
||||||
"""Clear all terminal (completed, cancelled, failed) downloads from slskd."""
|
"""Clear all terminal (completed, cancelled, failed) downloads from slskd."""
|
||||||
try:
|
try:
|
||||||
success = _downloads_cancel.clear_finished_active(
|
success = _downloads_cancel.clear_finished_active(
|
||||||
soulseek_client, run_async, _sweep_empty_download_directories,
|
download_orchestrator, run_async, _sweep_empty_download_directories,
|
||||||
)
|
)
|
||||||
if success:
|
if success:
|
||||||
return jsonify({"success": True, "message": "Finished downloads cleared."})
|
return jsonify({"success": True, "message": "Finished downloads cleared."})
|
||||||
|
|
@ -8074,7 +8075,7 @@ def clear_all_searches():
|
||||||
Clear all searches from slskd search history.
|
Clear all searches from slskd search history.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
success = run_async(soulseek_client.clear_all_searches())
|
success = run_async(download_orchestrator.clear_all_searches())
|
||||||
if success:
|
if success:
|
||||||
add_activity_item("", "Search Cleanup", "All search history cleared manually", "Now")
|
add_activity_item("", "Search Cleanup", "All search history cleared manually", "Now")
|
||||||
return jsonify({"success": True, "message": "All searches cleared."})
|
return jsonify({"success": True, "message": "All searches cleared."})
|
||||||
|
|
@ -8094,7 +8095,7 @@ def maintain_search_history():
|
||||||
keep_searches = data.get('keep_searches', 50)
|
keep_searches = data.get('keep_searches', 50)
|
||||||
trigger_threshold = data.get('trigger_threshold', 200)
|
trigger_threshold = data.get('trigger_threshold', 200)
|
||||||
|
|
||||||
success = run_async(soulseek_client.maintain_search_history_with_buffer(
|
success = run_async(download_orchestrator.maintain_search_history_with_buffer(
|
||||||
keep_searches=keep_searches, trigger_threshold=trigger_threshold
|
keep_searches=keep_searches, trigger_threshold=trigger_threshold
|
||||||
))
|
))
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -11674,7 +11675,7 @@ def redownload_search_sources(track_id):
|
||||||
# Get all available download source clients
|
# Get all available download source clients
|
||||||
download_clients = {}
|
download_clients = {}
|
||||||
try:
|
try:
|
||||||
orch = soulseek_client # The download orchestrator
|
orch = download_orchestrator # The download orchestrator
|
||||||
if hasattr(orch, 'soulseek') and orch.soulseek:
|
if hasattr(orch, 'soulseek') and orch.soulseek:
|
||||||
if not (hasattr(orch.soulseek, 'is_configured') and not orch.soulseek.is_configured()):
|
if not (hasattr(orch.soulseek, 'is_configured') and not orch.soulseek.is_configured()):
|
||||||
download_clients['soulseek'] = orch.soulseek
|
download_clients['soulseek'] = orch.soulseek
|
||||||
|
|
@ -11698,7 +11699,7 @@ def redownload_search_sources(track_id):
|
||||||
|
|
||||||
if not download_clients:
|
if not download_clients:
|
||||||
# Fallback: use orchestrator directly
|
# Fallback: use orchestrator directly
|
||||||
download_clients = {'default': soulseek_client}
|
download_clients = {'default': download_orchestrator}
|
||||||
|
|
||||||
logger.info(f"[Redownload] Streaming search across {len(download_clients)} sources: {list(download_clients.keys())}")
|
logger.info(f"[Redownload] Streaming search across {len(download_clients)} sources: {list(download_clients.keys())}")
|
||||||
|
|
||||||
|
|
@ -12660,7 +12661,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Start download
|
# Start download
|
||||||
download_id = run_async(soulseek_client.download(username, filename, size))
|
download_id = run_async(download_orchestrator.download(username, filename, size))
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
context_key = _make_context_key(username, filename)
|
context_key = _make_context_key(username, filename)
|
||||||
|
|
@ -12708,7 +12709,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
|
||||||
if not username or not filename:
|
if not username or not filename:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
download_id = run_async(soulseek_client.download(username, filename, size))
|
download_id = run_async(download_orchestrator.download(username, filename, size))
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
context_key = _make_context_key(username, filename)
|
context_key = _make_context_key(username, filename)
|
||||||
|
|
@ -12816,7 +12817,7 @@ def _start_album_download_tasks(album_result, spotify_artist, spotify_album):
|
||||||
'disc_number': corrected_meta.get('disc_number', 1)
|
'disc_number': corrected_meta.get('disc_number', 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
download_id = run_async(soulseek_client.download(username, filename, size))
|
download_id = run_async(download_orchestrator.download(username, filename, size))
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
context_key = _make_context_key(username, filename)
|
context_key = _make_context_key(username, filename)
|
||||||
|
|
@ -12883,7 +12884,7 @@ def start_matched_download():
|
||||||
if not username or not filename:
|
if not username or not filename:
|
||||||
return jsonify({"success": False, "error": "Missing username or filename"}), 400
|
return jsonify({"success": False, "error": "Missing username or filename"}), 400
|
||||||
|
|
||||||
download_id = run_async(soulseek_client.download(username, filename, size))
|
download_id = run_async(download_orchestrator.download(username, filename, size))
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
context_key = _make_context_key(username, filename)
|
context_key = _make_context_key(username, filename)
|
||||||
|
|
@ -12943,7 +12944,7 @@ def start_matched_download():
|
||||||
download_payload['title'] = parsed_meta.get('title') or download_payload.get('title')
|
download_payload['title'] = parsed_meta.get('title') or download_payload.get('title')
|
||||||
download_payload['artist'] = parsed_meta.get('artist') or download_payload.get('artist')
|
download_payload['artist'] = parsed_meta.get('artist') or download_payload.get('artist')
|
||||||
|
|
||||||
download_id = run_async(soulseek_client.download(username, filename, size))
|
download_id = run_async(download_orchestrator.download(username, filename, size))
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
context_key = _make_context_key(username, filename)
|
context_key = _make_context_key(username, filename)
|
||||||
|
|
@ -12972,7 +12973,7 @@ def start_matched_download():
|
||||||
|
|
||||||
def _parse_filename_metadata(filename: str) -> dict:
|
def _parse_filename_metadata(filename: str) -> dict:
|
||||||
"""
|
"""
|
||||||
A direct port of the metadata parsing logic from the GUI's soulseek_client.py.
|
A direct port of the metadata parsing logic from the GUI's download_orchestrator.py.
|
||||||
This is the crucial missing step that cleans filenames BEFORE Spotify matching.
|
This is the crucial missing step that cleans filenames BEFORE Spotify matching.
|
||||||
"""
|
"""
|
||||||
return parse_filename_metadata(filename)
|
return parse_filename_metadata(filename)
|
||||||
|
|
@ -14148,7 +14149,7 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
|
||||||
genius_worker=genius_worker,
|
genius_worker=genius_worker,
|
||||||
spotify_enrichment_worker=spotify_enrichment_worker,
|
spotify_enrichment_worker=spotify_enrichment_worker,
|
||||||
itunes_enrichment_worker=itunes_enrichment_worker,
|
itunes_enrichment_worker=itunes_enrichment_worker,
|
||||||
hifi_client=soulseek_client.client("hifi") if soulseek_client else None,
|
hifi_client=download_orchestrator.client("hifi") if download_orchestrator else None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14253,7 +14254,7 @@ def _post_process_matched_download_with_verification(context_key, context, file_
|
||||||
genius_worker=genius_worker,
|
genius_worker=genius_worker,
|
||||||
spotify_enrichment_worker=spotify_enrichment_worker,
|
spotify_enrichment_worker=spotify_enrichment_worker,
|
||||||
itunes_enrichment_worker=itunes_enrichment_worker,
|
itunes_enrichment_worker=itunes_enrichment_worker,
|
||||||
hifi_client=soulseek_client.client("hifi") if soulseek_client else None,
|
hifi_client=download_orchestrator.client("hifi") if download_orchestrator else None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14377,7 +14378,7 @@ def _post_process_matched_download(context_key, context, file_path):
|
||||||
genius_worker=genius_worker,
|
genius_worker=genius_worker,
|
||||||
spotify_enrichment_worker=spotify_enrichment_worker,
|
spotify_enrichment_worker=spotify_enrichment_worker,
|
||||||
itunes_enrichment_worker=itunes_enrichment_worker,
|
itunes_enrichment_worker=itunes_enrichment_worker,
|
||||||
hifi_client=soulseek_client.client("hifi") if soulseek_client else None,
|
hifi_client=download_orchestrator.client("hifi") if download_orchestrator else None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16821,7 +16822,7 @@ def _build_master_deps():
|
||||||
|
|
||||||
return _downloads_master.MasterDeps(
|
return _downloads_master.MasterDeps(
|
||||||
config_manager=config_manager,
|
config_manager=config_manager,
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
run_async=run_async,
|
run_async=run_async,
|
||||||
mb_worker=mb_worker,
|
mb_worker=mb_worker,
|
||||||
mb_release_cache=mb_release_cache,
|
mb_release_cache=mb_release_cache,
|
||||||
|
|
@ -16859,7 +16860,7 @@ def _build_post_processing_deps():
|
||||||
"""Build the PostProcessDeps bundle from web_server.py globals on each call."""
|
"""Build the PostProcessDeps bundle from web_server.py globals on each call."""
|
||||||
return _downloads_post_processing.PostProcessDeps(
|
return _downloads_post_processing.PostProcessDeps(
|
||||||
config_manager=config_manager,
|
config_manager=config_manager,
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
run_async=run_async,
|
run_async=run_async,
|
||||||
docker_resolve_path=docker_resolve_path,
|
docker_resolve_path=docker_resolve_path,
|
||||||
extract_filename=extract_filename,
|
extract_filename=extract_filename,
|
||||||
|
|
@ -16886,7 +16887,7 @@ from core.downloads import task_worker as _downloads_task_worker
|
||||||
def _build_task_worker_deps():
|
def _build_task_worker_deps():
|
||||||
"""Build TaskWorkerDeps bundle from web_server.py globals on each call."""
|
"""Build TaskWorkerDeps bundle from web_server.py globals on each call."""
|
||||||
return _downloads_task_worker.TaskWorkerDeps(
|
return _downloads_task_worker.TaskWorkerDeps(
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
matching_engine=matching_engine,
|
matching_engine=matching_engine,
|
||||||
run_async=run_async,
|
run_async=run_async,
|
||||||
try_source_reuse=_try_source_reuse,
|
try_source_reuse=_try_source_reuse,
|
||||||
|
|
@ -16912,7 +16913,7 @@ from core.downloads import candidates as _downloads_candidates
|
||||||
def _build_candidates_deps():
|
def _build_candidates_deps():
|
||||||
"""Build the CandidatesDeps bundle from web_server.py globals on each call."""
|
"""Build the CandidatesDeps bundle from web_server.py globals on each call."""
|
||||||
return _downloads_candidates.CandidatesDeps(
|
return _downloads_candidates.CandidatesDeps(
|
||||||
soulseek_client=soulseek_client,
|
download_orchestrator=download_orchestrator,
|
||||||
spotify_client=spotify_client,
|
spotify_client=spotify_client,
|
||||||
run_async=run_async,
|
run_async=run_async,
|
||||||
get_database=get_database,
|
get_database=get_database,
|
||||||
|
|
@ -17077,7 +17078,7 @@ def _try_source_reuse(task_id, batch_id, track):
|
||||||
# Sort by confidence, filter by quality preference
|
# Sort by confidence, filter by quality preference
|
||||||
candidates.sort(key=lambda c: c.confidence, reverse=True)
|
candidates.sort(key=lambda c: c.confidence, reverse=True)
|
||||||
_sr.info(f"Found {len(candidates)} candidates above 0.70, best={candidates[0].confidence:.3f} ({candidates[0].filename})")
|
_sr.info(f"Found {len(candidates)} candidates above 0.70, best={candidates[0].confidence:.3f} ({candidates[0].filename})")
|
||||||
slsk = soulseek_client.client("soulseek") if hasattr(soulseek_client, 'soulseek') else soulseek_client
|
slsk = download_orchestrator.client("soulseek") if hasattr(download_orchestrator, 'soulseek') else download_orchestrator
|
||||||
filtered = slsk.filter_results_by_quality_preference(candidates)
|
filtered = slsk.filter_results_by_quality_preference(candidates)
|
||||||
if not filtered:
|
if not filtered:
|
||||||
_sr.info(f"Quality filter rejected all candidates for task {task_id}")
|
_sr.info(f"Quality filter rejected all candidates for task {task_id}")
|
||||||
|
|
@ -17156,8 +17157,8 @@ def _store_batch_source(batch_id, username, filename):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Access SoulseekClient directly (soulseek_client is DownloadOrchestrator)
|
# Access SoulseekClient directly (download_orchestrator is DownloadOrchestrator)
|
||||||
slsk = soulseek_client.client("soulseek") if hasattr(soulseek_client, 'soulseek') else soulseek_client
|
slsk = download_orchestrator.client("soulseek") if hasattr(download_orchestrator, 'soulseek') else download_orchestrator
|
||||||
_sr.info(f"Browsing {username}:{folder_path}...")
|
_sr.info(f"Browsing {username}:{folder_path}...")
|
||||||
files = run_async(slsk.browse_user_directory(username, folder_path))
|
files = run_async(slsk.browse_user_directory(username, folder_path))
|
||||||
if not files:
|
if not files:
|
||||||
|
|
@ -17488,7 +17489,7 @@ def cancel_download_task():
|
||||||
if download_id and username:
|
if download_id and username:
|
||||||
try:
|
try:
|
||||||
# This is an async call, so we run it and wait
|
# This is an async call, so we run it and wait
|
||||||
run_async(soulseek_client.cancel_download(download_id, username, remove=True))
|
run_async(download_orchestrator.cancel_download(download_id, username, remove=True))
|
||||||
logger.warning(f"Successfully cancelled Soulseek download {download_id} for task {task_id}")
|
logger.warning(f"Successfully cancelled Soulseek download {download_id} for task {task_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to cancel download on slskd, but worker already moved on: {e}")
|
logger.error(f"Failed to cancel download on slskd, but worker already moved on: {e}")
|
||||||
|
|
@ -17733,14 +17734,14 @@ def cancel_task_v2():
|
||||||
# username: youtube/tidal/qobuz/hifi/deezer_dl/lidarr go to
|
# username: youtube/tidal/qobuz/hifi/deezer_dl/lidarr go to
|
||||||
# their streaming clients, anything else goes to Soulseek.
|
# their streaming clients, anything else goes to Soulseek.
|
||||||
#
|
#
|
||||||
# Replaces an older block that assumed soulseek_client was a
|
# Replaces an older block that assumed download_orchestrator was a
|
||||||
# raw SoulseekClient and accessed .base_url / ._make_request
|
# raw SoulseekClient and accessed .base_url / ._make_request
|
||||||
# directly — crashed with AttributeError on the orchestrator
|
# directly — crashed with AttributeError on the orchestrator
|
||||||
# and silently left streaming downloads running in background.
|
# and silently left streaming downloads running in background.
|
||||||
try:
|
try:
|
||||||
logger.info(f"[Atomic Cancel] Dispatching cancel to orchestrator: username={username} download_id={download_id}")
|
logger.info(f"[Atomic Cancel] Dispatching cancel to orchestrator: username={username} download_id={download_id}")
|
||||||
cancel_success = run_async(
|
cancel_success = run_async(
|
||||||
soulseek_client.cancel_download(download_id, username, remove=True)
|
download_orchestrator.cancel_download(download_id, username, remove=True)
|
||||||
)
|
)
|
||||||
if cancel_success:
|
if cancel_success:
|
||||||
logger.info(f"[Atomic Cancel] Orchestrator cancelled download: {download_id}")
|
logger.info(f"[Atomic Cancel] Orchestrator cancelled download: {download_id}")
|
||||||
|
|
@ -19725,7 +19726,7 @@ def get_discover_album(source, album_id):
|
||||||
def hifi_status():
|
def hifi_status():
|
||||||
"""Check if HiFi API instances are reachable."""
|
"""Check if HiFi API instances are reachable."""
|
||||||
try:
|
try:
|
||||||
hifi = soulseek_client.client("hifi")
|
hifi = download_orchestrator.client("hifi")
|
||||||
available = hifi.is_available()
|
available = hifi.is_available()
|
||||||
version = hifi.get_version() if available else None
|
version = hifi.get_version() if available else None
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
@ -19749,8 +19750,8 @@ def soundcloud_status():
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
sc = None
|
sc = None
|
||||||
if soulseek_client and hasattr(soulseek_client, 'soundcloud'):
|
if download_orchestrator and hasattr(download_orchestrator, 'soundcloud'):
|
||||||
sc = soulseek_client.client("soundcloud")
|
sc = download_orchestrator.client("soundcloud")
|
||||||
if not sc:
|
if not sc:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"available": False,
|
"available": False,
|
||||||
|
|
@ -19778,7 +19779,7 @@ def hifi_instances():
|
||||||
"""Check availability of all HiFi API instances."""
|
"""Check availability of all HiFi API instances."""
|
||||||
import requests as req
|
import requests as req
|
||||||
try:
|
try:
|
||||||
hifi = soulseek_client.client("hifi")
|
hifi = download_orchestrator.client("hifi")
|
||||||
instances = list(hifi._instances)
|
instances = list(hifi._instances)
|
||||||
results = []
|
results = []
|
||||||
for url in instances:
|
for url in instances:
|
||||||
|
|
@ -19834,8 +19835,8 @@ def hifi_add_instance():
|
||||||
if not added:
|
if not added:
|
||||||
return jsonify({'success': False, 'error': 'Instance already exists'}), 400
|
return jsonify({'success': False, 'error': 'Instance already exists'}), 400
|
||||||
# Reload the HiFi client
|
# Reload the HiFi client
|
||||||
if soulseek_client:
|
if download_orchestrator:
|
||||||
soulseek_client.reload_instances('hifi')
|
download_orchestrator.reload_instances('hifi')
|
||||||
return jsonify({'success': True, 'url': url})
|
return jsonify({'success': True, 'url': url})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding HiFi instance: {e}")
|
logger.error(f"Error adding HiFi instance: {e}")
|
||||||
|
|
@ -19856,8 +19857,8 @@ def hifi_remove_instance():
|
||||||
if not removed:
|
if not removed:
|
||||||
return jsonify({'success': False, 'error': 'Instance not found'}), 404
|
return jsonify({'success': False, 'error': 'Instance not found'}), 404
|
||||||
# Reload the HiFi client
|
# Reload the HiFi client
|
||||||
if soulseek_client:
|
if download_orchestrator:
|
||||||
soulseek_client.reload_instances('hifi')
|
download_orchestrator.reload_instances('hifi')
|
||||||
return jsonify({'success': True, 'url': url})
|
return jsonify({'success': True, 'url': url})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error removing HiFi instance: {e}")
|
logger.error(f"Error removing HiFi instance: {e}")
|
||||||
|
|
@ -19877,8 +19878,8 @@ def hifi_toggle_instance():
|
||||||
from database.music_database import get_database
|
from database.music_database import get_database
|
||||||
db = get_database()
|
db = get_database()
|
||||||
db.toggle_hifi_instance(url, enabled)
|
db.toggle_hifi_instance(url, enabled)
|
||||||
if soulseek_client:
|
if download_orchestrator:
|
||||||
soulseek_client.reload_instances('hifi')
|
download_orchestrator.reload_instances('hifi')
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error toggling HiFi instance: {e}")
|
logger.error(f"Error toggling HiFi instance: {e}")
|
||||||
|
|
@ -19899,8 +19900,8 @@ def hifi_reorder_instances():
|
||||||
if not db.reorder_hifi_instances(urls):
|
if not db.reorder_hifi_instances(urls):
|
||||||
return jsonify({'success': False, 'error': 'One or more URLs not found'}), 400
|
return jsonify({'success': False, 'error': 'One or more URLs not found'}), 400
|
||||||
# Reload the HiFi client
|
# Reload the HiFi client
|
||||||
if soulseek_client:
|
if download_orchestrator:
|
||||||
soulseek_client.reload_instances('hifi')
|
download_orchestrator.reload_instances('hifi')
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error reordering HiFi instances: {e}")
|
logger.error(f"Error reordering HiFi instances: {e}")
|
||||||
|
|
@ -20052,9 +20053,9 @@ def deezer_download_test_download():
|
||||||
|
|
||||||
def _get_tidal_download_client():
|
def _get_tidal_download_client():
|
||||||
"""Get Tidal download client from the orchestrator, with helpful error if unavailable."""
|
"""Get Tidal download client from the orchestrator, with helpful error if unavailable."""
|
||||||
if not soulseek_client:
|
if not download_orchestrator:
|
||||||
raise RuntimeError("Download orchestrator not initialized — check startup logs for errors")
|
raise RuntimeError("Download orchestrator not initialized — check startup logs for errors")
|
||||||
tidal = soulseek_client.client("tidal") if hasattr(soulseek_client, 'client') else None
|
tidal = download_orchestrator.client("tidal") if hasattr(download_orchestrator, 'client') else None
|
||||||
if not tidal:
|
if not tidal:
|
||||||
raise RuntimeError("Tidal download client not available — ensure tidalapi is installed")
|
raise RuntimeError("Tidal download client not available — ensure tidalapi is installed")
|
||||||
return tidal
|
return tidal
|
||||||
|
|
@ -20126,7 +20127,7 @@ def qobuz_auth_login():
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
return jsonify({"success": False, "error": "Email and password required"}), 400
|
return jsonify({"success": False, "error": "Email and password required"}), 400
|
||||||
|
|
||||||
qobuz = soulseek_client.client("qobuz")
|
qobuz = download_orchestrator.client("qobuz")
|
||||||
result = qobuz.login(email, password)
|
result = qobuz.login(email, password)
|
||||||
|
|
||||||
if result['status'] == 'success':
|
if result['status'] == 'success':
|
||||||
|
|
@ -20149,7 +20150,7 @@ def qobuz_auth_token():
|
||||||
if not token:
|
if not token:
|
||||||
return jsonify({"success": False, "error": "Auth token required"}), 400
|
return jsonify({"success": False, "error": "Auth token required"}), 400
|
||||||
|
|
||||||
qobuz = soulseek_client.client("qobuz")
|
qobuz = download_orchestrator.client("qobuz")
|
||||||
result = qobuz.login_with_token(token)
|
result = qobuz.login_with_token(token)
|
||||||
|
|
||||||
if result['status'] == 'success':
|
if result['status'] == 'success':
|
||||||
|
|
@ -20166,7 +20167,7 @@ def qobuz_auth_token():
|
||||||
def qobuz_auth_status():
|
def qobuz_auth_status():
|
||||||
"""Check if Qobuz client is authenticated."""
|
"""Check if Qobuz client is authenticated."""
|
||||||
try:
|
try:
|
||||||
qobuz = soulseek_client.client("qobuz")
|
qobuz = download_orchestrator.client("qobuz")
|
||||||
authenticated = qobuz.is_authenticated()
|
authenticated = qobuz.is_authenticated()
|
||||||
user_info = {}
|
user_info = {}
|
||||||
if authenticated and qobuz.user_info:
|
if authenticated and qobuz.user_info:
|
||||||
|
|
@ -20183,7 +20184,7 @@ def qobuz_auth_status():
|
||||||
def qobuz_auth_logout():
|
def qobuz_auth_logout():
|
||||||
"""Logout from Qobuz."""
|
"""Logout from Qobuz."""
|
||||||
try:
|
try:
|
||||||
soulseek_client.client("qobuz").logout()
|
download_orchestrator.client("qobuz").logout()
|
||||||
_sync_qobuz_credentials_to_worker()
|
_sync_qobuz_credentials_to_worker()
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -21125,7 +21126,7 @@ def _get_metadata_fallback_client():
|
||||||
def get_deezer_arl_status():
|
def get_deezer_arl_status():
|
||||||
"""Check if Deezer ARL is configured and authenticated."""
|
"""Check if Deezer ARL is configured and authenticated."""
|
||||||
try:
|
try:
|
||||||
deezer_dl = soulseek_client.client("deezer_dl") if soulseek_client and hasattr(soulseek_client, 'client') else None
|
deezer_dl = download_orchestrator.client("deezer_dl") if download_orchestrator and hasattr(download_orchestrator, 'client') else None
|
||||||
if deezer_dl and deezer_dl.is_authenticated():
|
if deezer_dl and deezer_dl.is_authenticated():
|
||||||
user_data = deezer_dl._user_data or {}
|
user_data = deezer_dl._user_data or {}
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
@ -21142,7 +21143,7 @@ def get_deezer_arl_status():
|
||||||
def get_deezer_arl_playlists():
|
def get_deezer_arl_playlists():
|
||||||
"""Fetch user playlists via Deezer ARL authentication (like /api/spotify/playlists)."""
|
"""Fetch user playlists via Deezer ARL authentication (like /api/spotify/playlists)."""
|
||||||
try:
|
try:
|
||||||
deezer_dl = soulseek_client.client("deezer_dl") if soulseek_client and hasattr(soulseek_client, 'client') else None
|
deezer_dl = download_orchestrator.client("deezer_dl") if download_orchestrator and hasattr(download_orchestrator, 'client') else None
|
||||||
if not deezer_dl or not deezer_dl.is_authenticated():
|
if not deezer_dl or not deezer_dl.is_authenticated():
|
||||||
return jsonify({'error': 'Deezer ARL not authenticated. Configure your ARL token in Settings > Downloads.'}), 401
|
return jsonify({'error': 'Deezer ARL not authenticated. Configure your ARL token in Settings > Downloads.'}), 401
|
||||||
|
|
||||||
|
|
@ -21170,7 +21171,7 @@ def get_deezer_arl_playlists():
|
||||||
def get_deezer_arl_playlist_tracks(playlist_id):
|
def get_deezer_arl_playlist_tracks(playlist_id):
|
||||||
"""Fetch full playlist with tracks via ARL (like /api/spotify/playlist/<id>)."""
|
"""Fetch full playlist with tracks via ARL (like /api/spotify/playlist/<id>)."""
|
||||||
try:
|
try:
|
||||||
deezer_dl = soulseek_client.client("deezer_dl") if soulseek_client and hasattr(soulseek_client, 'client') else None
|
deezer_dl = download_orchestrator.client("deezer_dl") if download_orchestrator and hasattr(download_orchestrator, 'client') else None
|
||||||
if not deezer_dl or not deezer_dl.is_authenticated():
|
if not deezer_dl or not deezer_dl.is_authenticated():
|
||||||
return jsonify({'error': 'Deezer ARL not authenticated.'}), 401
|
return jsonify({'error': 'Deezer ARL not authenticated.'}), 401
|
||||||
|
|
||||||
|
|
@ -27362,8 +27363,8 @@ def get_your_artists_sources():
|
||||||
try:
|
try:
|
||||||
deezer_cl = _get_deezer_client()
|
deezer_cl = _get_deezer_client()
|
||||||
deezer_oauth = deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated()
|
deezer_oauth = deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated()
|
||||||
deezer_arl = (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.client("deezer_dl")
|
deezer_arl = (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
|
||||||
and soulseek_client.client("deezer_dl").is_authenticated())
|
and download_orchestrator.client("deezer_dl").is_authenticated())
|
||||||
if deezer_oauth or deezer_arl:
|
if deezer_oauth or deezer_arl:
|
||||||
connected.append('deezer')
|
connected.append('deezer')
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -27484,10 +27485,10 @@ def _fetch_and_match_liked_artists(profile_id: int):
|
||||||
if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated():
|
if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated():
|
||||||
logger.info("[Your Artists] Fetching favorite artists from Deezer (OAuth)...")
|
logger.info("[Your Artists] Fetching favorite artists from Deezer (OAuth)...")
|
||||||
artists = deezer_cl.get_user_favorite_artists(limit=200)
|
artists = deezer_cl.get_user_favorite_artists(limit=200)
|
||||||
elif (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.client("deezer_dl")
|
elif (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
|
||||||
and soulseek_client.client("deezer_dl").is_authenticated()):
|
and download_orchestrator.client("deezer_dl").is_authenticated()):
|
||||||
logger.info("[Your Artists] Fetching favorite artists from Deezer (ARL)...")
|
logger.info("[Your Artists] Fetching favorite artists from Deezer (ARL)...")
|
||||||
artists = soulseek_client.client("deezer_dl").get_user_favorite_artists(limit=200)
|
artists = download_orchestrator.client("deezer_dl").get_user_favorite_artists(limit=200)
|
||||||
for a in artists:
|
for a in artists:
|
||||||
database.upsert_liked_artist(
|
database.upsert_liked_artist(
|
||||||
artist_name=a['name'], source_service='deezer',
|
artist_name=a['name'], source_service='deezer',
|
||||||
|
|
@ -27634,8 +27635,8 @@ def get_your_albums_sources():
|
||||||
try:
|
try:
|
||||||
deezer_cl = _get_deezer_client()
|
deezer_cl = _get_deezer_client()
|
||||||
deezer_oauth = deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated()
|
deezer_oauth = deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated()
|
||||||
deezer_arl = (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.client("deezer_dl")
|
deezer_arl = (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
|
||||||
and soulseek_client.client("deezer_dl").is_authenticated())
|
and download_orchestrator.client("deezer_dl").is_authenticated())
|
||||||
if deezer_oauth or deezer_arl:
|
if deezer_oauth or deezer_arl:
|
||||||
connected.append('deezer')
|
connected.append('deezer')
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -27742,10 +27743,10 @@ def _fetch_liked_albums(profile_id: int):
|
||||||
if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated():
|
if deezer_cl and hasattr(deezer_cl, 'is_user_authenticated') and deezer_cl.is_user_authenticated():
|
||||||
logger.info("[Your Albums] Fetching favorite albums from Deezer (OAuth)...")
|
logger.info("[Your Albums] Fetching favorite albums from Deezer (OAuth)...")
|
||||||
albums = deezer_cl.get_user_favorite_albums(limit=500)
|
albums = deezer_cl.get_user_favorite_albums(limit=500)
|
||||||
elif (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.client("deezer_dl")
|
elif (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
|
||||||
and soulseek_client.client("deezer_dl").is_authenticated()):
|
and download_orchestrator.client("deezer_dl").is_authenticated()):
|
||||||
logger.info("[Your Albums] Fetching favorite albums from Deezer (ARL)...")
|
logger.info("[Your Albums] Fetching favorite albums from Deezer (ARL)...")
|
||||||
albums = soulseek_client.client("deezer_dl").get_user_favorite_albums(limit=500)
|
albums = download_orchestrator.client("deezer_dl").get_user_favorite_albums(limit=500)
|
||||||
for a in albums:
|
for a in albums:
|
||||||
database.upsert_liked_album(
|
database.upsert_liked_album(
|
||||||
album_name=a['album_name'], artist_name=a['artist_name'],
|
album_name=a['album_name'], artist_name=a['artist_name'],
|
||||||
|
|
@ -32666,7 +32667,7 @@ register_runtime_clients(
|
||||||
)
|
)
|
||||||
|
|
||||||
_init_connection_test(
|
_init_connection_test(
|
||||||
soulseek_client_obj=soulseek_client,
|
download_orchestrator_obj=download_orchestrator,
|
||||||
qobuz_worker=qobuz_enrichment_worker,
|
qobuz_worker=qobuz_enrichment_worker,
|
||||||
hydrabase_client_obj=hydrabase_client,
|
hydrabase_client_obj=hydrabase_client,
|
||||||
docker_resolve_url_fn=docker_resolve_url,
|
docker_resolve_url_fn=docker_resolve_url,
|
||||||
|
|
@ -32679,12 +32680,12 @@ _init_discover_hero(get_metadata_fallback_client_fn=_get_metadata_fallback_clien
|
||||||
|
|
||||||
_init_download_validation(
|
_init_download_validation(
|
||||||
matching_engine_obj=matching_engine,
|
matching_engine_obj=matching_engine,
|
||||||
soulseek_client_obj=soulseek_client,
|
download_orchestrator_obj=download_orchestrator,
|
||||||
)
|
)
|
||||||
|
|
||||||
_init_wishlist_failed(
|
_init_wishlist_failed(
|
||||||
engine=automation_engine,
|
engine=automation_engine,
|
||||||
soulseek_client_obj=soulseek_client,
|
download_orchestrator_obj=download_orchestrator,
|
||||||
sweep_fn=_sweep_empty_download_directories,
|
sweep_fn=_sweep_empty_download_directories,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32703,7 +32704,7 @@ _init_debug_info(
|
||||||
sync_states_dict=sync_states,
|
sync_states_dict=sync_states,
|
||||||
youtube_playlist_states_dict=youtube_playlist_states,
|
youtube_playlist_states_dict=youtube_playlist_states,
|
||||||
tidal_discovery_states_dict=tidal_discovery_states,
|
tidal_discovery_states_dict=tidal_discovery_states,
|
||||||
soulseek_client_obj=soulseek_client,
|
download_orchestrator_obj=download_orchestrator,
|
||||||
log_path=_log_path,
|
log_path=_log_path,
|
||||||
log_dir=_log_dir,
|
log_dir=_log_dir,
|
||||||
flask_app=app,
|
flask_app=app,
|
||||||
|
|
@ -32722,7 +32723,7 @@ _init_download_monitor(
|
||||||
start_next_batch_of_downloads=_start_next_batch_of_downloads,
|
start_next_batch_of_downloads=_start_next_batch_of_downloads,
|
||||||
orphaned_download_keys=_orphaned_download_keys,
|
orphaned_download_keys=_orphaned_download_keys,
|
||||||
missing_download_executor_obj=missing_download_executor,
|
missing_download_executor_obj=missing_download_executor,
|
||||||
soulseek_client_obj=soulseek_client,
|
download_orchestrator_obj=download_orchestrator,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Hydrabase Auto-Reconnect ---
|
# --- Hydrabase Auto-Reconnect ---
|
||||||
|
|
|
||||||
|
|
@ -3432,6 +3432,7 @@ const WHATS_NEW = {
|
||||||
'2.4.2': [
|
'2.4.2': [
|
||||||
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
|
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
|
||||||
{ date: 'Unreleased — 2.4.2 dev cycle' },
|
{ date: 'Unreleased — 2.4.2 dev cycle' },
|
||||||
|
{ title: 'Internal: Rename `soulseek_client` Global → `download_orchestrator`', desc: 'internal — followup cleanup. the global handle in web_server.py was named `soulseek_client` for historical reasons (the orchestrator was originally just the soulseek client and grew downstream sources around it), but the type has long been `DownloadOrchestrator` not `SoulseekClient`. renamed the global + every parameter/attribute that carried the legacy name across web_server.py, api/, core/downloads/*, core/search/*, core/streaming/*, services/sync_service.py, and the test fixtures (`MasterDeps.soulseek_client` → `download_orchestrator`, `init(soulseek_client_obj)` → `init(download_orchestrator_obj)`, etc). module path `core.soulseek_client` and class `SoulseekClient` (the actual soulseek-only client) are unchanged — only the orchestrator handle renamed. ~250 references touched, suite green.' },
|
||||||
{ title: 'Internal: Drop Backward-Compat Per-Source Attrs', desc: 'internal — followup to cin\'s download engine review. removed the `orchestrator.soulseek` / `.youtube` / `.tidal` / `.qobuz` / `.hifi` / `.deezer_dl` / `.lidarr` / `.soundcloud` attribute aliases that were preserved for backward compat. external callers (core/downloads/, core/search/, web_server.py) all migrated to the generic `orchestrator.client(\'<source>\')` accessor — alias-aware (legacy `deezer_dl` resolves to canonical `deezer`), single source of truth via the registry. the orchestrator\'s own internal `self.soulseek` / `self.deezer_dl` reaches also routed through `client()` so the only place that knows about per-source identity is the registry. test fakes updated to expose `client(name)` instead of stuffing attributes; conformance test pinned to the new accessor contract. zero behavior change — just cleaner shape.' },
|
{ title: 'Internal: Drop Backward-Compat Per-Source Attrs', desc: 'internal — followup to cin\'s download engine review. removed the `orchestrator.soulseek` / `.youtube` / `.tidal` / `.qobuz` / `.hifi` / `.deezer_dl` / `.lidarr` / `.soundcloud` attribute aliases that were preserved for backward compat. external callers (core/downloads/, core/search/, web_server.py) all migrated to the generic `orchestrator.client(\'<source>\')` accessor — alias-aware (legacy `deezer_dl` resolves to canonical `deezer`), single source of truth via the registry. the orchestrator\'s own internal `self.soulseek` / `self.deezer_dl` reaches also routed through `client()` so the only place that knows about per-source identity is the registry. test fakes updated to expose `client(name)` instead of stuffing attributes; conformance test pinned to the new accessor contract. zero behavior change — just cleaner shape.' },
|
||||||
{ title: 'Internal: Download Engine Review Followup', desc: 'internal — three correctness fixes on top of the download engine refactor, all flagged in cin\'s pr review. (1) `engine.cancel_download(source_hint=\'deezer_dl\')` was silently routing deezer cancels to soulseek because the legacy alias never made it to the engine\'s plugin map — only the registry knew about it. fix: aliases now flow through `register_plugin` and `get_plugin` / `cancel_download` resolve them to the canonical name. (2) `_resolve_source_chain` filtered hybrid_order against canonical registry names only, so any user with `deezer_dl` in their config quietly dropped deezer from hybrid mode. fix: orchestrator normalizes through `registry.get_spec()` first. (3) the worker\'s terminal write was a read-then-write split — a cancel landing between the snapshot and the update could be overwritten back to errored / completed. fix: new atomic `update_record_unless_state` on the engine holds `state_lock` across the check + write; both `_mark_terminal` AND the success path use it now. also added generic `client(name)` / `configured_clients()` / `reload_instances(name?)` accessors on the orchestrator + a `get/set_download_orchestrator()` singleton matching cin\'s `get_metadata_engine()` shape, and migrated 30 external `soulseek_client.<source>` reaches in web_server.py to `client("<source>")`. 18 new tests pin every fix.' },
|
{ title: 'Internal: Download Engine Review Followup', desc: 'internal — three correctness fixes on top of the download engine refactor, all flagged in cin\'s pr review. (1) `engine.cancel_download(source_hint=\'deezer_dl\')` was silently routing deezer cancels to soulseek because the legacy alias never made it to the engine\'s plugin map — only the registry knew about it. fix: aliases now flow through `register_plugin` and `get_plugin` / `cancel_download` resolve them to the canonical name. (2) `_resolve_source_chain` filtered hybrid_order against canonical registry names only, so any user with `deezer_dl` in their config quietly dropped deezer from hybrid mode. fix: orchestrator normalizes through `registry.get_spec()` first. (3) the worker\'s terminal write was a read-then-write split — a cancel landing between the snapshot and the update could be overwritten back to errored / completed. fix: new atomic `update_record_unless_state` on the engine holds `state_lock` across the check + write; both `_mark_terminal` AND the success path use it now. also added generic `client(name)` / `configured_clients()` / `reload_instances(name?)` accessors on the orchestrator + a `get/set_download_orchestrator()` singleton matching cin\'s `get_metadata_engine()` shape, and migrated 30 external `soulseek_client.<source>` reaches in web_server.py to `client("<source>")`. 18 new tests pin every fix.' },
|
||||||
{ title: 'Internal: Typed Metadata Foundation', desc: 'internal — first step of a multi-pr migration to give the metadata pipeline a real contract. the codebase historically grew duck-typed extractors (`_extract_lookup_value(album_data, "id", "album_id", "collectionId", "release_id", default=...)`) at every consumer site because each provider returns its own response shape. ~150 of those across the codebase. new `core/metadata/types.py` defines canonical typed `Album` / `Track` / `Artist` dataclasses with strict required fields. per-source classmethod converters (from_spotify_dict, from_itunes_dict, from_deezer_dict, from_discogs_dict, from_musicbrainz_dict, from_hydrabase_dict) are the SINGLE place that knows each provider\'s wire shape. zero behavior changes in this pr — pure additive foundation. follow-up prs migrate consumers one at a time. full migration plan documented at docs/metadata-types-migration.md.', page: 'library' },
|
{ title: 'Internal: Typed Metadata Foundation', desc: 'internal — first step of a multi-pr migration to give the metadata pipeline a real contract. the codebase historically grew duck-typed extractors (`_extract_lookup_value(album_data, "id", "album_id", "collectionId", "release_id", default=...)`) at every consumer site because each provider returns its own response shape. ~150 of those across the codebase. new `core/metadata/types.py` defines canonical typed `Album` / `Track` / `Artist` dataclasses with strict required fields. per-source classmethod converters (from_spotify_dict, from_itunes_dict, from_deezer_dict, from_discogs_dict, from_musicbrainz_dict, from_hydrabase_dict) are the SINGLE place that knows each provider\'s wire shape. zero behavior changes in this pr — pure additive foundation. follow-up prs migrate consumers one at a time. full migration plan documented at docs/metadata-types-migration.md.', page: 'library' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue