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": [ "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", "id": "7a6a40ce-c5d9-4e7e-8f71-a08141e9ce6f",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
@ -1500,10 +1443,7 @@
"error": "Download failed", "error": "Download failed",
"retry_count": 0, "retry_count": 0,
"source_url": null "source_url": null
} },
],
"active": [],
"failed": [
{ {
"id": "55e21a17-ac91-4dfe-ba96-df5c4261a9a5", "id": "55e21a17-ac91-4dfe-ba96-df5c4261a9a5",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
@ -1513,7 +1453,7 @@
"episode": 4, "episode": 4,
"title": null "title": null
}, },
"status": "failed", "status": "pending",
"priority": "normal", "priority": "normal",
"added_at": "2025-11-01T14:30:56.881920Z", "added_at": "2025-11-01T14:30:56.881920Z",
"started_at": "2025-11-01T15:04:26.488261Z", "started_at": "2025-11-01T15:04:26.488261Z",
@ -1524,5 +1464,65 @@
"source_url": null "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), _: dict = Depends(require_auth),
download_service: DownloadService = Depends(get_download_service), 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 Starts processing all pending downloads sequentially, one at a time.
can be active at a time. If the queue is empty or a download is already The queue will continue processing until all items are complete or
active, an error is returned. 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. Requires authentication.
Returns: Returns:
dict: Status message with started item ID dict: Status message confirming queue processing started
Raises: Raises:
HTTPException: 401 if not authenticated, 400 if queue is empty or 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: 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( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="No pending downloads in queue", detail="No pending downloads in queue",
@ -306,8 +310,7 @@ async def start_queue(
return { return {
"status": "success", "status": "success",
"message": "Download started", "message": "Queue processing started",
"item_id": item_id,
} }
except DownloadServiceError as e: except DownloadServiceError as e:
@ -320,7 +323,7 @@ async def start_queue(
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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)}" f"Failed to remove items: {str(e)}"
) from e ) from e
async def start_next_download(self) -> Optional[str]: async def start_queue_processing(self) -> Optional[str]:
"""Manually start the next download from pending queue. """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: 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: Raises:
DownloadServiceError: If a download is already active DownloadServiceError: If queue processing is already active
""" """
try: try:
# Check if download already active # Check if download already active
if self._active_download: if self._active_download:
raise DownloadServiceError( raise DownloadServiceError(
"A download is already in progress" "Queue processing is already active"
) )
# Check if queue is empty # Check if queue is empty
@ -377,20 +381,42 @@ class DownloadService:
logger.info("No pending downloads to start") logger.info("No pending downloads to start")
return None return None
# Get first item from queue
item = self._pending_queue.popleft()
del self._pending_items_by_id[item.id]
# Mark queue as running # Mark queue as running
self._is_stopped = False self._is_stopped = False
# Start download in background # Start queue processing in background
asyncio.create_task(self._process_download(item)) asyncio.create_task(self._process_queue())
logger.info("Queue processing started")
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( logger.info(
"Started download manually", "Processing next item from queue",
item_id=item.id, item_id=item.id,
serie=item.serie_name serie=item.serie_name,
remaining=len(self._pending_queue)
) )
# Broadcast queue status update # Broadcast queue status update
@ -406,13 +432,47 @@ class DownloadService:
}, },
) )
return item.id # 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: except Exception as e:
logger.error("Failed to start download", error=str(e)) logger.error(
raise DownloadServiceError( "Error in queue processing loop",
f"Failed to start download: {str(e)}" error=str(e),
) from 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(
"queue_completed",
{
"message": "All downloads completed",
"queue_status": queue_status.model_dump(mode="json"),
},
)
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: async def stop_downloads(self) -> None:
"""Stop processing new downloads from queue. """Stop processing new downloads from queue.

View File

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