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:
Broque Thomas 2026-05-04 23:23:32 -07:00
parent 7519c3d50c
commit 61ba3a15de
28 changed files with 212 additions and 212 deletions

View file

@ -121,7 +121,7 @@ def register_routes(bp):
try:
from utils.async_helpers import run_async
soulseek = current_app.soulsync.get("soulseek_client")
soulseek = current_app.soulsync.get("download_orchestrator")
if not soulseek:
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."""
try:
from utils.async_helpers import run_async
soulseek = current_app.soulsync.get("soulseek_client")
soulseek = current_app.soulsync.get("download_orchestrator")
if not soulseek:
return api_error("NOT_AVAILABLE", "Soulseek client not configured.", 503)

View file

@ -104,7 +104,7 @@ def _run_search_and_download(request_id, query, notify_url):
if request_id in _pending_requests:
_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:
with _requests_lock:
if request_id in _pending_requests:

View file

@ -26,7 +26,7 @@ def register_routes(bp):
spotify = ctx.get("spotify_client")
spotify_ok = bool(spotify and spotify.is_authenticated())
soulseek = ctx.get("soulseek_client")
soulseek = ctx.get("download_orchestrator")
soulseek_ok = bool(soulseek)
hydrabase = ctx.get("hydrabase_client")

View file

@ -1,6 +1,6 @@
"""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
docker_resolve_path are injected at runtime because they live in
web_server.py and are constructed there.
@ -27,7 +27,7 @@ def _get_metadata_fallback_source():
# Injected at runtime via init().
soulseek_client = None
download_orchestrator = None
qobuz_enrichment_worker = None
hydrabase_client = None
docker_resolve_url = None
@ -35,16 +35,16 @@ docker_resolve_path = None
def init(
soulseek_client_obj,
download_orchestrator_obj,
qobuz_worker,
hydrabase_client_obj,
docker_resolve_url_fn,
docker_resolve_path_fn,
):
"""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
soulseek_client = soulseek_client_obj
download_orchestrator = download_orchestrator_obj
qobuz_enrichment_worker = qobuz_worker
hydrabase_client = hydrabase_client_obj
docker_resolve_url = docker_resolve_url_fn
@ -177,13 +177,13 @@ def run_service_test(service, test_config):
else:
return False, f"Output folder not found: {transfer_path}"
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."
# Test the orchestrator's configured download source (not just Soulseek)
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
mode_messages = {
'soulseek': "Successfully connected to Soulseek network via slskd.",

View file

@ -64,7 +64,7 @@ download_batches = None
sync_states = None
youtube_playlist_states = None
tidal_discovery_states = None
soulseek_client = None
download_orchestrator = None
_log_path = None
_log_dir = None
app = None
@ -80,7 +80,7 @@ def init(
sync_states_dict,
youtube_playlist_states_dict,
tidal_discovery_states_dict,
soulseek_client_obj,
download_orchestrator_obj,
log_path,
log_dir,
flask_app,
@ -90,7 +90,7 @@ def init(
"""Bind shared state/helpers from web_server."""
global SOULSYNC_VERSION, _DIRECT_RUN, _status_cache, qobuz_enrichment_worker
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
SOULSYNC_VERSION = soulsync_version
_DIRECT_RUN = direct_run
@ -100,7 +100,7 @@ def init(
sync_states = sync_states_dict
youtube_playlist_states = youtube_playlist_states_dict
tidal_discovery_states = tidal_discovery_states_dict
soulseek_client = soulseek_client_obj
download_orchestrator = download_orchestrator_obj
_log_path = log_path
_log_dir = log_dir
app = flask_app
@ -292,9 +292,9 @@ def get_debug_info():
# Download client init failures
info['download_client_failures'] = []
if soulseek_client and hasattr(soulseek_client, '_init_failures'):
info['download_client_failures'] = soulseek_client._init_failures
elif not soulseek_client:
if download_orchestrator and hasattr(download_orchestrator, '_init_failures'):
info['download_client_failures'] = download_orchestrator._init_failures
elif not download_orchestrator:
info['download_client_failures'] = ['ALL (orchestrator failed to initialize)']
# API rate monitor — current calls/min, 24h totals, peaks, rate limit events

View file

@ -547,10 +547,8 @@ class DownloadOrchestrator:
# Singleton accessor — mirrors Cin's metadata engine pattern
# (``get_metadata_engine()``). Callers that don't need a custom
# registry use this instead of instantiating DownloadOrchestrator
# directly. Currently web_server.py constructs the singleton at
# startup and exposes it via the legacy ``soulseek_client`` global;
# this factory exists for new callers + future migration of that
# global to a more honestly-named ``download_orchestrator``.
# directly. web_server.py constructs the singleton at startup and
# exposes it via the ``download_orchestrator`` global.
# ---------------------------------------------------------------------------
_default_orchestrator: Optional['DownloadOrchestrator'] = None

View file

@ -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:
"""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]:
"""Cancel every active slskd download, clear the resulting ones, sweep dirs.
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:
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()
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:
"""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:
sweep_callback()
return success

