added remove all item from queue

This commit is contained in:
Lukas 2025-11-01 18:09:23 +01:00
parent 4dba4db344
commit 18faf3fe91
8 changed files with 155 additions and 1473 deletions

File diff suppressed because it is too large Load Diff

View File

@ -232,6 +232,40 @@ async def clear_failed(
) )
@router.delete("/pending", status_code=status.HTTP_200_OK)
async def clear_pending(
_: dict = Depends(require_auth),
download_service: DownloadService = Depends(get_download_service),
):
"""Clear all pending downloads from the queue.
Removes all pending download items from the queue. This is useful for
clearing the entire queue at once instead of removing items one by one.
Requires authentication.
Returns:
dict: Status message with count of cleared items
Raises:
HTTPException: 401 if not authenticated, 500 on service error
"""
try:
cleared_count = await download_service.clear_pending()
return {
"status": "success",
"message": f"Removed {cleared_count} pending item(s)",
"count": cleared_count,
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to clear pending items: {str(e)}",
)
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_from_queue( async def remove_from_queue(
item_id: str = Path(..., description="Download item ID to remove"), item_id: str = Path(..., description="Download item ID to remove"),

View File

@ -27,9 +27,9 @@ class DownloadStatus(str, Enum):
class DownloadPriority(str, Enum): class DownloadPriority(str, Enum):
"""Priority level for download queue items.""" """Priority level for download queue items."""
LOW = "low" LOW = "LOW"
NORMAL = "normal" NORMAL = "NORMAL"
HIGH = "high" HIGH = "HIGH"
class EpisodeIdentifier(BaseModel): class EpisodeIdentifier(BaseModel):
@ -175,9 +175,9 @@ class DownloadRequest(BaseModel):
@field_validator('priority', mode='before') @field_validator('priority', mode='before')
@classmethod @classmethod
def normalize_priority(cls, v): def normalize_priority(cls, v):
"""Normalize priority to lowercase for case-insensitive matching.""" """Normalize priority to uppercase for case-insensitive matching."""
if isinstance(v, str): if isinstance(v, str):
return v.lower() return v.upper()
return v return v

View File

@ -600,6 +600,34 @@ class DownloadService:
return count return count
async def clear_pending(self) -> int:
"""Clear all pending downloads from the queue.
Returns:
Number of items cleared
"""
count = len(self._pending_queue)
self._pending_queue.clear()
self._pending_items_by_id.clear()
logger.info("Cleared pending items", count=count)
# Save queue state
self._save_queue()
# Broadcast queue status update
if count > 0:
queue_status = await self.get_queue_status()
await self._broadcast_update(
"queue_status",
{
"action": "pending_cleared",
"cleared_count": count,
"queue_status": queue_status.model_dump(mode="json"),
},
)
return count
async def retry_failed( async def retry_failed(
self, item_ids: Optional[List[str]] = None self, item_ids: Optional[List[str]] = None
) -> List[str]: ) -> List[str]:

View File

@ -142,6 +142,10 @@ class QueueManager {
this.clearQueue('failed'); this.clearQueue('failed');
}); });
document.getElementById('clear-pending-btn').addEventListener('click', () => {
this.clearQueue('pending');
});
document.getElementById('retry-all-btn').addEventListener('click', () => { document.getElementById('retry-all-btn').addEventListener('click', () => {
this.retryAllFailed(); this.retryAllFailed();
}); });
@ -442,6 +446,14 @@ class QueueManager {
const hasFailed = (data.failed_downloads || []).length > 0; const hasFailed = (data.failed_downloads || []).length > 0;
const hasCompleted = (data.completed_downloads || []).length > 0; const hasCompleted = (data.completed_downloads || []).length > 0;
console.log('Button states update:', {
hasPending,
pendingCount: (data.pending_queue || []).length,
hasActive,
hasFailed,
hasCompleted
});
// Enable start button only if there are pending items and no active downloads // Enable start button only if there are pending items and no active downloads
document.getElementById('start-queue-btn').disabled = !hasPending || hasActive; document.getElementById('start-queue-btn').disabled = !hasPending || hasActive;
@ -458,17 +470,28 @@ class QueueManager {
document.getElementById('retry-all-btn').disabled = !hasFailed; document.getElementById('retry-all-btn').disabled = !hasFailed;
document.getElementById('clear-completed-btn').disabled = !hasCompleted; document.getElementById('clear-completed-btn').disabled = !hasCompleted;
document.getElementById('clear-failed-btn').disabled = !hasFailed; document.getElementById('clear-failed-btn').disabled = !hasFailed;
// Update clear pending button if it exists
const clearPendingBtn = document.getElementById('clear-pending-btn');
if (clearPendingBtn) {
clearPendingBtn.disabled = !hasPending;
console.log('Clear pending button updated:', { disabled: !hasPending, hasPending });
} else {
console.error('Clear pending button not found in DOM');
}
} }
async clearQueue(type) { async clearQueue(type) {
const titles = { const titles = {
completed: 'Clear Completed Downloads', completed: 'Clear Completed Downloads',
failed: 'Clear Failed Downloads' failed: 'Clear Failed Downloads',
pending: 'Remove All Pending Downloads'
}; };
const messages = { const messages = {
completed: 'Are you sure you want to clear all completed downloads?', completed: 'Are you sure you want to clear all completed downloads?',
failed: 'Are you sure you want to clear all failed downloads?' failed: 'Are you sure you want to clear all failed downloads?',
pending: 'Are you sure you want to remove all pending downloads from the queue?'
}; };
const confirmed = await this.showConfirmModal(titles[type], messages[type]); const confirmed = await this.showConfirmModal(titles[type], messages[type]);
@ -495,6 +518,16 @@ class QueueManager {
this.showToast(`Cleared ${data.count} failed downloads`, 'success'); this.showToast(`Cleared ${data.count} failed downloads`, 'success');
this.loadQueueData(); this.loadQueueData();
} else if (type === 'pending') {
const response = await this.makeAuthenticatedRequest('/api/queue/pending', {
method: 'DELETE'
});
if (!response) return;
const data = await response.json();
this.showToast(`Removed ${data.count} pending downloads`, 'success');
this.loadQueueData();
} }
} catch (error) { } catch (error) {

View File

@ -124,6 +124,10 @@
Download Queue (<span id="queue-count">0</span>) Download Queue (<span id="queue-count">0</span>)
</h2> </h2>
<div class="section-actions"> <div class="section-actions">
<button id="clear-pending-btn" class="btn btn-secondary" disabled>
<i class="fas fa-trash-alt"></i>
Remove All
</button>
<button id="start-queue-btn" class="btn btn-primary" disabled> <button id="start-queue-btn" class="btn btn-primary" disabled>
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
Start Start

View File

@ -335,6 +335,22 @@ async def test_clear_completed(authenticated_client, mock_download_service):
mock_download_service.clear_completed.assert_called_once() mock_download_service.clear_completed.assert_called_once()
@pytest.mark.asyncio
async def test_clear_pending(authenticated_client, mock_download_service):
"""Test DELETE /api/queue/pending endpoint."""
mock_download_service.clear_pending = AsyncMock(return_value=3)
response = await authenticated_client.delete("/api/queue/pending")
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert data["count"] == 3
mock_download_service.clear_pending.assert_called_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_retry_failed(authenticated_client, mock_download_service): async def test_retry_failed(authenticated_client, mock_download_service):
"""Test POST /api/queue/retry endpoint.""" """Test POST /api/queue/retry endpoint."""

View File

@ -340,6 +340,37 @@ class TestQueueControl:
assert count == 1 assert count == 1
assert len(download_service._completed_items) == 0 assert len(download_service._completed_items) == 0
@pytest.mark.asyncio
async def test_clear_pending(self, download_service):
"""Test clearing all pending downloads from the queue."""
# Add multiple items to the queue
await download_service.add_to_queue(
serie_id="series-1",
serie_folder="test-series-1",
serie_name="Test Series 1",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
await download_service.add_to_queue(
serie_id="series-2",
serie_folder="test-series-2",
serie_name="Test Series 2",
episodes=[
EpisodeIdentifier(season=1, episode=2),
EpisodeIdentifier(season=1, episode=3),
],
)
# Verify items were added
assert len(download_service._pending_queue) == 3
# Clear pending queue
count = await download_service.clear_pending()
# Verify all pending items were cleared
assert count == 3
assert len(download_service._pending_queue) == 0
assert len(download_service._pending_items_by_id) == 0
class TestPersistence: class TestPersistence:
"""Test queue persistence functionality.""" """Test queue persistence functionality."""