Three fixes from on-device testing of best-quality mode: 1. clear_completed_local no longer prunes terminal tasks that belong to a STILL-ACTIVE batch (one with non-terminal work remaining). The 5-min "Clean Completed Downloads" automation was yanking completed/failed/ unverified rows out of download_tasks mid-run — and failed/cancelled aren't in library_history — so they only reappeared after the batch ended. Now the whole active batch stays intact until it finishes. 2. search_all_sources runs every source CONCURRENTLY (asyncio.gather) instead of sequentially, so the pool waits only for the slowest source (e.g. usenet/Prowlarr) in parallel rather than summing all latencies. 3. The pool log now reports per-source contribution counts (e.g. "usenet=0, hifi=11, soulseek=1") instead of just echoing the chain, so a release-level source that returns nothing for a track-title query is visible rather than appearing to have been searched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
4.8 KiB
Python
123 lines
4.8 KiB
Python
"""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:
|
|
# Protect tasks belonging to a still-active batch. A batch is "active"
|
|
# while any of its queued tasks is non-terminal (still searching /
|
|
# downloading / queued / post-processing). Pruning a batch's completed
|
|
# or failed tasks mid-run would yank them out of the Downloads page —
|
|
# and failed/cancelled rows aren't recoverable from library_history —
|
|
# so the user would never see them until the batch ended. Keep the whole
|
|
# active batch intact; it gets cleaned by a later run once it finishes.
|
|
protected_task_ids: set = set()
|
|
for batch in download_batches.values():
|
|
queue = batch.get('queue', []) if isinstance(batch, dict) else []
|
|
batch_active = any(
|
|
download_tasks.get(tid, {}).get('status') not in _TERMINAL_STATUSES
|
|
for tid in queue if tid in download_tasks
|
|
)
|
|
if batch_active:
|
|
protected_task_ids.update(queue)
|
|
|
|
task_ids_to_remove = [
|
|
tid for tid, task in download_tasks.items()
|
|
if task.get('status') in _TERMINAL_STATUSES and tid not in protected_task_ids
|
|
]
|
|
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
|