mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-25 06:51:26 -03:00
41ed03e5c6
- Log expected "GID not found" tellStatus probes at DEBUG, and treat a forgotten GID as permanent so the poll loop recovers immediately instead of burning 4 retries x 3s of ERROR lines per cycle - cancel_download tolerates a forgotten GID and always pops the in-memory transfer so concurrent polls cannot re-register a cancelled download - Restore sweep deletes aria2 state records with no resolvable target path instead of skipping them forever - Clearing the download queue now also cancels in-memory tasks, removes live aria2 transfers and drops persisted state for the cleared ids (partial files on disk are preserved)
208 lines
7.5 KiB
Python
208 lines
7.5 KiB
Python
"""Service wrapper for coordinating download lifecycle events."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Awaitable, Callable, Dict, Iterable, Optional
|
|
|
|
from .downloader import DownloadProgress
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DownloadCoordinator:
|
|
"""Manage download scheduling, cancellation and introspection."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
ws_manager,
|
|
download_manager_factory: Callable[[], Awaitable[Any]],
|
|
) -> None:
|
|
self._ws_manager = ws_manager
|
|
self._download_manager_factory = download_manager_factory
|
|
|
|
async def schedule_download(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Schedule a download using the provided payload."""
|
|
|
|
download_manager = await self._download_manager_factory()
|
|
|
|
download_id = payload.get("download_id") or self._ws_manager.generate_download_id()
|
|
payload.setdefault("download_id", download_id)
|
|
|
|
async def progress_callback(progress: Any, snapshot: Optional[DownloadProgress] = None) -> None:
|
|
percent = 0.0
|
|
metrics: Optional[DownloadProgress] = None
|
|
|
|
if isinstance(progress, DownloadProgress):
|
|
metrics = progress
|
|
percent = progress.percent_complete
|
|
elif isinstance(snapshot, DownloadProgress):
|
|
metrics = snapshot
|
|
percent = snapshot.percent_complete
|
|
else:
|
|
try:
|
|
percent = float(progress)
|
|
except (TypeError, ValueError):
|
|
percent = 0.0
|
|
|
|
payload: Dict[str, Any] = {
|
|
"status": "progress",
|
|
"progress": round(percent),
|
|
"download_id": download_id,
|
|
}
|
|
|
|
if metrics is not None:
|
|
payload.update(
|
|
{
|
|
"bytes_downloaded": metrics.bytes_downloaded,
|
|
"total_bytes": metrics.total_bytes,
|
|
"bytes_per_second": metrics.bytes_per_second,
|
|
}
|
|
)
|
|
|
|
await self._ws_manager.broadcast_download_progress(
|
|
download_id,
|
|
payload,
|
|
)
|
|
|
|
model_id = self._parse_optional_int(payload.get("model_id"), "model_id")
|
|
model_version_id = self._parse_optional_int(
|
|
payload.get("model_version_id"), "model_version_id"
|
|
)
|
|
|
|
if model_id is None and model_version_id is None:
|
|
raise ValueError(
|
|
"Missing required parameter: Please provide either 'model_id' or 'model_version_id'"
|
|
)
|
|
|
|
result = await download_manager.download_from_civitai(
|
|
model_id=model_id,
|
|
model_version_id=model_version_id,
|
|
save_dir=payload.get("model_root"),
|
|
relative_path=payload.get("relative_path", ""),
|
|
use_default_paths=payload.get("use_default_paths", False),
|
|
use_save_dir_as_root=payload.get("use_save_dir_as_root", False),
|
|
progress_callback=progress_callback,
|
|
download_id=download_id,
|
|
source=payload.get("source"),
|
|
# Normalize falsy file_params (e.g. {}) to None so download gates
|
|
# treat it as "no explicit file selection" (#1058).
|
|
file_params=payload.get("file_params") or None,
|
|
)
|
|
|
|
result["download_id"] = download_id
|
|
return result
|
|
|
|
async def cancel_download(self, download_id: str) -> Dict[str, Any]:
|
|
"""Cancel an active download and emit a broadcast event."""
|
|
|
|
download_manager = await self._download_manager_factory()
|
|
result = await download_manager.cancel_download(download_id)
|
|
|
|
await self._ws_manager.broadcast_download_progress(
|
|
download_id,
|
|
{
|
|
"status": "cancelled",
|
|
"progress": 0,
|
|
"download_id": download_id,
|
|
"message": "Download cancelled by user",
|
|
},
|
|
)
|
|
|
|
return result
|
|
|
|
async def skip_download(self, download_id: str) -> Dict[str, Any]:
|
|
"""Skip a download while preserving all partial files on disk."""
|
|
download_manager = await self._download_manager_factory()
|
|
result = await download_manager.skip_download(download_id)
|
|
|
|
await self._ws_manager.broadcast_download_progress(
|
|
download_id,
|
|
{
|
|
"status": "skipped",
|
|
"progress": 0,
|
|
"download_id": download_id,
|
|
"message": "Download skipped by user (partial files preserved)",
|
|
},
|
|
)
|
|
|
|
return result
|
|
|
|
async def pause_download(self, download_id: str) -> Dict[str, Any]:
|
|
"""Pause an active download and notify listeners."""
|
|
|
|
download_manager = await self._download_manager_factory()
|
|
result = await download_manager.pause_download(download_id)
|
|
|
|
if result.get("success"):
|
|
cached_progress = self._ws_manager.get_download_progress(download_id) or {}
|
|
payload: Dict[str, Any] = {
|
|
"status": "paused",
|
|
"progress": cached_progress.get("progress", 0),
|
|
"download_id": download_id,
|
|
"message": "Download paused by user",
|
|
}
|
|
|
|
for field in ("bytes_downloaded", "total_bytes", "bytes_per_second"):
|
|
if field in cached_progress:
|
|
payload[field] = cached_progress[field]
|
|
|
|
payload["bytes_per_second"] = 0.0
|
|
|
|
await self._ws_manager.broadcast_download_progress(download_id, payload)
|
|
|
|
return result
|
|
|
|
async def resume_download(self, download_id: str) -> Dict[str, Any]:
|
|
"""Resume a paused download and notify listeners."""
|
|
|
|
download_manager = await self._download_manager_factory()
|
|
result = await download_manager.resume_download(download_id)
|
|
|
|
if result.get("success"):
|
|
cached_progress = self._ws_manager.get_download_progress(download_id) or {}
|
|
payload: Dict[str, Any] = {
|
|
"status": "downloading",
|
|
"progress": cached_progress.get("progress", 0),
|
|
"download_id": download_id,
|
|
"message": "Download resumed by user",
|
|
}
|
|
|
|
for field in ("bytes_downloaded", "total_bytes"):
|
|
if field in cached_progress:
|
|
payload[field] = cached_progress[field]
|
|
|
|
payload["bytes_per_second"] = cached_progress.get("bytes_per_second", 0.0)
|
|
|
|
await self._ws_manager.broadcast_download_progress(download_id, payload)
|
|
|
|
return result
|
|
|
|
async def list_active_downloads(self) -> Dict[str, Any]:
|
|
"""Return the active download map from the underlying manager."""
|
|
|
|
download_manager = await self._download_manager_factory()
|
|
return await download_manager.get_active_downloads()
|
|
|
|
async def discard_cleared_downloads(self, download_ids: Iterable[str]) -> int:
|
|
"""Tear down in-memory/aria2 tracking for queue-cleared downloads."""
|
|
|
|
if not download_ids:
|
|
return 0
|
|
download_manager = await self._download_manager_factory()
|
|
return await download_manager.discard_cleared_downloads(download_ids)
|
|
|
|
def _parse_optional_int(self, value: Any, field: str) -> Optional[int]:
|
|
"""Parse an optional integer from user input."""
|
|
|
|
if value is None or value == "":
|
|
return None
|
|
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError) as exc:
|
|
raise ValueError(f"Invalid {field}: Must be an integer") from exc
|
|
|