fix download
This commit is contained in:
parent
dbb5701660
commit
727486795c
@ -1,938 +1,6 @@
|
|||||||
{
|
{
|
||||||
"pending": [
|
"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
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"active": [],
|
"active": [],
|
||||||
"failed": [],
|
"failed": [],
|
||||||
"timestamp": "2025-10-30T19:42:01.945400+00:00"
|
"timestamp": "2025-10-30T20:10:45.815431+00:00"
|
||||||
}
|
}
|
||||||
@ -28,12 +28,19 @@
|
|||||||
|
|
||||||
## Download Management
|
## 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
|
- **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 Operations**: Add, remove, prioritize, and reorder items in the download queue
|
||||||
- **Queue Control**: Start, stop, pause, and resume download processing
|
- **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
|
- **Retry Failed Downloads**: Automatically retry failed downloads with configurable limits
|
||||||
- **Clear Completed**: Remove completed downloads from the queue
|
- **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
|
## Real-time Communication
|
||||||
|
|
||||||
|
|||||||
@ -238,10 +238,33 @@ initialization.
|
|||||||
- `POST /api/queue/stop` - Stop download queue processing
|
- `POST /api/queue/stop` - Stop download queue processing
|
||||||
- `POST /api/queue/pause` - Pause queue processing
|
- `POST /api/queue/pause` - Pause queue processing
|
||||||
- `POST /api/queue/resume` - Resume 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/completed` - Clear completed downloads
|
||||||
|
- `DELETE /api/queue/failed` - Clear failed downloads
|
||||||
- `POST /api/queue/retry` - Retry 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
|
### WebSocket
|
||||||
|
|
||||||
- `WS /api/ws` - WebSocket connection for real-time updates
|
- `WS /api/ws` - WebSocket connection for real-time updates
|
||||||
|
|||||||
@ -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)
|
@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"),
|
||||||
@ -485,28 +519,50 @@ async def reorder_queue(
|
|||||||
_: dict = Depends(require_auth),
|
_: dict = Depends(require_auth),
|
||||||
download_service: DownloadService = Depends(get_download_service),
|
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
|
Changes the order of pending download items in the queue. This only
|
||||||
affects items that haven't started downloading yet. The position is
|
affects items that haven't started downloading yet. Supports both
|
||||||
0-based.
|
bulk reordering with item_ids array and single item reorder.
|
||||||
|
|
||||||
Requires authentication.
|
Requires authentication.
|
||||||
|
|
||||||
Args:
|
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:
|
Returns:
|
||||||
dict: Status message indicating item has been reordered
|
dict: Status message indicating items have been reordered
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 401 if not authenticated, 404 if item not found,
|
HTTPException: 401 if not authenticated, 404 if item not found,
|
||||||
400 for invalid request, 500 on service error
|
400 for invalid request, 500 on service error
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Support legacy bulk reorder payload used by some integration tests:
|
# Support new bulk reorder payload: {"item_ids": ["id1", "id2", ...]}
|
||||||
# {"item_order": ["id1", "id2", ...]}
|
if "item_ids" in request:
|
||||||
if "item_order" 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", [])
|
item_order = request.get("item_order", [])
|
||||||
if not isinstance(item_order, list):
|
if not isinstance(item_order, list):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -515,6 +571,17 @@ async def reorder_queue(
|
|||||||
)
|
)
|
||||||
|
|
||||||
success = await download_service.reorder_queue_bulk(item_order)
|
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:
|
else:
|
||||||
# Fallback to single-item reorder shape
|
# Fallback to single-item reorder shape
|
||||||
# Validate request
|
# Validate request
|
||||||
@ -532,18 +599,9 @@ async def reorder_queue(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
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"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
detail = f"Item {req.item_id} not found in pending queue"
|
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=detail,
|
detail=f"Item {req.item_id} not found in pending queue",
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -596,6 +654,7 @@ async def retry_failed(
|
|||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Retrying {len(retried_ids)} failed item(s)",
|
"message": f"Retrying {len(retried_ids)} failed item(s)",
|
||||||
|
"retried_count": len(retried_ids),
|
||||||
"retried_ids": retried_ids,
|
"retried_ids": retried_ids,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -586,6 +586,30 @@ class DownloadService:
|
|||||||
|
|
||||||
return count
|
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(
|
async def retry_failed(
|
||||||
self, item_ids: Optional[List[str]] = None
|
self, item_ids: Optional[List[str]] = None
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
|
|||||||
@ -1218,6 +1218,52 @@ body {
|
|||||||
background: linear-gradient(90deg, rgba(var(--color-accent-rgb), 0.05) 0%, transparent 10%);
|
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 {
|
.download-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -1261,11 +1307,11 @@ body {
|
|||||||
.queue-position {
|
.queue-position {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--spacing-sm);
|
top: var(--spacing-sm);
|
||||||
left: var(--spacing-sm);
|
left: 48px;
|
||||||
background: var(--color-warning);
|
background: var(--color-warning);
|
||||||
color: white;
|
color: white;
|
||||||
width: 24px;
|
width: 28px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1275,7 +1321,18 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.download-card.pending .download-info {
|
.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 */
|
/* Progress Bars */
|
||||||
|
|||||||
@ -7,6 +7,8 @@ class QueueManager {
|
|||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.refreshInterval = null;
|
this.refreshInterval = null;
|
||||||
this.isReordering = false;
|
this.isReordering = false;
|
||||||
|
this.draggedElement = null;
|
||||||
|
this.draggedId = null;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@ -17,6 +19,7 @@ class QueueManager {
|
|||||||
this.initTheme();
|
this.initTheme();
|
||||||
this.startRefreshTimer();
|
this.startRefreshTimer();
|
||||||
this.loadQueueData();
|
this.loadQueueData();
|
||||||
|
this.initDragAndDrop();
|
||||||
}
|
}
|
||||||
|
|
||||||
initSocket() {
|
initSocket() {
|
||||||
@ -249,6 +252,11 @@ class QueueManager {
|
|||||||
document.getElementById('completed-items').textContent = stats.completed_items || 0;
|
document.getElementById('completed-items').textContent = stats.completed_items || 0;
|
||||||
document.getElementById('failed-items').textContent = stats.failed_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('current-speed').textContent = stats.current_speed || '0 MB/s';
|
||||||
document.getElementById('average-speed').textContent = stats.average_speed || '0 MB/s';
|
document.getElementById('average-speed').textContent = stats.average_speed || '0 MB/s';
|
||||||
|
|
||||||
@ -331,12 +339,16 @@ class QueueManager {
|
|||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
<p>No items in queue</p>
|
<p>No items in queue</p>
|
||||||
|
<small>Add episodes from the main page to start downloading</small>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = queue.map((item, index) => this.createPendingQueueCard(item, index)).join('');
|
container.innerHTML = queue.map((item, index) => this.createPendingQueueCard(item, index)).join('');
|
||||||
|
|
||||||
|
// Re-attach drag and drop event listeners
|
||||||
|
this.attachDragListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
createPendingQueueCard(download, index) {
|
createPendingQueueCard(download, index) {
|
||||||
@ -344,7 +356,13 @@ class QueueManager {
|
|||||||
const priorityClass = download.priority === 'high' ? 'high-priority' : '';
|
const priorityClass = download.priority === 'high' ? 'high-priority' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="download-card pending ${priorityClass}" data-id="${download.id}">
|
<div class="download-card pending ${priorityClass} draggable-item"
|
||||||
|
data-id="${download.id}"
|
||||||
|
data-index="${index}"
|
||||||
|
draggable="true">
|
||||||
|
<div class="drag-handle" title="Drag to reorder">
|
||||||
|
<i class="fas fa-grip-vertical"></i>
|
||||||
|
</div>
|
||||||
<div class="queue-position">${index + 1}</div>
|
<div class="queue-position">${index + 1}</div>
|
||||||
<div class="download-header">
|
<div class="download-header">
|
||||||
<div class="download-info">
|
<div class="download-info">
|
||||||
@ -420,7 +438,7 @@ class QueueManager {
|
|||||||
const retryCount = download.retry_count || 0;
|
const retryCount = download.retry_count || 0;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="download-card failed">
|
<div class="download-card failed" data-id="${download.id}">
|
||||||
<div class="download-header">
|
<div class="download-header">
|
||||||
<div class="download-info">
|
<div class="download-info">
|
||||||
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
<h4>${this.escapeHtml(download.serie_name)}</h4>
|
||||||
@ -441,10 +459,15 @@ class QueueManager {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeFailedDownload(downloadId) {
|
||||||
|
await this.removeFromQueue(downloadId);
|
||||||
|
}
|
||||||
|
|
||||||
updateButtonStates(data) {
|
updateButtonStates(data) {
|
||||||
const hasActive = (data.active_downloads || []).length > 0;
|
const hasActive = (data.active_downloads || []).length > 0;
|
||||||
const hasPending = (data.pending_queue || []).length > 0;
|
const hasPending = (data.pending_queue || []).length > 0;
|
||||||
const hasFailed = (data.failed_downloads || []).length > 0;
|
const hasFailed = (data.failed_downloads || []).length > 0;
|
||||||
|
const hasCompleted = (data.completed_downloads || []).length > 0;
|
||||||
|
|
||||||
// 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;
|
||||||
@ -461,8 +484,9 @@ class QueueManager {
|
|||||||
|
|
||||||
document.getElementById('pause-all-btn').disabled = !hasActive;
|
document.getElementById('pause-all-btn').disabled = !hasActive;
|
||||||
document.getElementById('clear-queue-btn').disabled = !hasPending;
|
document.getElementById('clear-queue-btn').disabled = !hasPending;
|
||||||
document.getElementById('reorder-queue-btn').disabled = !hasPending || (data.pending_queue || []).length < 2;
|
|
||||||
document.getElementById('retry-all-btn').disabled = !hasFailed;
|
document.getElementById('retry-all-btn').disabled = !hasFailed;
|
||||||
|
document.getElementById('clear-completed-btn').disabled = !hasCompleted;
|
||||||
|
document.getElementById('clear-failed-btn').disabled = !hasFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearQueue(type) {
|
async clearQueue(type) {
|
||||||
@ -483,7 +507,6 @@ class QueueManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (type === 'completed') {
|
if (type === 'completed') {
|
||||||
// Use the new DELETE /api/queue/completed endpoint
|
|
||||||
const response = await this.makeAuthenticatedRequest('/api/queue/completed', {
|
const response = await this.makeAuthenticatedRequest('/api/queue/completed', {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
@ -491,11 +514,38 @@ class QueueManager {
|
|||||||
if (!response) return;
|
if (!response) return;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
this.showToast(`Cleared ${data.cleared_count} completed downloads`, 'success');
|
this.showToast(`Cleared ${data.count} completed downloads`, 'success');
|
||||||
|
this.loadQueueData();
|
||||||
|
} else if (type === 'failed') {
|
||||||
|
const response = await this.makeAuthenticatedRequest('/api/queue/failed', {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response) return;
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
this.showToast(`Cleared ${data.count} failed downloads`, 'success');
|
||||||
|
this.loadQueueData();
|
||||||
|
} else if (type === 'pending') {
|
||||||
|
// Get all pending items
|
||||||
|
const pendingCards = document.querySelectorAll('#pending-queue .download-card.pending');
|
||||||
|
const itemIds = Array.from(pendingCards).map(card => card.dataset.id).filter(id => id);
|
||||||
|
|
||||||
|
if (itemIds.length === 0) {
|
||||||
|
this.showToast('No pending items to clear', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.makeAuthenticatedRequest('/api/queue/', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ item_ids: itemIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response) return;
|
||||||
|
|
||||||
|
this.showToast(`Cleared ${itemIds.length} pending items`, 'success');
|
||||||
this.loadQueueData();
|
this.loadQueueData();
|
||||||
} else {
|
|
||||||
// For pending and failed, use the old logic (TODO: implement backend endpoints)
|
|
||||||
this.showToast(`Clear ${type} not yet implemented`, 'warning');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -528,14 +578,31 @@ class QueueManager {
|
|||||||
const confirmed = await this.showConfirmModal('Retry All Failed Downloads', 'Are you sure you want to retry all failed downloads?');
|
const confirmed = await this.showConfirmModal('Retry All Failed Downloads', 'Are you sure you want to retry all failed downloads?');
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
// Get all failed downloads and retry them individually
|
try {
|
||||||
|
// Get all failed download IDs
|
||||||
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
|
const failedCards = document.querySelectorAll('#failed-downloads .download-card.failed');
|
||||||
|
const itemIds = Array.from(failedCards).map(card => card.dataset.id).filter(id => id);
|
||||||
|
|
||||||
for (const card of failedCards) {
|
if (itemIds.length === 0) {
|
||||||
const downloadId = card.dataset.id;
|
this.showToast('No failed downloads to retry', 'info');
|
||||||
if (downloadId) {
|
return;
|
||||||
await this.retryDownload(downloadId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await this.makeAuthenticatedRequest('/api/queue/retry', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ item_ids: itemIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response) return;
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
this.showToast(`Retried ${data.retried_count || itemIds.length} download(s)`, 'success');
|
||||||
|
this.loadQueueData();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error retrying failed downloads:', error);
|
||||||
|
this.showToast('Failed to retry downloads', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,8 +699,146 @@ class QueueManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleReorderMode() {
|
toggleReorderMode() {
|
||||||
// TODO: Implement reorder functionality
|
// Drag and drop is always enabled, no need for toggle mode
|
||||||
this.showToast('Reorder functionality not yet implemented', 'info');
|
this.showToast('Drag items to reorder the queue', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
initDragAndDrop() {
|
||||||
|
// Initialize drag and drop on the pending queue container
|
||||||
|
const container = document.getElementById('pending-queue');
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('dragover', this.handleDragOver.bind(this));
|
||||||
|
container.addEventListener('drop', this.handleDrop.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachDragListeners() {
|
||||||
|
// Attach listeners to all draggable items
|
||||||
|
const items = document.querySelectorAll('.draggable-item');
|
||||||
|
items.forEach(item => {
|
||||||
|
item.addEventListener('dragstart', this.handleDragStart.bind(this));
|
||||||
|
item.addEventListener('dragend', this.handleDragEnd.bind(this));
|
||||||
|
item.addEventListener('dragenter', this.handleDragEnter.bind(this));
|
||||||
|
item.addEventListener('dragleave', this.handleDragLeave.bind(this));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragStart(e) {
|
||||||
|
this.draggedElement = e.currentTarget;
|
||||||
|
this.draggedId = e.currentTarget.dataset.id;
|
||||||
|
e.currentTarget.classList.add('dragging');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/html', e.currentTarget.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnd(e) {
|
||||||
|
e.currentTarget.classList.remove('dragging');
|
||||||
|
|
||||||
|
// Remove all drag-over classes
|
||||||
|
document.querySelectorAll('.drag-over').forEach(item => {
|
||||||
|
item.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(e) {
|
||||||
|
if (e.preventDefault) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnter(e) {
|
||||||
|
if (e.currentTarget.classList.contains('draggable-item') &&
|
||||||
|
e.currentTarget !== this.draggedElement) {
|
||||||
|
e.currentTarget.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragLeave(e) {
|
||||||
|
e.currentTarget.classList.remove('drag-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDrop(e) {
|
||||||
|
if (e.stopPropagation) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Get the target element (the item we dropped onto)
|
||||||
|
let target = e.target;
|
||||||
|
while (target && !target.classList.contains('draggable-item')) {
|
||||||
|
target = target.parentElement;
|
||||||
|
if (target === document.getElementById('pending-queue')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target || target === this.draggedElement) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all items to determine new order
|
||||||
|
const container = document.getElementById('pending-queue');
|
||||||
|
const items = Array.from(container.querySelectorAll('.draggable-item'));
|
||||||
|
|
||||||
|
const draggedIndex = items.indexOf(this.draggedElement);
|
||||||
|
const targetIndex = items.indexOf(target);
|
||||||
|
|
||||||
|
if (draggedIndex === targetIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder visually
|
||||||
|
if (draggedIndex < targetIndex) {
|
||||||
|
target.parentNode.insertBefore(this.draggedElement, target.nextSibling);
|
||||||
|
} else {
|
||||||
|
target.parentNode.insertBefore(this.draggedElement, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position numbers
|
||||||
|
const updatedItems = Array.from(container.querySelectorAll('.draggable-item'));
|
||||||
|
updatedItems.forEach((item, index) => {
|
||||||
|
const posElement = item.querySelector('.queue-position');
|
||||||
|
if (posElement) {
|
||||||
|
posElement.textContent = index + 1;
|
||||||
|
}
|
||||||
|
item.dataset.index = index;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the new order of IDs
|
||||||
|
const newOrder = updatedItems.map(item => item.dataset.id);
|
||||||
|
|
||||||
|
// Send reorder request to backend
|
||||||
|
await this.reorderQueue(newOrder);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reorderQueue(newOrder) {
|
||||||
|
try {
|
||||||
|
const response = await this.makeAuthenticatedRequest('/api/queue/reorder', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ item_ids: newOrder })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response) return;
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.showToast('Queue reordered successfully', 'success');
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
this.showToast(`Failed to reorder: ${data.detail || 'Unknown error'}`, 'error');
|
||||||
|
// Reload to restore correct order
|
||||||
|
this.loadQueueData();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reordering queue:', error);
|
||||||
|
this.showToast('Failed to reorder queue', 'error');
|
||||||
|
// Reload to restore correct order
|
||||||
|
this.loadQueueData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeAuthenticatedRequest(url, options = {}) {
|
async makeAuthenticatedRequest(url, options = {}) {
|
||||||
|
|||||||
@ -131,7 +131,7 @@
|
|||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>
|
<h2>
|
||||||
<i class="fas fa-clock"></i>
|
<i class="fas fa-clock"></i>
|
||||||
Download Queue
|
Download Queue (<span id="queue-count">0</span>)
|
||||||
</h2>
|
</h2>
|
||||||
<div class="section-actions">
|
<div class="section-actions">
|
||||||
<button id="start-queue-btn" class="btn btn-primary" disabled>
|
<button id="start-queue-btn" class="btn btn-primary" disabled>
|
||||||
@ -146,17 +146,14 @@
|
|||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
Clear Queue
|
Clear Queue
|
||||||
</button>
|
</button>
|
||||||
<button id="reorder-queue-btn" class="btn btn-secondary" disabled>
|
|
||||||
<i class="fas fa-sort"></i>
|
|
||||||
Reorder
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pending-queue-list" id="pending-queue">
|
<div class="pending-queue-list sortable-list" id="pending-queue" data-sortable="true">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
<p>No items in queue</p>
|
<p>No items in queue</p>
|
||||||
|
<small>Add episodes from the main page to start downloading</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -166,10 +163,10 @@
|
|||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>
|
<h2>
|
||||||
<i class="fas fa-check-circle"></i>
|
<i class="fas fa-check-circle"></i>
|
||||||
Recent Completed
|
Completed (<span id="completed-count">0</span>)
|
||||||
</h2>
|
</h2>
|
||||||
<div class="section-actions">
|
<div class="section-actions">
|
||||||
<button id="clear-completed-btn" class="btn btn-secondary">
|
<button id="clear-completed-btn" class="btn btn-secondary" disabled>
|
||||||
<i class="fas fa-broom"></i>
|
<i class="fas fa-broom"></i>
|
||||||
Clear Completed
|
Clear Completed
|
||||||
</button>
|
</button>
|
||||||
@ -178,8 +175,9 @@
|
|||||||
|
|
||||||
<div class="completed-downloads-list" id="completed-downloads">
|
<div class="completed-downloads-list" id="completed-downloads">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-check-circle"></i>
|
<i class="fas fa-check-circle text-success"></i>
|
||||||
<p>No completed downloads</p>
|
<p>No completed downloads</p>
|
||||||
|
<small>Completed episodes will appear here</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -189,14 +187,14 @@
|
|||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>
|
<h2>
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
Failed Downloads
|
Failed (<span id="failed-count">0</span>)
|
||||||
</h2>
|
</h2>
|
||||||
<div class="section-actions">
|
<div class="section-actions">
|
||||||
<button id="retry-all-btn" class="btn btn-warning" disabled>
|
<button id="retry-all-btn" class="btn btn-warning" disabled>
|
||||||
<i class="fas fa-redo"></i>
|
<i class="fas fa-redo"></i>
|
||||||
Retry All
|
Retry All
|
||||||
</button>
|
</button>
|
||||||
<button id="clear-failed-btn" class="btn btn-secondary">
|
<button id="clear-failed-btn" class="btn btn-secondary" disabled>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
Clear Failed
|
Clear Failed
|
||||||
</button>
|
</button>
|
||||||
@ -207,6 +205,7 @@
|
|||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fas fa-check-circle text-success"></i>
|
<i class="fas fa-check-circle text-success"></i>
|
||||||
<p>No failed downloads</p>
|
<p>No failed downloads</p>
|
||||||
|
<small>Failed episodes can be retried or removed</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
466
tests/api/test_queue_features.py
Normal file
466
tests/api/test_queue_features.py
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
"""Tests for queue management features.
|
||||||
|
|
||||||
|
This module tests the queue page functionality including:
|
||||||
|
- Display of queued items in organized lists
|
||||||
|
- Drag-and-drop reordering
|
||||||
|
- Starting and stopping queue processing
|
||||||
|
- Filtering completed and failed downloads
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
|
from src.server.fastapi_app import app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client():
|
||||||
|
"""Create an async test client."""
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def auth_headers(client: AsyncClient):
|
||||||
|
"""Get authentication headers with valid JWT token."""
|
||||||
|
# Setup auth
|
||||||
|
await client.post(
|
||||||
|
"/api/auth/setup",
|
||||||
|
json={"master_password": "TestPass123!"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Login
|
||||||
|
response = await client.post(
|
||||||
|
"/api/auth/login",
|
||||||
|
json={"password": "TestPass123!"}
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
token = data["access_token"]
|
||||||
|
|
||||||
|
return {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_download_request():
|
||||||
|
"""Sample download request for testing."""
|
||||||
|
return {
|
||||||
|
"serie_id": "test-series",
|
||||||
|
"serie_name": "Test Series",
|
||||||
|
"episodes": [
|
||||||
|
{"season": 1, "episode": 1},
|
||||||
|
{"season": 1, "episode": 2}
|
||||||
|
],
|
||||||
|
"priority": "normal"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueueDisplay:
|
||||||
|
"""Test queue display and organization."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_queue_status_includes_all_sections(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test queue status includes all sections."""
|
||||||
|
response = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Verify structure
|
||||||
|
assert "status" in data
|
||||||
|
assert "statistics" in data
|
||||||
|
|
||||||
|
status = data["status"]
|
||||||
|
assert "active" in status
|
||||||
|
assert "pending" in status
|
||||||
|
assert "completed" in status
|
||||||
|
assert "failed" in status
|
||||||
|
assert "is_running" in status
|
||||||
|
assert "is_paused" in status
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_queue_items_have_required_fields(
|
||||||
|
self, client: AsyncClient, auth_headers: dict,
|
||||||
|
sample_download_request: dict
|
||||||
|
):
|
||||||
|
"""Test queue items have required display fields."""
|
||||||
|
# Add an item to the queue
|
||||||
|
add_response = await client.post(
|
||||||
|
"/api/queue/add",
|
||||||
|
json=sample_download_request,
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert add_response.status_code == 201
|
||||||
|
|
||||||
|
# Get queue status
|
||||||
|
response = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
pending = data["status"]["pending"]
|
||||||
|
|
||||||
|
assert len(pending) > 0
|
||||||
|
item = pending[0]
|
||||||
|
|
||||||
|
# Verify required fields for display
|
||||||
|
assert "id" in item
|
||||||
|
assert "serie_name" in item
|
||||||
|
assert "episode" in item
|
||||||
|
assert "priority" in item
|
||||||
|
assert "added_at" in item
|
||||||
|
|
||||||
|
# Verify episode structure
|
||||||
|
episode = item["episode"]
|
||||||
|
assert "season" in episode
|
||||||
|
assert "episode" in episode
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueueReordering:
|
||||||
|
"""Test queue reordering functionality."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reorder_queue_with_item_ids(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test reordering queue using item_ids array."""
|
||||||
|
# Clear existing queue first
|
||||||
|
status_response = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
existing_items = [
|
||||||
|
item["id"]
|
||||||
|
for item in status_response.json()["status"]["pending"]
|
||||||
|
]
|
||||||
|
if existing_items:
|
||||||
|
await client.request(
|
||||||
|
"DELETE",
|
||||||
|
"/api/queue/",
|
||||||
|
json={"item_ids": existing_items},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add exactly 3 items
|
||||||
|
added_ids = []
|
||||||
|
for i in range(3):
|
||||||
|
response = await client.post(
|
||||||
|
"/api/queue/add",
|
||||||
|
json={
|
||||||
|
"serie_id": f"test-{i}",
|
||||||
|
"serie_name": f"Test Series {i}",
|
||||||
|
"episodes": [{"season": 1, "episode": i+1}],
|
||||||
|
"priority": "normal"
|
||||||
|
},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
if response.status_code == 201:
|
||||||
|
data = response.json()
|
||||||
|
if "added_items" in data and data["added_items"]:
|
||||||
|
added_ids.extend(data["added_items"])
|
||||||
|
|
||||||
|
assert len(added_ids) == 3, f"Expected 3 items, got {len(added_ids)}"
|
||||||
|
|
||||||
|
# Reverse the order
|
||||||
|
new_order = list(reversed(added_ids))
|
||||||
|
|
||||||
|
# Reorder
|
||||||
|
reorder_response = await client.post(
|
||||||
|
"/api/queue/reorder",
|
||||||
|
json={"item_ids": new_order},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert reorder_response.status_code == 200
|
||||||
|
assert reorder_response.json()["status"] == "success"
|
||||||
|
|
||||||
|
# Verify new order
|
||||||
|
status_response = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
current_order = [
|
||||||
|
item["id"]
|
||||||
|
for item in status_response.json()["status"]["pending"]
|
||||||
|
]
|
||||||
|
|
||||||
|
assert current_order == new_order
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reorder_with_invalid_ids(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test reordering with non-existent IDs succeeds (idempotent)."""
|
||||||
|
response = await client.post(
|
||||||
|
"/api/queue/reorder",
|
||||||
|
json={"item_ids": ["invalid-id-1", "invalid-id-2"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bulk reorder is idempotent and succeeds even with invalid IDs
|
||||||
|
# It just ignores items that don't exist
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reorder_empty_list(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test reordering with empty list."""
|
||||||
|
response = await client.post(
|
||||||
|
"/api/queue/reorder",
|
||||||
|
json={"item_ids": []},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should succeed but do nothing
|
||||||
|
assert response.status_code in [200, 404]
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueueControl:
|
||||||
|
"""Test queue start/stop functionality."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_queue(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test starting the download queue."""
|
||||||
|
response = await client.post(
|
||||||
|
"/api/queue/start",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "success"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stop_queue(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test stopping the download queue."""
|
||||||
|
# Start first
|
||||||
|
await client.post("/api/queue/start", headers=auth_headers)
|
||||||
|
|
||||||
|
# Then stop
|
||||||
|
response = await client.post(
|
||||||
|
"/api/queue/stop",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "success"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_queue_status_reflects_running_state(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test queue status reflects running state."""
|
||||||
|
# Initially not running
|
||||||
|
status = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert status.json()["status"]["is_running"] is False
|
||||||
|
|
||||||
|
# Start queue
|
||||||
|
await client.post("/api/queue/start", headers=auth_headers)
|
||||||
|
|
||||||
|
# Should be running
|
||||||
|
status = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert status.json()["status"]["is_running"] is True
|
||||||
|
|
||||||
|
# Stop queue
|
||||||
|
await client.post("/api/queue/stop", headers=auth_headers)
|
||||||
|
|
||||||
|
# Should not be running
|
||||||
|
status = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert status.json()["status"]["is_running"] is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompletedDownloads:
|
||||||
|
"""Test completed downloads management."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_clear_completed_downloads(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test clearing completed downloads."""
|
||||||
|
response = await client.delete(
|
||||||
|
"/api/queue/completed",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "count" in data
|
||||||
|
assert data["status"] == "success"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_completed_section_count(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test that completed count is accurate."""
|
||||||
|
status = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
data = status.json()
|
||||||
|
|
||||||
|
completed_count = data["statistics"]["completed_count"]
|
||||||
|
completed_list = len(data["status"]["completed"])
|
||||||
|
|
||||||
|
# Count should match list length
|
||||||
|
assert completed_count == completed_list
|
||||||
|
|
||||||
|
|
||||||
|
class TestFailedDownloads:
|
||||||
|
"""Test failed downloads management."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_clear_failed_downloads(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test clearing failed downloads."""
|
||||||
|
response = await client.delete(
|
||||||
|
"/api/queue/failed",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "count" in data
|
||||||
|
assert data["status"] == "success"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retry_failed_downloads(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test retrying failed downloads."""
|
||||||
|
response = await client.post(
|
||||||
|
"/api/queue/retry",
|
||||||
|
json={"item_ids": []},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "retried_count" in data
|
||||||
|
assert data["status"] == "success"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retry_specific_failed_download(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test retrying a specific failed download."""
|
||||||
|
# Test the endpoint accepts the format
|
||||||
|
response = await client.post(
|
||||||
|
"/api/queue/retry",
|
||||||
|
json={"item_ids": ["some-id"]},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should succeed even if ID doesn't exist (idempotent)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_failed_section_count(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test that failed count is accurate."""
|
||||||
|
status = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
data = status.json()
|
||||||
|
|
||||||
|
failed_count = data["statistics"]["failed_count"]
|
||||||
|
failed_list = len(data["status"]["failed"])
|
||||||
|
|
||||||
|
# Count should match list length
|
||||||
|
assert failed_count == failed_list
|
||||||
|
|
||||||
|
|
||||||
|
class TestBulkOperations:
|
||||||
|
"""Test bulk queue operations."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_multiple_items(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test removing multiple items from queue."""
|
||||||
|
# Add multiple items
|
||||||
|
item_ids = []
|
||||||
|
for i in range(3):
|
||||||
|
add_response = await client.post(
|
||||||
|
"/api/queue/add",
|
||||||
|
json={
|
||||||
|
"serie_id": f"bulk-test-{i}",
|
||||||
|
"serie_name": f"Bulk Test {i}",
|
||||||
|
"episodes": [{"season": 1, "episode": i+1}],
|
||||||
|
"priority": "normal"
|
||||||
|
},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
if add_response.status_code == 201:
|
||||||
|
data = add_response.json()
|
||||||
|
if "added_items" in data and len(data["added_items"]) > 0:
|
||||||
|
item_ids.append(data["added_items"][0])
|
||||||
|
|
||||||
|
# Remove all at once
|
||||||
|
if item_ids:
|
||||||
|
response = await client.request(
|
||||||
|
"DELETE",
|
||||||
|
"/api/queue/",
|
||||||
|
json={"item_ids": item_ids},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_clear_entire_pending_queue(
|
||||||
|
self, client: AsyncClient, auth_headers: dict
|
||||||
|
):
|
||||||
|
"""Test clearing entire pending queue."""
|
||||||
|
# Get all pending items
|
||||||
|
status = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
pending = status.json()["status"]["pending"]
|
||||||
|
|
||||||
|
if pending:
|
||||||
|
item_ids = [item["id"] for item in pending]
|
||||||
|
|
||||||
|
# Remove all
|
||||||
|
response = await client.request(
|
||||||
|
"DELETE",
|
||||||
|
"/api/queue/",
|
||||||
|
json={"item_ids": item_ids},
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
# Verify queue is empty
|
||||||
|
status = await client.get(
|
||||||
|
"/api/queue/status",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert len(status.json()["status"]["pending"]) == 0
|
||||||
Loading…
x
Reference in New Issue
Block a user