View file

@ -25,7 +25,7 @@ and notifies the lifecycle via `on_download_completed(success=False)` so the
worker slot frees up.
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`.
"""
@ -49,7 +49,7 @@ logger = logging.getLogger(__name__)
@dataclass
class CandidatesDeps:
"""Bundle of cross-cutting deps the candidate-fallback logic needs."""
soulseek_client: Any
download_orchestrator: Any
spotify_client: Any
run_async: Callable[..., Any]
get_database: Callable[[], Any]
@ -209,7 +209,7 @@ def attempt_download_with_candidates(task_id, candidates, track, batch_id=None,
# Initiate download
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:
# 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")
# Try to cancel the download immediately
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}")
except Exception as 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)
return False
# Store download information - use real download ID from soulseek_client
# CRITICAL FIX: Trust the download ID returned by soulseek_client.download()
# Store download information - use real download ID from download_orchestrator
# CRITICAL FIX: Trust the download ID returned by download_orchestrator.download()
download_tasks[task_id]['download_id'] = download_id
download_tasks[task_id]['username'] = username

View file

@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
class MasterDeps:
"""Bundle of cross-cutting deps the master worker needs."""
config_manager: Any
soulseek_client: Any
download_orchestrator: Any
run_async: Callable[..., Any]
mb_worker: Any
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}'")
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)
album_queries = [f"{artist_name} {album_name}"]

View file

@ -33,7 +33,7 @@ _run_post_processing_worker = None
_start_next_batch_of_downloads = None
_orphaned_download_keys = None
missing_download_executor = None
soulseek_client = None
download_orchestrator = None
def init(
@ -44,12 +44,12 @@ def init(
start_next_batch_of_downloads,
orphaned_download_keys,
missing_download_executor_obj,
soulseek_client_obj,
download_orchestrator_obj,
):
"""Bind web_server-side helpers/globals so the class body can resolve them."""
global _make_context_key, _on_download_completed, _download_track_worker
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
_on_download_completed = on_download_completed
_download_track_worker = download_track_worker
@ -57,7 +57,7 @@ def init(
_start_next_batch_of_downloads = start_next_batch_of_downloads
_orphaned_download_keys = orphaned_download_keys
missing_download_executor = missing_download_executor_obj
soulseek_client = soulseek_client_obj
download_orchestrator = download_orchestrator_obj
class WebUIDownloadMonitor:
@ -199,7 +199,7 @@ class WebUIDownloadMonitor:
if op[0] == 'cancel_download':
_, download_id, username = op
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}")
elif op[0] == 'cleanup_orphan':
_, context_key = op
@ -254,9 +254,9 @@ class WebUIDownloadMonitor:
# Get Soulseek downloads from API
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:
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:
for user_data in transfers_data:
username = user_data.get('username', 'Unknown')
@ -271,9 +271,9 @@ class WebUIDownloadMonitor:
# cross-source aggregation, no per-source iteration.
try:
all_downloads = []
if soulseek_client and hasattr(soulseek_client, 'engine'):
if download_orchestrator and hasattr(download_orchestrator, 'engine'):
try:
all_downloads = run_async(soulseek_client.engine.get_all_downloads())
all_downloads = run_async(download_orchestrator.engine.get_all_downloads())
except Exception:
pass
for download in all_downloads:

View file

@ -54,7 +54,7 @@ class PostProcessDeps:
always live (no caching of pre-init Spotify clients etc).
"""
config_manager: Any
soulseek_client: Any
download_orchestrator: Any
run_async: Callable
docker_resolve_path: 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
# 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
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:
real_path = status.file_path
if os.path.exists(real_path):

View file

@ -34,7 +34,7 @@ logger = logging.getLogger(__name__)
@dataclass
class TaskWorkerDeps:
"""Bundle of cross-cutting deps the per-task download worker needs."""
soulseek_client: Any
download_orchestrator: Any
matching_engine: Any
run_async: Callable
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:
# 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")
# 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 ===
# The orchestrator's hybrid search stops at the first source with results, even if
# 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:
orch = deps.soulseek_client
orch = deps.download_orchestrator
hybrid_order = getattr(orch, 'hybrid_order', None) or []
if not hybrid_order:
primary = getattr(orch, 'hybrid_primary', 'soulseek')

View file

@ -1,7 +1,7 @@
"""Soulseek/streaming candidate validation — lifted from web_server.py.
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
the body.
"""
@ -14,14 +14,14 @@ logger = logging.getLogger(__name__)
# Injected at runtime via init().
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."""
global matching_engine, soulseek_client
global matching_engine, download_orchestrator
matching_engine = matching_engine_obj
soulseek_client = soulseek_client_obj
download_orchestrator = download_orchestrator_obj
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
else:
# Filter by user's quality profile before artist verification (Soulseek only)
# Use existing soulseek_client to avoid re-initializing (which accesses download_path filesystem)
quality_filtered_candidates = soulseek_client.client('soulseek').filter_results_by_quality_preference(initial_candidates)
# Use existing download_orchestrator to avoid re-initializing (which accesses download_path filesystem)
quality_filtered_candidates = download_orchestrator.client('soulseek').filter_results_by_quality_preference(initial_candidates)
# IMPORTANT: Respect empty results from quality filter
# If user has strict quality requirements (e.g., FLAC-only with fallback disabled),

View file

@ -2,7 +2,7 @@
Body is byte-identical to the original. Wishlist helpers are
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
in web_server.py.
"""
@ -29,15 +29,15 @@ logger = logging.getLogger(__name__)
# Injected at runtime via init().
automation_engine = None
soulseek_client = None
download_orchestrator = 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."""
global automation_engine, soulseek_client, _sweep_empty_download_directories
global automation_engine, download_orchestrator, _sweep_empty_download_directories
automation_engine = engine
soulseek_client = soulseek_client_obj
download_orchestrator = download_orchestrator_obj
_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
try:
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")
except Exception as cleanup_error:
logger.warning(f"[Auto-Cleanup] Failed to clear completed downloads: {cleanup_error}")

View file

@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
def run_basic_soulseek_search(
query: str,
soulseek_client,
download_orchestrator,
run_async: Callable,
) -> list[dict]:
"""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
`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 = []
for album in albums:

