"""Download cancellation + clear helpers. Four discrete operations lifted from web_server.py: - `cancel_single_download(client, run_async, download_id, username)` — cancel one slskd transfer. - `cancel_all_active(client, run_async, sweep_callback)` — cancel every active slskd transfer, then clear the now-cancelled ones, then sweep empty download directories. - `clear_finished_active(client, run_async, sweep_callback)` — clear all terminal transfers from slskd (no cancel step), sweep dirs. - `clear_completed_local()` — prune terminal-status tasks from the local `download_tasks` tracker, drop empty batches, drop their locks. Pure local mutation, doesn't touch slskd. The slskd-touching helpers take the soulseek client and run_async callback explicitly; the local helper imports its globals directly from `core.runtime_state` since those are module-level shared state and every caller sees the same dict. Out of scope for this PR (deferred to the batch-lifecycle lift): - `cancel_download_task` (calls _on_download_completed) - `cancel_task_v2` + `_atomic_cancel_task` (manipulate batch active_count) """ from __future__ import annotations import logging from typing import Callable from core.runtime_state import ( batch_locks, download_batches, download_tasks, tasks_lock, ) logger = logging.getLogger(__name__) _TERMINAL_STATUSES = { 'completed', 'failed', 'not_found', 'cancelled', 'skipped', 'already_owned', } def cancel_single_download(download_orchestrator, run_async: Callable, download_id: str, username: str) -> bool: """Cancel one specific slskd download (with `remove=True`).""" logger.info( f"[CancelTrigger:api.manual_cancel_single] download_id={download_id} username={username}" ) return run_async(download_orchestrator.cancel_download(download_id, username, remove=True)) 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(download_orchestrator.cancel_all_downloads()) if not cancel_success: return False, "Failed to cancel active downloads." run_async(download_orchestrator.clear_all_completed_downloads()) sweep_callback() return True, "All downloads cancelled and cleared." 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(download_orchestrator.clear_all_completed_downloads()) if success: sweep_callback() return success def clear_completed_local() -> int: """Remove completed/failed/cancelled tasks from the local tracker. Also prunes batches whose queues are now empty, and removes the matching `batch_locks` entry. Returns the number of cleared tasks. """ cleared = 0 with tasks_lock: task_ids_to_remove = [ tid for tid, task in download_tasks.items() if task.get('status') in _TERMINAL_STATUSES ] for tid in task_ids_to_remove: del download_tasks[tid] cleared += 1 empty_batches = [] for bid, batch in download_batches.items(): remaining = [t for t in batch.get('queue', []) if t in download_tasks] if not remaining: empty_batches.append(bid) else: batch['queue'] = remaining for bid in empty_batches: del download_batches[bid] if bid in batch_locks: del batch_locks[bid] return cleared