feat(downloads): add /downloads/queue/status endpoint and integrate queue lifecycle

- New GET /api/lm/downloads/queue/status handler for non-terminal status
  transitions (queued -> downloading, downloading -> paused, etc.)
- Queue lifecycle auto-integration in DownloadManager._download_with_semaphore:
  downloading -> SQLite update_status('downloading') on semaphore acquire
  completed -> complete_download('completed') on success
  canceled -> complete_download('canceled') on CancelledError
  failed -> complete_download('failed') on Exception
- All queue operations wrapped in try/except to never break the download flow
This commit is contained in:
Will Miao
2026-06-17 23:04:30 +08:00
parent 75298a402f
commit 8314b9bedb
3 changed files with 92 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ from .metadata_service import get_default_metadata_provider, get_metadata_provid
from .downloader import get_downloader, DownloadProgress, DownloadStreamControl
from .aria2_downloader import Aria2Error, get_aria2_downloader
from .aria2_transfer_state import Aria2TransferStateStore
from .download_queue_service import DownloadQueueService
# Download to temporary file first
import tempfile
@@ -360,6 +361,15 @@ class DownloadManager:
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
await self._persist_aria2_state(task_id)
# Update SQLite queue status to 'downloading'
try:
queue_service = await DownloadQueueService.get_instance()
await queue_service.update_status(task_id, "downloading")
except Exception:
logger.warning(
"Failed to update queue status for %s", task_id, exc_info=True
)
# Use original download implementation
try:
# Check for cancellation before starting
@@ -396,6 +406,22 @@ class DownloadManager:
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
await self._persist_aria2_state(task_id)
# Move queue item to history on completion
try:
queue_service = await DownloadQueueService.get_instance()
await queue_service.complete_download(
download_id=task_id,
status=result.get("status", "completed") if result.get("success") else "failed",
error=result.get("error") if not result.get("success") else None,
file_path=result.get("file_path"),
bytes_downloaded=self._active_downloads.get(task_id, {}).get("bytes_downloaded", 0),
total_bytes=self._active_downloads.get(task_id, {}).get("total_bytes"),
)
except Exception:
logger.warning(
"Failed to complete queue item for %s", task_id, exc_info=True
)
return result
except asyncio.CancelledError:
# Handle cancellation
@@ -404,6 +430,19 @@ class DownloadManager:
self._active_downloads[task_id]["bytes_per_second"] = 0.0
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
await self._persist_aria2_state(task_id)
# Move queue item to history as canceled
try:
queue_service = await DownloadQueueService.get_instance()
await queue_service.complete_download(
download_id=task_id,
status="canceled",
)
except Exception:
logger.warning(
"Failed to cancel queue item for %s", task_id, exc_info=True
)
logger.info(f"Download cancelled for task {task_id}")
raise
except Exception as e:
@@ -417,6 +456,22 @@ class DownloadManager:
self._active_downloads[task_id]["bytes_per_second"] = 0.0
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
await self._persist_aria2_state(task_id)
# Move queue item to history as failed
try:
queue_service = await DownloadQueueService.get_instance()
await queue_service.complete_download(
download_id=task_id,
status="failed",
error=str(e),
bytes_downloaded=self._active_downloads.get(task_id, {}).get("bytes_downloaded", 0),
total_bytes=self._active_downloads.get(task_id, {}).get("total_bytes"),
)
except Exception:
logger.warning(
"Failed to complete queue item for %s", task_id, exc_info=True
)
return {"success": False, "error": str(e)}
finally:
# Schedule cleanup of download record after delay