download the queue
This commit is contained in:
parent
eaf6bb9957
commit
33aeac0141
@ -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"
|
||||||
}
|
}
|
||||||
@ -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)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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,42 +381,98 @@ 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(
|
logger.info("Queue processing started")
|
||||||
"Started download manually",
|
|
||||||
item_id=item.id,
|
|
||||||
serie=item.serie_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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()
|
queue_status = await self.get_queue_status()
|
||||||
await self._broadcast_update(
|
await self._broadcast_update(
|
||||||
"download_started",
|
"queue_completed",
|
||||||
{
|
{
|
||||||
"item_id": item.id,
|
"message": "All downloads completed",
|
||||||
"serie_name": item.serie_name,
|
|
||||||
"season": item.episode.season,
|
|
||||||
"episode": item.episode.episode,
|
|
||||||
"queue_status": queue_status.model_dump(mode="json"),
|
"queue_status": queue_status.model_dump(mode="json"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.info("Queue processing stopped by user")
|
||||||
|
|
||||||
return item.id
|
async def start_next_download(self) -> Optional[str]:
|
||||||
|
"""Legacy method - redirects to start_queue_processing.
|
||||||
|
|
||||||
except Exception as e:
|
Returns:
|
||||||
logger.error("Failed to start download", error=str(e))
|
Item ID of started download, or None if queue is empty
|
||||||
raise DownloadServiceError(
|
|
||||||
f"Failed to start download: {str(e)}"
|
Raises:
|
||||||
) from e
|
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.
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user