View file

@ -49,7 +49,7 @@ class SearchDeps:
spotify_client: Any
hydrabase_client: Any
hydrabase_worker: Any
soulseek_client: Any
download_orchestrator: Any
fix_artist_image_url: Callable[[Optional[str]], Optional[str]]
is_hydrabase_active: Callable[[], bool]
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):
"""Return the YouTube download client (used for music-video search)
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 deps.soulseek_client.client('youtube')
return deps.download_orchestrator.client('youtube')
def stream_youtube_videos(query: str, youtube_client, run_async: Callable) -> Iterator[str]:

View file

@ -96,7 +96,7 @@ def stream_search_track(
album_name: Optional[str],
duration_ms: int,
config_manager,
soulseek_client,
download_orchestrator,
matching_engine,
run_async: Callable,
) -> Optional[dict]:
@ -120,7 +120,7 @@ def stream_search_track(
# Map mode name to canonical registry name (legacy 'deezer_dl'
# 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
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:
tracks_result, _ = run_async(stream_client.search(query, timeout=15))
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:
logger.info(f"No results for query '{query}', trying next...")

View file

@ -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.
2. Clear any prior file from the Stream/ folder (only one stream lives
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.
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
states. Queue timeout = 15 s; overall timeout = 60 s.
5. On completion (state ~ 'succeeded' or progress >= 100% AND bytes
@ -45,7 +45,7 @@ logger = logging.getLogger(__name__)
class PrepareStreamDeps:
"""Bundle of cross-cutting deps the stream-prep worker needs."""
config_manager: Any
soulseek_client: Any
download_orchestrator: Any
stream_lock: Any # threading.Lock
project_root: str # absolute path to web_server.py's directory
docker_resolve_path: Callable[[str], str]
@ -112,7 +112,7 @@ def prepare_stream_task(track_data, deps: PrepareStreamDeps):
asyncio.set_event_loop(loop)
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('filename'),
track_data.get('size', 0)
@ -144,7 +144,7 @@ def prepare_stream_task(track_data, deps: PrepareStreamDeps):
try:
# 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)
if download_status:
@ -252,7 +252,7 @@ def prepare_stream_task(track_data, deps: PrepareStreamDeps):
download_id = download_status.get('id', '')
if download_id and track_data.get('username'):
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)
)
if success:

View file

@ -44,12 +44,12 @@ class SyncProgress:
failed_tracks: int = 0
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.plex_client = plex_client
self.jellyfin_client = jellyfin_client
self.navidrome_client = navidrome_client
self.soulseek_client = soulseek_client
self.download_orchestrator = download_orchestrator
self.progress_callbacks = {} # Playlist-specific progress callbacks
self.syncing_playlists = set() # Track multiple syncing playlists
self._cancelled = False
@ -635,7 +635,7 @@ class PlaylistSyncService:
query = self.matching_engine.generate_download_query(match_result.spotify_track)
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:
downloaded_count += 1

View file

@ -101,7 +101,7 @@ def _build_deps(
on_complete=None,
):
deps = dc.CandidatesDeps(
soulseek_client=soulseek or _FakeSoulseek(),
download_orchestrator=soulseek or _FakeSoulseek(),
spotify_client=spotify or _FakeSpotify(),
run_async=_run_async,
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)
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 "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)
# 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)
# First candidate skipped (already used), second one tried
assert len(deps.soulseek_client.download_calls) == 1
assert deps.soulseek_client.download_calls[0][1] == "fresh.flac"
assert len(deps.download_orchestrator.download_calls) == 1
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)
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)
assert result is False
assert deps.soulseek_client.download_calls == []
assert deps.download_orchestrator.download_calls == []
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)
# 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():
@ -266,7 +266,7 @@ def test_cancellation_after_download_starts_calls_cancel_and_lifecycle():
assert result is False
# 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
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():
"""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)
deps = _build_deps(soulseek=soulseek)
_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)
# First one wins — second never tried because download succeeded
assert len(deps.soulseek_client.download_calls) == 1
assert deps.soulseek_client.download_calls[0][1] == "a.flac"
assert len(deps.download_orchestrator.download_calls) == 1
assert deps.download_orchestrator.download_calls[0][1] == "a.flac"

