diff --git a/data/download_queue.json b/data/download_queue.json index 67a44f2..62d4a00 100644 --- a/data/download_queue.json +++ b/data/download_queue.json @@ -1,938 +1,6 @@ { - "pending": [ - { - "id": "9fb9775e-b32a-4135-a6ed-1d1036346f18", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 1, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.943728Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "8aa3bf69-c03d-4989-a55d-02f866d58cd8", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 2, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.943812Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "898eb7fb-db00-4145-b238-367f1618201e", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 3, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.943853Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "6df03270-6ae3-4840-a7ff-68b5f93d3506", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 4, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.943886Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "b3e75c0a-ab96-49a8-9535-f6a14495068f", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 5, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.943917Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "e52a8e4e-6312-4963-9d71-0170240f809b", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 6, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.943947Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "9c09d211-8b9f-406f-9791-4c117c4f3f79", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 7, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.943977Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "915fe96b-0b7c-499b-892b-4f0329582da3", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 8, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944005Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "733a10da-6dad-42cf-b704-d9600289717a", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 9, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944033Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "189406ee-0c60-44c9-9171-ed1c97a9a891", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 10, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944077Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "8ee756c0-3bb9-4f53-a146-680f53e38fec", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 11, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944106Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "15ca7253-ae2e-4d9c-bcf0-8139af247b06", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 1, - "episode": 12, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944133Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "72f5f99e-a7f0-4314-ba29-3e97a1c9010e", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 1, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944161Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "1b2ee092-eeed-4ec2-bbdb-521193025be6", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 2, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944189Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "4f7efd63-d484-491e-a774-c2895a4cd643", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 3, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944215Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "032b88f3-c213-4148-8f6f-8600eb7099d8", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 4, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944246Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "50df5a01-a27d-4f5b-9916-57224a53ec99", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 5, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944283Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "649e57ab-b03a-449c-aba7-3aeea4cba8da", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 6, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944311Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "78bfa2a4-3791-4676-939e-749c787498e9", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 7, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944339Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "9ee61590-c2b1-43bb-bb13-79615082946c", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 8, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944367Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "58e96572-1114-471d-a00f-0c626c73f4b2", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 9, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944394Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "e3b17f57-dd32-479a-8aab-12b1513073dd", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 10, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944422Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "2fc7ae5a-8d22-4150-9b83-dd35bef7ba4c", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 11, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944450Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "58eb4fae-b54a-4e45-a6ce-279e9840b984", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 2, - "episode": 12, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944477Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "013d610b-10ad-410d-be0e-27ef8be0c5e0", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 1, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944505Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "15fa6d6e-60eb-460f-92d3-7fc9e36e5b8c", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 2, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944533Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "1df97d61-3d97-40d3-9c23-0d2b0de0329b", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 3, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944559Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "bff1235d-812d-46ac-9608-f464277e224f", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 4, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944590Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "f606397b-ef34-4e60-8580-0491faf33d48", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 5, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944618Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "8b2b5bec-df11-4472-b92f-683c6895f231", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 6, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944644Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "5adb2c51-4a9d-41c7-b449-dbd75dea3c7d", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 7, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944672Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "bd9e959d-07bb-4001-991b-37c33cf9ff0a", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 8, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944699Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "420bb3a9-3ea2-4ab3-9342-fd39f1be21ca", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 9, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944726Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "47730909-e8e6-45f1-b5b8-834905a40f25", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 10, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944753Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "5253e107-1bdf-45ad-bc46-215af06f8ae5", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 11, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944779Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "2aba5952-96ea-481b-b53f-911c147e0c2d", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 3, - "episode": 12, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944806Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "4ab012b9-b33b-4b6d-8edd-b5bfbc7845fb", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 1, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944833Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "f3bac2f8-01af-4b91-96c8-858788a28e50", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 2, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944860Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "2642f866-0f48-486b-a6e1-9c3b60dfa454", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 3, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944887Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "6c457ef9-a9de-4f34-9dd4-d58d61a9c05a", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 4, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944914Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "5eb8c695-ef64-44ef-9508-b9acd3d7308b", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 5, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944941Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "7937162b-813b-4d18-a8df-f90118fcdad9", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 6, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944968Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "36091ee6-763a-4a7b-af3d-faae03cf8aa3", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 7, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.944994Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "2c74156f-7a8b-4d86-a018-ea70ff0f8920", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 8, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.945021Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "2ed313c6-13ff-4118-8a3c-0a88f5834430", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 9, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.945053Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "80ff29a6-129a-4d94-9d44-2fab1993aeb6", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 10, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.945081Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "11e0d511-fc6b-4fe3-8100-d36237dd8108", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 11, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.945108Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "dc115071-bde0-4b95-999e-26b20fd0edce", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 12, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.945134Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - }, - { - "id": "bfaf6a34-bb10-4607-a189-0d6bda974770", - "serie_id": "highschool-dxd", - "serie_name": "Highschool DxD", - "episode": { - "season": 4, - "episode": 13, - "title": null - }, - "status": "pending", - "priority": "normal", - "added_at": "2025-10-30T19:42:01.945161Z", - "started_at": null, - "completed_at": null, - "progress": null, - "error": null, - "retry_count": 0, - "source_url": null - } - ], + "pending": [], "active": [], "failed": [], - "timestamp": "2025-10-30T19:42:01.945400+00:00" + "timestamp": "2025-10-30T20:10:45.815431+00:00" } \ No newline at end of file diff --git a/features.md b/features.md index 4ae50b8..b861153 100644 --- a/features.md +++ b/features.md @@ -28,12 +28,19 @@ ## Download Management -- **Download Queue Page**: View and manage the current download queue +- **Download Queue Page**: View and manage the current download queue with organized sections +- **Queue List Display**: Pending downloads shown in an ordered, draggable list +- **Drag-and-Drop Reordering**: Reorder pending items by dragging them to new positions - **Download Status Display**: Real-time status updates and progress of current downloads - **Queue Operations**: Add, remove, prioritize, and reorder items in the download queue - **Queue Control**: Start, stop, pause, and resume download processing +- **Completed Downloads List**: Separate section for completed downloads with clear button +- **Failed Downloads List**: Separate section for failed downloads with retry and clear options - **Retry Failed Downloads**: Automatically retry failed downloads with configurable limits - **Clear Completed**: Remove completed downloads from the queue +- **Clear Failed**: Remove failed downloads from the queue +- **Bulk Operations**: Select and manage multiple queue items at once +- **Queue Statistics**: Real-time counters for pending, active, completed, and failed items ## Real-time Communication diff --git a/infrastructure.md b/infrastructure.md index 3449e21..1cb7ac6 100644 --- a/infrastructure.md +++ b/infrastructure.md @@ -238,10 +238,33 @@ initialization. - `POST /api/queue/stop` - Stop download queue processing - `POST /api/queue/pause` - Pause queue processing - `POST /api/queue/resume` - Resume queue processing -- `POST /api/queue/reorder` - Reorder pending queue items +- `POST /api/queue/reorder` - Reorder pending queue items (bulk or single) - `DELETE /api/queue/completed` - Clear completed downloads +- `DELETE /api/queue/failed` - Clear failed downloads - `POST /api/queue/retry` - Retry failed downloads +**Queue Reordering:** + +- Supports bulk reordering with `{"item_ids": ["id1", "id2", ...]}` payload +- Items are reordered in the exact order provided in the array +- Only affects pending (non-active) downloads +- Real-time drag-and-drop UI with visual feedback + +**Queue Organization:** + +- **Pending Queue**: Items waiting to be downloaded, displayed in order with drag handles +- **Active Downloads**: Currently downloading items with progress bars +- **Completed Downloads**: Successfully downloaded items with completion timestamps +- **Failed Downloads**: Failed items with error messages and retry options + +**Queue Display Features:** + +- Numbered position indicators for pending items +- Drag handle icons for visual reordering cues +- Real-time statistics counters (pending, active, completed, failed) +- Empty state messages with helpful hints +- Per-section action buttons (clear, retry all) + ### WebSocket - `WS /api/ws` - WebSocket connection for real-time updates diff --git a/src/server/api/download.py b/src/server/api/download.py index af4b31f..2631b87 100644 --- a/src/server/api/download.py +++ b/src/server/api/download.py @@ -208,6 +208,40 @@ async def clear_completed( ) +@router.delete("/failed", status_code=status.HTTP_200_OK) +async def clear_failed( + _: dict = Depends(require_auth), + download_service: DownloadService = Depends(get_download_service), +): + """Clear failed downloads from history. + + Removes all failed download items from the queue history. This helps + keep the queue display clean and manageable. + + 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_failed() + + return { + "status": "success", + "message": f"Cleared {cleared_count} failed item(s)", + "count": cleared_count, + } + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to clear failed items: {str(e)}", + ) + + @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_from_queue( item_id: str = Path(..., description="Download item ID to remove"), @@ -485,28 +519,50 @@ async def reorder_queue( _: dict = Depends(require_auth), download_service: DownloadService = Depends(get_download_service), ): - """Reorder an item in the pending queue. + """Reorder items in the pending queue. - Changes the position of a pending download item in the queue. This only - affects items that haven't started downloading yet. The position is - 0-based. + Changes the order of pending download items in the queue. This only + affects items that haven't started downloading yet. Supports both + bulk reordering with item_ids array and single item reorder. Requires authentication. Args: - request: Item ID and new position in queue + request: Either {"item_ids": ["id1", "id2", ...]} for bulk reorder + or {"item_id": "id", "new_position": 0} for single item Returns: - dict: Status message indicating item has been reordered + dict: Status message indicating items have been reordered Raises: HTTPException: 401 if not authenticated, 404 if item not found, 400 for invalid request, 500 on service error """ try: - # Support legacy bulk reorder payload used by some integration tests: - # {"item_order": ["id1", "id2", ...]} - if "item_order" in request: + # Support new bulk reorder payload: {"item_ids": ["id1", "id2", ...]} + if "item_ids" in request: + item_order = request.get("item_ids", []) + if not isinstance(item_order, list): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="item_ids must be a list of item IDs", + ) + + success = await download_service.reorder_queue_bulk(item_order) + + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="One or more items in item_ids were not found in pending queue", + ) + + return { + "status": "success", + "message": "Queue reordered successfully", + } + + # Support legacy bulk reorder payload: {"item_order": ["id1", "id2", ...]} + elif "item_order" in request: item_order = request.get("item_order", []) if not isinstance(item_order, list): raise HTTPException( @@ -515,6 +571,17 @@ async def reorder_queue( ) success = await download_service.reorder_queue_bulk(item_order) + + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="One or more items in item_order were not found in pending queue", + ) + + return { + "status": "success", + "message": "Queue item reordered successfully", + } else: # Fallback to single-item reorder shape # Validate request @@ -531,25 +598,16 @@ async def reorder_queue( new_position=req.new_position, ) - if not success: - # Provide an appropriate 404 message depending on request shape - if "item_order" in request: - detail = ( - "One or more items in item_order were not " - "found in pending queue" + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item {req.item_id} not found in pending queue", ) - else: - detail = f"Item {req.item_id} not found in pending queue" - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=detail, - ) - - return { - "status": "success", - "message": "Queue item reordered successfully", - } + return { + "status": "success", + "message": "Queue item reordered successfully", + } except DownloadServiceError as e: raise HTTPException( @@ -596,6 +654,7 @@ async def retry_failed( return { "status": "success", "message": f"Retrying {len(retried_ids)} failed item(s)", + "retried_count": len(retried_ids), "retried_ids": retried_ids, } diff --git a/src/server/services/download_service.py b/src/server/services/download_service.py index 2ee76d1..9da8bd8 100644 --- a/src/server/services/download_service.py +++ b/src/server/services/download_service.py @@ -586,6 +586,30 @@ class DownloadService: return count + async def clear_failed(self) -> int: + """Clear failed downloads from history. + + Returns: + Number of items cleared + """ + count = len(self._failed_items) + self._failed_items.clear() + logger.info("Cleared failed items", count=count) + + # Broadcast queue status update + if count > 0: + queue_status = await self.get_queue_status() + await self._broadcast_update( + "queue_status", + { + "action": "failed_cleared", + "cleared_count": count, + "queue_status": queue_status.model_dump(mode="json"), + }, + ) + + return count + async def retry_failed( self, item_ids: Optional[List[str]] = None ) -> List[str]: diff --git a/src/server/web/static/css/styles.css b/src/server/web/static/css/styles.css index 0e4ad5b..0630cae 100644 --- a/src/server/web/static/css/styles.css +++ b/src/server/web/static/css/styles.css @@ -1218,6 +1218,52 @@ body { background: linear-gradient(90deg, rgba(var(--color-accent-rgb), 0.05) 0%, transparent 10%); } +/* Drag and Drop Styles */ +.draggable-item { + cursor: move; + user-select: none; +} + +.draggable-item.dragging { + opacity: 0.5; + transform: scale(0.98); + cursor: grabbing; +} + +.draggable-item.drag-over { + border-top: 3px solid var(--color-primary); + margin-top: 8px; +} + +.drag-handle { + position: absolute; + left: 8px; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-tertiary); + cursor: grab; + font-size: 1.2rem; + padding: var(--spacing-xs); + transition: color var(--transition-duration); +} + +.drag-handle:hover { + color: var(--color-primary); +} + +.drag-handle:active { + cursor: grabbing; +} + +.sortable-list { + position: relative; + min-height: 100px; +} + +.pending-queue-list { + position: relative; +} + .download-header { display: flex; justify-content: space-between; @@ -1261,11 +1307,11 @@ body { .queue-position { position: absolute; top: var(--spacing-sm); - left: var(--spacing-sm); + left: 48px; background: var(--color-warning); color: white; - width: 24px; - height: 24px; + width: 28px; + height: 28px; border-radius: 50%; display: flex; align-items: center; @@ -1275,7 +1321,18 @@ body { } .download-card.pending .download-info { - margin-left: 40px; + margin-left: 80px; +} + +.download-card.pending .download-header { + padding-left: 0; +} + +.empty-state small { + display: block; + margin-top: var(--spacing-sm); + font-size: var(--font-size-small); + opacity: 0.7; } /* Progress Bars */ diff --git a/src/server/web/static/js/queue.js b/src/server/web/static/js/queue.js index 8a9b91c..37ae169 100644 --- a/src/server/web/static/js/queue.js +++ b/src/server/web/static/js/queue.js @@ -7,6 +7,8 @@ class QueueManager { this.socket = null; this.refreshInterval = null; this.isReordering = false; + this.draggedElement = null; + this.draggedId = null; this.init(); } @@ -17,6 +19,7 @@ class QueueManager { this.initTheme(); this.startRefreshTimer(); this.loadQueueData(); + this.initDragAndDrop(); } initSocket() { @@ -249,6 +252,11 @@ class QueueManager { document.getElementById('completed-items').textContent = stats.completed_items || 0; document.getElementById('failed-items').textContent = stats.failed_items || 0; + // Update section counts + document.getElementById('queue-count').textContent = (data.pending_queue || []).length; + document.getElementById('completed-count').textContent = stats.completed_items || 0; + document.getElementById('failed-count').textContent = stats.failed_items || 0; + document.getElementById('current-speed').textContent = stats.current_speed || '0 MB/s'; document.getElementById('average-speed').textContent = stats.average_speed || '0 MB/s'; @@ -331,12 +339,16 @@ class QueueManager {
No items in queue
+ Add episodes from the main page to start downloadingNo items in queue
+ Add episodes from the main page to start downloading