download the queue

This commit is contained in:
Lukas 2025-11-01 16:13:28 +01:00
parent eaf6bb9957
commit 33aeac0141
4 changed files with 176 additions and 108 deletions

View File

@ -1,62 +1,5 @@
{
"pending": [
{
"id": "467b39ac-48be-48c5-a04c-ce94b6f6c0d9",
"serie_id": "highschool-dxd",
"serie_name": "Highschool DxD",
"episode": {
"season": 3,
"episode": 5,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-11-01T14:30:56.881950Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "4c0b9bd0-52ad-4833-baf7-19e2c920f94f",
"serie_id": "highschool-dxd",
"serie_name": "Highschool DxD",
"episode": {
"season": 3,
"episode": 6,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-11-01T14:30:56.881979Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "fabf8a46-6588-4dff-959d-de5e2bf48e29",
"serie_id": "highschool-dxd",
"serie_name": "Highschool DxD",
"episode": {
"season": 3,
"episode": 7,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-11-01T14:30:56.882013Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "7a6a40ce-c5d9-4e7e-8f71-a08141e9ce6f",
"serie_id": "highschool-dxd",
@ -1500,10 +1443,7 @@
"error": "Download failed",
"retry_count": 0,
"source_url": null
}
],
"active": [],
"failed": [
},
{
"id": "55e21a17-ac91-4dfe-ba96-df5c4261a9a5",
"serie_id": "highschool-dxd",
@ -1513,7 +1453,7 @@
"episode": 4,
"title": null
},
"status": "failed",
"status": "pending",
"priority": "normal",
"added_at": "2025-11-01T14:30:56.881920Z",
"started_at": "2025-11-01T15:04:26.488261Z",
@ -1524,5 +1464,65 @@
"source_url": null
}
],
"timestamp": "2025-11-01T15:04:43.998876+00:00"
"active": [],
"failed": [
{
"id": "467b39ac-48be-48c5-a04c-ce94b6f6c0d9",
"serie_id": "highschool-dxd",
"serie_name": "Highschool DxD",
"episode": {
"season": 3,
"episode": 5,
"title": null
},
"status": "failed",
"priority": "normal",
"added_at": "2025-11-01T14:30:56.881950Z",
"started_at": "2025-11-01T15:12:21.534113Z",
"completed_at": "2025-11-01T15:12:30.481928Z",
"progress": null,
"error": "Download failed",
"retry_count": 0,
"source_url": null
},
{
"id": "4c0b9bd0-52ad-4833-baf7-19e2c920f94f",
"serie_id": "highschool-dxd",
"serie_name": "Highschool DxD",
"episode": {
"season": 3,
"episode": 6,
"title": null
},
"status": "failed",
"priority": "normal",
"added_at": "2025-11-01T14:30:56.881979Z",
"started_at": "2025-11-01T15:12:31.484945Z",
"completed_at": "2025-11-01T15:12:39.118803Z",
"progress": null,
"error": "Download failed",
"retry_count": 0,
"source_url": null
},
{
"id": "fabf8a46-6588-4dff-959d-de5e2bf48e29",
"serie_id": "highschool-dxd",
"serie_name": "Highschool DxD",
"episode": {
"season": 3,
"episode": 7,
"title": null
},
"status": "failed",
"priority": "normal",
"added_at": "2025-11-01T14:30:56.882013Z",
"started_at": "2025-11-01T15:12:40.122525Z",
"completed_at": "2025-11-01T15:12:46.419411Z",
"progress": null,
"error": "Download failed",
"retry_count": 0,
"source_url": null
}
],
"timestamp": "2025-11-01T15:12:46.420241+00:00"
}

View File

@ -280,25 +280,29 @@ async def start_queue(
_: dict = Depends(require_auth),
download_service: DownloadService = Depends(get_download_service),
):
"""Start the next download from pending queue.
"""Start automatic queue processing.
Manually starts the first pending download in the queue. Only one download
can be active at a time. If the queue is empty or a download is already
active, an error is returned.
Starts processing all pending downloads sequentially, one at a time.
The queue will continue processing until all items are complete or
the queue is manually stopped. Processing continues even if the browser
is closed.
Only one download can be active at a time. If a download is already
active or queue processing is running, an error is returned.
Requires authentication.
Returns:
dict: Status message with started item ID
dict: Status message confirming queue processing started
Raises:
HTTPException: 401 if not authenticated, 400 if queue is empty or
download already active, 500 on service error
processing already active, 500 on service error
"""
try:
item_id = await download_service.start_next_download()
result = await download_service.start_queue_processing()
if item_id is None:
if result is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No pending downloads in queue",
@ -306,8 +310,7 @@ async def start_queue(
return {
"status": "success",
"message": "Download started",
"item_id": item_id,
"message": "Queue processing started",
}
except DownloadServiceError as e:
@ -320,7 +323,7 @@ async def start_queue(
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to start download: {str(e)}",
detail=f"Failed to start queue processing: {str(e)}",
)

View File

@ -356,20 +356,24 @@ class DownloadService:
f"Failed to remove items: {str(e)}"
) from e
async def start_next_download(self) -> Optional[str]:
"""Manually start the next download from pending queue.
async def start_queue_processing(self) -> Optional[str]:
"""Start automatic queue processing of all pending downloads.
This will process all pending downloads one by one until the queue
is empty or stopped. The processing continues even if the browser
is closed.
Returns:
Item ID of started download, or None if queue is empty
Item ID of first started download, or None if queue is empty
Raises:
DownloadServiceError: If a download is already active
DownloadServiceError: If queue processing is already active
"""
try:
# Check if download already active
if self._active_download:
raise DownloadServiceError(
"A download is already in progress"
"Queue processing is already active"
)
# Check if queue is empty
@ -377,42 +381,98 @@ class DownloadService:
logger.info("No pending downloads to start")
return None
# Get first item from queue
item = self._pending_queue.popleft()
del self._pending_items_by_id[item.id]
# Mark queue as running
self._is_stopped = False
# Start download in background
asyncio.create_task(self._process_download(item))
# Start queue processing in background
asyncio.create_task(self._process_queue())
logger.info(
"Started download manually",
item_id=item.id,
serie=item.serie_name
)
logger.info("Queue processing started")
# Broadcast queue status update
return "queue_started"
except Exception as e:
logger.error("Failed to start queue processing", error=str(e))
raise DownloadServiceError(
f"Failed to start queue processing: {str(e)}"
) from e
async def _process_queue(self) -> None:
"""Process all items in the queue sequentially.
This runs continuously until the queue is empty or stopped.
Each download is processed one at a time, and the next one starts
automatically after the previous one completes.
"""
logger.info("Queue processor started")
while not self._is_stopped and len(self._pending_queue) > 0:
try:
# Get next item from queue
item = self._pending_queue.popleft()
del self._pending_items_by_id[item.id]
logger.info(
"Processing next item from queue",
item_id=item.id,
serie=item.serie_name,
remaining=len(self._pending_queue)
)
# Broadcast queue status update
queue_status = await self.get_queue_status()
await self._broadcast_update(
"download_started",
{
"item_id": item.id,
"serie_name": item.serie_name,
"season": item.episode.season,
"episode": item.episode.episode,
"queue_status": queue_status.model_dump(mode="json"),
},
)
# Process the download (this will wait until complete)
await self._process_download(item)
# Small delay between downloads
await asyncio.sleep(1)
except Exception as e:
logger.error(
"Error in queue processing loop",
error=str(e),
exc_info=True
)
# Continue with next item even if one fails
await asyncio.sleep(2)
# Queue processing completed
self._is_stopped = True
if len(self._pending_queue) == 0:
logger.info("Queue processing completed - all items processed")
queue_status = await self.get_queue_status()
await self._broadcast_update(
"download_started",
"queue_completed",
{
"item_id": item.id,
"serie_name": item.serie_name,
"season": item.episode.season,
"episode": item.episode.episode,
"message": "All downloads completed",
"queue_status": queue_status.model_dump(mode="json"),
},
)
return item.id
except Exception as e:
logger.error("Failed to start download", error=str(e))
raise DownloadServiceError(
f"Failed to start download: {str(e)}"
) from e
else:
logger.info("Queue processing stopped by user")
async def start_next_download(self) -> Optional[str]:
"""Legacy method - redirects to start_queue_processing.
Returns:
Item ID of started download, or None if queue is empty
Raises:
DownloadServiceError: If a download is already active
"""
return await self.start_queue_processing()
async def stop_downloads(self) -> None:
"""Stop processing new downloads from queue.

View File

@ -97,6 +97,11 @@ class QueueManager {
this.showToast('All downloads completed!', 'success');
this.loadQueueData(); // Refresh data
});
this.socket.on('queue_completed', () => {
this.showToast('All downloads completed!', 'success');
this.loadQueueData(); // Refresh data
});
this.socket.on('download_stop_requested', () => {
this.showToast('Stopping downloads...', 'info');
@ -592,7 +597,7 @@ class QueueManager {
const data = await response.json();
if (data.status === 'success') {
this.showToast('Download started', 'success');
this.showToast('Queue processing started - all items will download automatically', 'success');
// Update UI
document.getElementById('start-queue-btn').style.display = 'none';
@ -601,11 +606,11 @@ class QueueManager {
this.loadQueueData(); // Refresh display
} else {
this.showToast(`Failed to start download: ${data.message || 'Unknown error'}`, 'error');
this.showToast(`Failed to start queue: ${data.message || 'Unknown error'}`, 'error');
}
} catch (error) {
console.error('Error starting download:', error);
this.showToast('Failed to start download', 'error');
console.error('Error starting queue:', error);
this.showToast('Failed to start queue processing', 'error');
}
}