View file

@ -178,7 +178,7 @@ def _build_deps(
):
return mw.MasterDeps(
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(),
mb_worker=mb_worker,
mb_release_cache=mb_release_cache if mb_release_cache is not None else {},

View file

@ -49,7 +49,7 @@ class _Recorder:
def _build_deps(
*,
config=None,
soulseek_client=None,
download_orchestrator=None,
run_async=None,
docker_resolve_path=None,
extract_filename=None,
@ -64,7 +64,7 @@ def _build_deps(
rec = _Recorder()
return pp.PostProcessDeps(
config_manager=config or _FakeConfig(),
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
run_async=run_async or (lambda c: None),
docker_resolve_path=docker_resolve_path or (lambda p: p),
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')
deps, rec = _build_deps(
soulseek_client=_FakeYTClient(),
download_orchestrator=_FakeYTClient(),
run_async=lambda coro: coro, # not async — direct call
)
pp.run_post_processing_worker('t1', 'b1', deps)

View file

@ -76,7 +76,7 @@ def _build_deps(
):
rec = _Recorder()
return tw.TaskWorkerDeps(
soulseek_client=soulseek or _FakeClient(),
download_orchestrator=soulseek or _FakeClient(),
matching_engine=matching or _FakeMatchEngine(),
run_async=_sync_run_async,
try_source_reuse=try_source_reuse,

View file

@ -129,7 +129,7 @@ def _build_deps(**overrides):
spotify_client=None,
hydrabase_client=None,
hydrabase_worker=None,
soulseek_client=None,
download_orchestrator=None,
fix_artist_image_url=lambda u: f'FIXED::{u}' if u else None,
is_hydrabase_active=lambda: False,
get_metadata_fallback_source=lambda: 'spotify',
@ -470,19 +470,19 @@ class _FakeSoulseekWithYT:
def test_resolve_youtube_videos_returns_subclient():
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
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
def test_resolve_youtube_videos_no_youtube_attr_returns_none():
class _NoYT:
pass
deps = _build_deps(soulseek_client=_NoYT())
deps = _build_deps(download_orchestrator=_NoYT())
assert orchestrator.resolve_youtube_videos_client(deps) is None

View file

@ -169,7 +169,7 @@ def test_stream_finds_match_on_first_query():
result = stream.stream_search_track(
track_name='Money', artist_name='Pink Floyd', album_name=None,
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,
)
assert result is not None
@ -190,7 +190,7 @@ def test_stream_walks_to_second_query_on_no_match():
result = stream.stream_search_track(
track_name='Money (Remastered)', artist_name='Pink Floyd', album_name=None,
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,
)
assert result is not None
@ -209,7 +209,7 @@ def test_stream_returns_none_when_no_matches():
result = stream.stream_search_track(
track_name='Money', artist_name='Pink Floyd', album_name=None,
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,
)
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():
# 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".
soul = _FakeSoulseek(results_per_query={'Pink Floyd Money': ([object()], [])})
cfg = _FakeConfig({
@ -229,7 +229,7 @@ def test_stream_falls_back_to_default_soulseek_when_no_direct_client():
result = stream.stream_search_track(
track_name='Money', artist_name='Pink Floyd', album_name=None,
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,
)
assert result is not None
@ -257,7 +257,7 @@ def test_stream_continues_past_per_query_exception():
result = stream.stream_search_track(
track_name='Money (Live)', artist_name='Pink Floyd', album_name=None,
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,
)
# First query raised, second succeeded

View file

@ -10,7 +10,7 @@ from core.streaming import prepare as sp
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):
self._download_id = download_id
@ -43,7 +43,7 @@ def _build_deps(
state = state if state is not None else {}
deps = sp.PrepareStreamDeps(
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(),
project_root=project_root,
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):
"""soulseek_client.download() returning None → state.error."""
"""download_orchestrator.download() returning None → state.error."""
sk = _FakeSoulseek(download_id=None)
deps = _build_deps(soulseek=sk, project_root=str(tmp_path))

View file

@ -570,7 +570,7 @@ IS_SHUTTING_DOWN = False
# 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.
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:
spotify_client = get_spotify_client()
@ -604,7 +604,7 @@ except Exception as e:
logger.error(f" SoulSync library client failed to initialize: {e}")
try:
soulseek_client = DownloadOrchestrator()
download_orchestrator = DownloadOrchestrator()
logger.info(" Download orchestrator initialized")
except Exception as 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}")
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")
except Exception as 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
# accepts one. Generic dispatch via the registry — no per-source
# attribute reaches needed.
if soulseek_client and hasattr(soulseek_client, 'registry'):
for _src_name, _src_client in soulseek_client.registry.all_plugins():
if download_orchestrator and hasattr(download_orchestrator, 'registry'):
for _src_name, _src_client in download_orchestrator.registry.all_plugins():
if _src_client is not None and hasattr(_src_client, 'set_shutdown_check'):
try:
_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'])
soulseek_active = (dl_mode == 'soulseek' or
(dl_mode == 'hybrid' and 'soulseek' in hybrid_order))
# soulseek_client is a DownloadOrchestrator; the real client lives on
# .soulseek. Match the getattr pattern used at the other call sites.
slskd = getattr(soulseek_client, 'soulseek', None) if soulseek_client else None
# Reach the underlying SoulseekClient via the orchestrator's
# generic accessor.
slskd = download_orchestrator.client('soulseek') if download_orchestrator else None
if not soulseek_active or not slskd or not slskd.base_url:
_update_automation_progress(automation_id,
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')
return {'status': 'skipped'}
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
))
if success:
@ -2041,7 +2041,7 @@ def _register_automation_handlers():
log_line='Skipped — downloads active', log_type='skip')
return {'status': 'completed'}
run_async(soulseek_client.clear_all_completed_downloads())
run_async(download_orchestrator.clear_all_completed_downloads())
if not has_post_processing:
_sweep_empty_download_directories()
_update_automation_progress(automation_id,
@ -2096,7 +2096,7 @@ def _register_automation_handlers():
log_line='Download queue: skipped (active batches)', log_type='skip')
else:
try:
run_async(soulseek_client.clear_all_completed_downloads())
run_async(download_orchestrator.clear_all_completed_downloads())
steps.append('Download queue: cleared')
_update_automation_progress(automation_id,
log_line='Download queue: cleared', log_type='success')
@ -2154,7 +2154,7 @@ def _register_automation_handlers():
_update_automation_progress(automation_id,
log_line='Search cleanup: disabled in settings', log_type='skip')
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
))
steps.append('Search history: cleaned')
@ -2286,7 +2286,7 @@ def _register_automation_handlers():
if automation_id:
_update_automation_progress(automation_id,
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 automation_id:
_update_automation_progress(automation_id,
@ -2348,7 +2348,7 @@ try:
app.register_blueprint(api_bp, url_prefix='/api/v1')
app.soulsync = {
'spotify_client': spotify_client,
'soulseek_client': soulseek_client,
'download_orchestrator': download_orchestrator,
'tidal_client': tidal_client,
'matching_engine': matching_engine,
'config_manager': config_manager,
@ -2453,8 +2453,9 @@ def get_cached_transfer_data():
# First, get Soulseek downloads from API
transfers_data = None
if not soulseek_known_down and soulseek_client and getattr(soulseek_client, 'soulseek', None) and soulseek_client.client("soulseek").base_url:
transfers_data = run_async(soulseek_client._make_request('GET', 'transfers/downloads'))
_slsk = download_orchestrator.client("soulseek") if download_orchestrator else None
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:
all_transfers = []
for user_data in transfers_data:
@ -2477,9 +2478,9 @@ def get_cached_transfer_data():
all_downloads = []
# Generic dispatch — engine returns active downloads
# 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:
all_downloads = run_async(soulseek_client.engine.get_all_downloads())
all_downloads = run_async(download_orchestrator.engine.get_all_downloads())
except Exception:
pass
for download in all_downloads:
@ -3052,7 +3053,7 @@ def _build_prepare_stream_deps():
return _streaming_prepare.PrepareStreamDeps(
config_manager=config_manager,
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
stream_lock=stream_lock,
project_root=os.path.dirname(os.path.abspath(__file__)),
docker_resolve_path=docker_resolve_path,
@ -3499,10 +3500,10 @@ def get_status():
if is_serverless:
soulseek_status = True
soulseek_response_time = 0
elif soulseek_relevant and soulseek_client:
elif soulseek_relevant and download_orchestrator:
soulseek_start = time.time()
try:
soulseek_status = run_async(soulseek_client.check_connection())
soulseek_status = run_async(download_orchestrator.check_connection())
except Exception:
soulseek_status = False
soulseek_response_time = (time.time() - soulseek_start) * 1000
@ -3915,7 +3916,7 @@ def _build_system_stats():
if soulseek_active and not soulseek_known_down:
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:
for user_data in transfers_data:
if 'directories' in user_data:
@ -4141,11 +4142,11 @@ def handle_settings():
if navidrome_client:
navidrome_client.reload_config()
# Reload orchestrator settings (download source mode, hybrid_primary, etc.)
if soulseek_client:
soulseek_client.reload_settings()
if download_orchestrator:
download_orchestrator.reload_settings()
# Reload YouTube client settings (rate limiting, cookies)
if hasattr(soulseek_client, 'youtube'):
soulseek_client.client("youtube").reload_settings()
if hasattr(download_orchestrator, 'youtube'):
download_orchestrator.client("youtube").reload_settings()
# FIX: Re-instantiate the global tidal_client to pick up new settings
try:
tidal_client = TidalClient()
@ -4177,7 +4178,7 @@ def handle_settings():
data = dict(config_manager.config_data)
# Include which download sources are configured so the UI can auto-disable unconfigured ones
try:
data['_source_status'] = soulseek_client.get_source_status()
data['_source_status'] = download_orchestrator.get_source_status()
except Exception:
pass
return jsonify(data)
@ -6728,7 +6729,7 @@ def _build_search_deps():
spotify_client=spotify_client,
hydrabase_client=hydrabase_client,
hydrabase_worker=hydrabase_worker,
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
fix_artist_image_url=fix_artist_image_url,
is_hydrabase_active=_is_hydrabase_active,
get_metadata_fallback_source=_get_metadata_fallback_source,
@ -6754,7 +6755,7 @@ def search_music():
add_activity_item("", "Search Started", f"'{query}'", "Now")
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")
return jsonify({"results": results})
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,
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
the original endpoint contract.
"""
@ -6894,7 +6895,7 @@ def stream_enhanced_search_track():
album_name=album_name,
duration_ms=duration_ms,
config_manager=config_manager,
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
matching_engine=matching_engine,
run_async=run_async,
)
@ -7033,7 +7034,7 @@ def download_music_video():
def _progress(pct):
_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):
_music_video_downloads[video_id]['status'] = 'completed'
@ -7092,7 +7093,7 @@ def start_download():
filename = track_data.get('filename')
file_size = track_data.get('size', 0)
download_id = run_async(soulseek_client.download(
download_id = run_async(download_orchestrator.download(
username,
filename,
file_size
@ -7141,7 +7142,7 @@ def start_download():
if not username or not filename:
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}")
if download_id:
@ -7337,7 +7338,7 @@ def get_download_status():
A robust status checker that correctly finds completed files by searching
the entire download directory with fuzzy matching, mirroring the logic from downloads.py.
"""
if not soulseek_client:
if not download_orchestrator:
return jsonify({"transfers": []})
try:
@ -7346,7 +7347,7 @@ def get_download_status():
soulseek_known_down = not _status_cache.get('soulseek', {}).get('connected', True)
transfers_data = None
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!
all_transfers = []
@ -7414,7 +7415,7 @@ def get_download_status():
transfer_id = file_info.get('id')
if transfer_id:
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:
pass
_orphaned_download_keys.discard(context_key)
@ -7526,7 +7527,7 @@ def get_download_status():
# Also include YouTube/Tidal downloads in the response
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:
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
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:
return jsonify({"success": True, "message": "Download cancelled."})
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."""
try:
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:
return jsonify({"success": True, "message": msg})
@ -7687,7 +7688,7 @@ def clear_finished_downloads():
"""Clear all terminal (completed, cancelled, failed) downloads from slskd."""
try:
success = _downloads_cancel.clear_finished_active(
soulseek_client, run_async, _sweep_empty_download_directories,
download_orchestrator, run_async, _sweep_empty_download_directories,
)
if success:
return jsonify({"success": True, "message": "Finished downloads cleared."})
@ -8074,7 +8075,7 @@ def clear_all_searches():
Clear all searches from slskd search history.
"""
try:
success = run_async(soulseek_client.clear_all_searches())
success = run_async(download_orchestrator.clear_all_searches())
if success:
add_activity_item("", "Search Cleanup", "All search history cleared manually", "Now")
return jsonify({"success": True, "message": "All searches cleared."})
@ -8094,7 +8095,7 @@ def maintain_search_history():
keep_searches = data.get('keep_searches', 50)
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
))
if success:
@ -11674,7 +11675,7 @@ def redownload_search_sources(track_id):
# Get all available download source clients
download_clients = {}
try:
orch = soulseek_client # The download orchestrator
orch = download_orchestrator # The download orchestrator
if hasattr(orch, 'soulseek') and orch.soulseek:
if not (hasattr(orch.soulseek, 'is_configured') and not orch.soulseek.is_configured()):
download_clients['soulseek'] = orch.soulseek
@ -11698,7 +11699,7 @@ def redownload_search_sources(track_id):
if not download_clients:
# 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())}")
@ -12660,7 +12661,7 @@ def _start_enhanced_album_download(enhanced_tracks, unmatched_tracks, spotify_ar
continue
# 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:
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:
continue
download_id = run_async(soulseek_client.download(username, filename, size))
download_id = run_async(download_orchestrator.download(username, filename, size))
if download_id:
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)
}
download_id = run_async(soulseek_client.download(username, filename, size))
download_id = run_async(download_orchestrator.download(username, filename, size))
if download_id:
context_key = _make_context_key(username, filename)
@ -12883,7 +12884,7 @@ def start_matched_download():
if not username or not filename:
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:
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['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:
context_key = _make_context_key(username, filename)
@ -12972,7 +12973,7 @@ def start_matched_download():
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.
"""
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,
spotify_enrichment_worker=spotify_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,
spotify_enrichment_worker=spotify_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,
spotify_enrichment_worker=spotify_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(
config_manager=config_manager,
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
run_async=run_async,
mb_worker=mb_worker,
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."""
return _downloads_post_processing.PostProcessDeps(
config_manager=config_manager,
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
run_async=run_async,
docker_resolve_path=docker_resolve_path,
extract_filename=extract_filename,
@ -16886,7 +16887,7 @@ from core.downloads import task_worker as _downloads_task_worker
def _build_task_worker_deps():
"""Build TaskWorkerDeps bundle from web_server.py globals on each call."""
return _downloads_task_worker.TaskWorkerDeps(
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
matching_engine=matching_engine,
run_async=run_async,
try_source_reuse=_try_source_reuse,
@ -16912,7 +16913,7 @@ from core.downloads import candidates as _downloads_candidates
def _build_candidates_deps():
"""Build the CandidatesDeps bundle from web_server.py globals on each call."""
return _downloads_candidates.CandidatesDeps(
soulseek_client=soulseek_client,
download_orchestrator=download_orchestrator,
spotify_client=spotify_client,
run_async=run_async,
get_database=get_database,
@ -17077,7 +17078,7 @@ def _try_source_reuse(task_id, batch_id, track):
# Sort by confidence, filter by quality preference
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})")
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)
if not filtered:
_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
try:
# Access SoulseekClient directly (soulseek_client is DownloadOrchestrator)
slsk = soulseek_client.client("soulseek") if hasattr(soulseek_client, 'soulseek') else soulseek_client
# Access SoulseekClient directly (download_orchestrator is DownloadOrchestrator)
slsk = download_orchestrator.client("soulseek") if hasattr(download_orchestrator, 'soulseek') else download_orchestrator
_sr.info(f"Browsing {username}:{folder_path}...")
files = run_async(slsk.browse_user_directory(username, folder_path))
if not files:
@ -17488,7 +17489,7 @@ def cancel_download_task():
if download_id and username:
try:
# 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}")
except Exception as 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
# 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
# directly — crashed with AttributeError on the orchestrator
# and silently left streaming downloads running in background.
try:
logger.info(f"[Atomic Cancel] Dispatching cancel to orchestrator: username={username} download_id={download_id}")
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:
logger.info(f"[Atomic Cancel] Orchestrator cancelled download: {download_id}")
@ -19725,7 +19726,7 @@ def get_discover_album(source, album_id):
def hifi_status():
"""Check if HiFi API instances are reachable."""
try:
hifi = soulseek_client.client("hifi")
hifi = download_orchestrator.client("hifi")
available = hifi.is_available()
version = hifi.get_version() if available else None
return jsonify({
@ -19749,8 +19750,8 @@ def soundcloud_status():
"""
try:
sc = None
if soulseek_client and hasattr(soulseek_client, 'soundcloud'):
sc = soulseek_client.client("soundcloud")
if download_orchestrator and hasattr(download_orchestrator, 'soundcloud'):
sc = download_orchestrator.client("soundcloud")
if not sc:
return jsonify({
"available": False,
@ -19778,7 +19779,7 @@ def hifi_instances():
"""Check availability of all HiFi API instances."""
import requests as req
try:
hifi = soulseek_client.client("hifi")
hifi = download_orchestrator.client("hifi")
instances = list(hifi._instances)
results = []
for url in instances:
@ -19834,8 +19835,8 @@ def hifi_add_instance():
if not added:
return jsonify({'success': False, 'error': 'Instance already exists'}), 400
# Reload the HiFi client
if soulseek_client:
soulseek_client.reload_instances('hifi')
if download_orchestrator:
download_orchestrator.reload_instances('hifi')
return jsonify({'success': True, 'url': url})
except Exception as e:
logger.error(f"Error adding HiFi instance: {e}")
@ -19856,8 +19857,8 @@ def hifi_remove_instance():
if not removed:
return jsonify({'success': False, 'error': 'Instance not found'}), 404
# Reload the HiFi client
if soulseek_client:
soulseek_client.reload_instances('hifi')
if download_orchestrator:
download_orchestrator.reload_instances('hifi')
return jsonify({'success': True, 'url': url})
except Exception as e:
logger.error(f"Error removing HiFi instance: {e}")
@ -19877,8 +19878,8 @@ def hifi_toggle_instance():
from database.music_database import get_database
db = get_database()
db.toggle_hifi_instance(url, enabled)
if soulseek_client:
soulseek_client.reload_instances('hifi')
if download_orchestrator:
download_orchestrator.reload_instances('hifi')
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error toggling HiFi instance: {e}")
@ -19899,8 +19900,8 @@ def hifi_reorder_instances():
if not db.reorder_hifi_instances(urls):
return jsonify({'success': False, 'error': 'One or more URLs not found'}), 400
# Reload the HiFi client
if soulseek_client:
soulseek_client.reload_instances('hifi')
if download_orchestrator:
download_orchestrator.reload_instances('hifi')
return jsonify({'success': True})
except Exception as e:
logger.error(f"Error reordering HiFi instances: {e}")
@ -20052,9 +20053,9 @@ def deezer_download_test_download():
def _get_tidal_download_client():
"""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")
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:
raise RuntimeError("Tidal download client not available — ensure tidalapi is installed")
return tidal
@ -20126,7 +20127,7 @@ def qobuz_auth_login():
if not email or not password:
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)
if result['status'] == 'success':
@ -20149,7 +20150,7 @@ def qobuz_auth_token():
if not token:
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)
if result['status'] == 'success':
@ -20166,7 +20167,7 @@ def qobuz_auth_token():
def qobuz_auth_status():
"""Check if Qobuz client is authenticated."""
try:
qobuz = soulseek_client.client("qobuz")
qobuz = download_orchestrator.client("qobuz")
authenticated = qobuz.is_authenticated()
user_info = {}
if authenticated and qobuz.user_info:
@ -20183,7 +20184,7 @@ def qobuz_auth_status():
def qobuz_auth_logout():
"""Logout from Qobuz."""
try:
soulseek_client.client("qobuz").logout()
download_orchestrator.client("qobuz").logout()
_sync_qobuz_credentials_to_worker()
return jsonify({"success": True})
except Exception as e:
@ -21125,7 +21126,7 @@ def _get_metadata_fallback_client():
def get_deezer_arl_status():
"""Check if Deezer ARL is configured and authenticated."""
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():
user_data = deezer_dl._user_data or {}
return jsonify({
@ -21142,7 +21143,7 @@ def get_deezer_arl_status():
def get_deezer_arl_playlists():
"""Fetch user playlists via Deezer ARL authentication (like /api/spotify/playlists)."""
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():
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):
"""Fetch full playlist with tracks via ARL (like /api/spotify/playlist/<id>)."""
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():
return jsonify({'error': 'Deezer ARL not authenticated.'}), 401
@ -27362,8 +27363,8 @@ def get_your_artists_sources():
try:
deezer_cl = _get_deezer_client()
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")
and soulseek_client.client("deezer_dl").is_authenticated())
deezer_arl = (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
and download_orchestrator.client("deezer_dl").is_authenticated())
if deezer_oauth or deezer_arl:
connected.append('deezer')
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():
logger.info("[Your Artists] Fetching favorite artists from Deezer (OAuth)...")
artists = deezer_cl.get_user_favorite_artists(limit=200)
elif (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.client("deezer_dl")
and soulseek_client.client("deezer_dl").is_authenticated()):
elif (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
and download_orchestrator.client("deezer_dl").is_authenticated()):
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:
database.upsert_liked_artist(
artist_name=a['name'], source_service='deezer',
@ -27634,8 +27635,8 @@ def get_your_albums_sources():
try:
deezer_cl = _get_deezer_client()
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")
and soulseek_client.client("deezer_dl").is_authenticated())
deezer_arl = (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
and download_orchestrator.client("deezer_dl").is_authenticated())
if deezer_oauth or deezer_arl:
connected.append('deezer')
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():
logger.info("[Your Albums] Fetching favorite albums from Deezer (OAuth)...")
albums = deezer_cl.get_user_favorite_albums(limit=500)
elif (hasattr(soulseek_client, 'deezer_dl') and soulseek_client.client("deezer_dl")
and soulseek_client.client("deezer_dl").is_authenticated()):
elif (hasattr(download_orchestrator, 'deezer_dl') and download_orchestrator.client("deezer_dl")
and download_orchestrator.client("deezer_dl").is_authenticated()):
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:
database.upsert_liked_album(
album_name=a['album_name'], artist_name=a['artist_name'],
@ -32666,7 +32667,7 @@ register_runtime_clients(
)
_init_connection_test(
soulseek_client_obj=soulseek_client,
download_orchestrator_obj=download_orchestrator,
qobuz_worker=qobuz_enrichment_worker,
hydrabase_client_obj=hydrabase_client,
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(
matching_engine_obj=matching_engine,
soulseek_client_obj=soulseek_client,
download_orchestrator_obj=download_orchestrator,
)
_init_wishlist_failed(
engine=automation_engine,
soulseek_client_obj=soulseek_client,
download_orchestrator_obj=download_orchestrator,
sweep_fn=_sweep_empty_download_directories,
)
@ -32703,7 +32704,7 @@ _init_debug_info(
sync_states_dict=sync_states,
youtube_playlist_states_dict=youtube_playlist_states,
tidal_discovery_states_dict=tidal_discovery_states,
soulseek_client_obj=soulseek_client,
download_orchestrator_obj=download_orchestrator,
log_path=_log_path,
log_dir=_log_dir,
flask_app=app,
@ -32722,7 +32723,7 @@ _init_download_monitor(
start_next_batch_of_downloads=_start_next_batch_of_downloads,
orphaned_download_keys=_orphaned_download_keys,
missing_download_executor_obj=missing_download_executor,
soulseek_client_obj=soulseek_client,
download_orchestrator_obj=download_orchestrator,
)
# --- Hydrabase Auto-Reconnect ---

View file

@ -3432,6 +3432,7 @@ const WHATS_NEW = {
'2.4.2': [
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
{ 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: 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' },