fix test and add doc
This commit is contained in:
parent
1637835fe6
commit
9692dfc63b
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pending": [
|
"pending": [
|
||||||
{
|
{
|
||||||
"id": "7ce31824-1042-4a7e-b358-021660fe3f57",
|
"id": "ec2570fb-9903-4942-87c9-0dc63078bb41",
|
||||||
"serie_id": "workflow-series",
|
"serie_id": "workflow-series",
|
||||||
"serie_name": "Workflow Test Series",
|
"serie_name": "Workflow Test Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"added_at": "2025-10-22T06:33:14.519721Z",
|
"added_at": "2025-10-22T09:08:49.319607Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -20,7 +20,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2037b69a-48c2-4878-aa01-4a715d09d824",
|
"id": "64d4a680-a4ec-49f8-8a73-ca27fa3e31b7",
|
||||||
"serie_id": "series-2",
|
"serie_id": "series-2",
|
||||||
"serie_name": "Series 2",
|
"serie_name": "Series 2",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.205751Z",
|
"added_at": "2025-10-22T09:08:49.051921Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "56d39fa2-5590-49ee-a5f9-11b811b8644a",
|
"id": "98e47c9e-17e5-4205-aacd-4a2d31ca6b29",
|
||||||
"serie_id": "series-1",
|
"serie_id": "series-1",
|
||||||
"serie_name": "Series 1",
|
"serie_name": "Series 1",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.202473Z",
|
"added_at": "2025-10-22T09:08:49.049588Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -58,7 +58,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a154fa76-d368-4b49-a440-677c22d497f7",
|
"id": "aa4bf164-0f66-488d-b5aa-04b152c5ec6b",
|
||||||
"serie_id": "series-0",
|
"serie_id": "series-0",
|
||||||
"serie_name": "Series 0",
|
"serie_name": "Series 0",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.197599Z",
|
"added_at": "2025-10-22T09:08:49.045265Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -77,7 +77,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e30b1101-eca3-4e72-891d-8b5f154448b3",
|
"id": "96b78a9c-bcba-461a-a3f7-c9413c8097bb",
|
||||||
"serie_id": "series-high",
|
"serie_id": "series-high",
|
||||||
"serie_name": "Series High",
|
"serie_name": "Series High",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -87,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"added_at": "2025-10-22T06:33:13.828413Z",
|
"added_at": "2025-10-22T09:08:48.825866Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -96,7 +96,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "36053249-03ad-42d6-83c5-67514f4c5ccd",
|
"id": "af79a00c-1677-41a4-8cf1-5edd715c660f",
|
||||||
"serie_id": "test-series-2",
|
"serie_id": "test-series-2",
|
||||||
"serie_name": "Another Series",
|
"serie_name": "Another Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -106,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"added_at": "2025-10-22T06:33:13.800966Z",
|
"added_at": "2025-10-22T09:08:48.802199Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6cf9ec9d-351c-4804-bbb1-fee061f3f9fd",
|
"id": "4f2a07da-0248-4a69-9c8a-e17913fa5fa2",
|
||||||
"serie_id": "test-series-1",
|
"serie_id": "test-series-1",
|
||||||
"serie_name": "Test Anime Series",
|
"serie_name": "Test Anime Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -125,7 +125,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:13.774745Z",
|
"added_at": "2025-10-22T09:08:48.776865Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -134,7 +134,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ac2f472c-4e3f-463b-b679-cf574af9174e",
|
"id": "7dd638cb-da1a-407f-8716-5bb9d4388a49",
|
||||||
"serie_id": "test-series-1",
|
"serie_id": "test-series-1",
|
||||||
"serie_name": "Test Anime Series",
|
"serie_name": "Test Anime Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -144,7 +144,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:13.774848Z",
|
"added_at": "2025-10-22T09:08:48.776962Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -153,7 +153,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b3d11784-aea5-41c0-8078-adc8c8294b04",
|
"id": "226764e6-1ac5-43cf-be43-a47a2e4f46e8",
|
||||||
"serie_id": "series-normal",
|
"serie_id": "series-normal",
|
||||||
"serie_name": "Series Normal",
|
"serie_name": "Series Normal",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -163,7 +163,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:13.831840Z",
|
"added_at": "2025-10-22T09:08:48.827876Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -172,7 +172,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dc1332cc-9230-46e6-bcdc-f0bb0f1ff58b",
|
"id": "04298256-9f47-41d8-b5ed-b2df0c978ad6",
|
||||||
"serie_id": "series-low",
|
"serie_id": "series-low",
|
||||||
"serie_name": "Series Low",
|
"serie_name": "Series Low",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -182,7 +182,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "low",
|
"priority": "low",
|
||||||
"added_at": "2025-10-22T06:33:13.835608Z",
|
"added_at": "2025-10-22T09:08:48.833026Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -191,7 +191,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "f86cf7ea-3f59-4e2a-a8bc-e63062995543",
|
"id": "b5f39f9a-afc1-42ba-94c7-10820413ae8f",
|
||||||
"serie_id": "test-series",
|
"serie_id": "test-series",
|
||||||
"serie_name": "Test Series",
|
"serie_name": "Test Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -201,7 +201,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.145652Z",
|
"added_at": "2025-10-22T09:08:49.000308Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -210,7 +210,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "f0bad497-7bc8-4983-b65e-80f8f61de9e4",
|
"id": "f8c9f7c1-4d24-4d13-bec2-25001b6b04fb",
|
||||||
"serie_id": "test-series",
|
"serie_id": "test-series",
|
||||||
"serie_name": "Test Series",
|
"serie_name": "Test Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -220,7 +220,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.235532Z",
|
"added_at": "2025-10-22T09:08:49.076920Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -229,7 +229,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "f4656791-4788-4088-aa76-7a9abbeef3d2",
|
"id": "1954ad7d-d977-4b5b-a603-2c9f4d3bc747",
|
||||||
"serie_id": "invalid-series",
|
"serie_id": "invalid-series",
|
||||||
"serie_name": "Invalid Series",
|
"serie_name": "Invalid Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -239,7 +239,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.295095Z",
|
"added_at": "2025-10-22T09:08:49.125379Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -248,7 +248,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5790af03-d28a-4f78-914c-f82d4c73bde5",
|
"id": "48d00dab-8caf-4eef-97c4-1ceead6906e7",
|
||||||
"serie_id": "test-series",
|
"serie_id": "test-series",
|
||||||
"serie_name": "Test Series",
|
"serie_name": "Test Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -258,7 +258,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.324681Z",
|
"added_at": "2025-10-22T09:08:49.150809Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -267,26 +267,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "4a293a5d-1bd8-4eb2-b006-286f7e0bed95",
|
"id": "4cdd33c4-e2bd-4425-8e4d-661b1c3d43b3",
|
||||||
"serie_id": "series-4",
|
|
||||||
"serie_name": "Series 4",
|
|
||||||
"episode": {
|
|
||||||
"season": 1,
|
|
||||||
"episode": 1,
|
|
||||||
"title": null
|
|
||||||
},
|
|
||||||
"status": "pending",
|
|
||||||
"priority": "normal",
|
|
||||||
"added_at": "2025-10-22T06:33:14.367510Z",
|
|
||||||
"started_at": null,
|
|
||||||
"completed_at": null,
|
|
||||||
"progress": null,
|
|
||||||
"error": null,
|
|
||||||
"retry_count": 0,
|
|
||||||
"source_url": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cc7ab85a-b63e-41ba-9e1b-d1c5c0b976f6",
|
|
||||||
"serie_id": "series-0",
|
"serie_id": "series-0",
|
||||||
"serie_name": "Series 0",
|
"serie_name": "Series 0",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -296,7 +277,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.368708Z",
|
"added_at": "2025-10-22T09:08:49.184788Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -305,7 +286,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2cd29cec-7805-465d-b3a0-141cf8583710",
|
"id": "93f7fba9-65c7-4b95-8610-416fe6b0f3df",
|
||||||
"serie_id": "series-1",
|
"serie_id": "series-1",
|
||||||
"serie_name": "Series 1",
|
"serie_name": "Series 1",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -315,7 +296,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.369487Z",
|
"added_at": "2025-10-22T09:08:49.185634Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -324,7 +305,26 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dc4f4a75-7823-4933-a5f2-491698f741e5",
|
"id": "a7204eaa-d3a6-4389-9634-1582aabeb963",
|
||||||
|
"serie_id": "series-4",
|
||||||
|
"serie_name": "Series 4",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-22T09:08:49.186289Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1a4a3ed9-2694-4edf-8448-2239cc240d46",
|
||||||
"serie_id": "series-2",
|
"serie_id": "series-2",
|
||||||
"serie_name": "Series 2",
|
"serie_name": "Series 2",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -334,7 +334,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.370252Z",
|
"added_at": "2025-10-22T09:08:49.186944Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -343,7 +343,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "956bb8fd-b745-436c-bdd1-ea1f522f8faa",
|
"id": "b3e007b3-da38-46ac-8a96-9cbbaf61777a",
|
||||||
"serie_id": "series-3",
|
"serie_id": "series-3",
|
||||||
"serie_name": "Series 3",
|
"serie_name": "Series 3",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -353,7 +353,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.371006Z",
|
"added_at": "2025-10-22T09:08:49.188800Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -362,7 +362,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "27adc5f4-32f6-4aa5-9119-7e11f89682d8",
|
"id": "7d0e5f7e-92f6-4d39-9635-9f4d490ddb3b",
|
||||||
"serie_id": "persistent-series",
|
"serie_id": "persistent-series",
|
||||||
"serie_name": "Persistent Series",
|
"serie_name": "Persistent Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -372,7 +372,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.437853Z",
|
"added_at": "2025-10-22T09:08:49.246329Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -381,7 +381,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "3234ac64-d825-444b-8b35-d5d6cad0ad51",
|
"id": "3466d362-602f-4410-b16a-ac70012035f1",
|
||||||
"serie_id": "ws-series",
|
"serie_id": "ws-series",
|
||||||
"serie_name": "WebSocket Series",
|
"serie_name": "WebSocket Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -391,7 +391,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.488776Z",
|
"added_at": "2025-10-22T09:08:49.293513Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -400,7 +400,7 @@
|
|||||||
"source_url": null
|
"source_url": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "281f5856-ebc5-4dfd-b983-2d11ba865b5b",
|
"id": "0433681e-6e3a-49fa-880d-24fbef35ff04",
|
||||||
"serie_id": "pause-test",
|
"serie_id": "pause-test",
|
||||||
"serie_name": "Pause Test Series",
|
"serie_name": "Pause Test Series",
|
||||||
"episode": {
|
"episode": {
|
||||||
@ -410,7 +410,7 @@
|
|||||||
},
|
},
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
"added_at": "2025-10-22T06:33:14.656270Z",
|
"added_at": "2025-10-22T09:08:49.452875Z",
|
||||||
"started_at": null,
|
"started_at": null,
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"progress": null,
|
"progress": null,
|
||||||
@ -421,5 +421,5 @@
|
|||||||
],
|
],
|
||||||
"active": [],
|
"active": [],
|
||||||
"failed": [],
|
"failed": [],
|
||||||
"timestamp": "2025-10-22T06:33:14.656532+00:00"
|
"timestamp": "2025-10-22T09:08:49.453140+00:00"
|
||||||
}
|
}
|
||||||
308
docs/README.md
Normal file
308
docs/README.md
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
# Aniworld Documentation
|
||||||
|
|
||||||
|
Complete documentation for the Aniworld Download Manager application.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
- **New Users**: Start with [User Guide](./user_guide.md)
|
||||||
|
- **Developers**: Check [API Reference](./api_reference.md)
|
||||||
|
- **System Admins**: See [Deployment Guide](./deployment.md)
|
||||||
|
- **Interactive Docs**: Visit `http://localhost:8000/api/docs`
|
||||||
|
|
||||||
|
## Documentation Structure
|
||||||
|
|
||||||
|
### 📖 User Guide (`user_guide.md`)
|
||||||
|
|
||||||
|
Complete guide for end users covering:
|
||||||
|
|
||||||
|
- Installation instructions
|
||||||
|
- Initial setup and configuration
|
||||||
|
- User interface walkthrough
|
||||||
|
- Managing anime library
|
||||||
|
- Download queue management
|
||||||
|
- Configuration and settings
|
||||||
|
- Troubleshooting common issues
|
||||||
|
- Keyboard shortcuts
|
||||||
|
- Frequently asked questions (FAQ)
|
||||||
|
|
||||||
|
**Best for**: Anyone using the Aniworld application
|
||||||
|
|
||||||
|
### 🔌 API Reference (`api_reference.md`)
|
||||||
|
|
||||||
|
Detailed API documentation including:
|
||||||
|
|
||||||
|
- Authentication and authorization
|
||||||
|
- Error handling and status codes
|
||||||
|
- All REST endpoints with examples
|
||||||
|
- WebSocket real-time updates
|
||||||
|
- Request/response formats
|
||||||
|
- Rate limiting and pagination
|
||||||
|
- Complete workflow examples
|
||||||
|
- API changelog
|
||||||
|
|
||||||
|
**Best for**: Developers integrating with the API
|
||||||
|
|
||||||
|
### 🚀 Deployment Guide (`deployment.md`)
|
||||||
|
|
||||||
|
Production deployment instructions covering:
|
||||||
|
|
||||||
|
- System requirements
|
||||||
|
- Pre-deployment checklist
|
||||||
|
- Local development setup
|
||||||
|
- Production deployment steps
|
||||||
|
- Docker and Docker Compose setup
|
||||||
|
- Nginx reverse proxy configuration
|
||||||
|
- SSL/TLS certificate setup
|
||||||
|
- Database configuration (SQLite and PostgreSQL)
|
||||||
|
- Security best practices
|
||||||
|
- Monitoring and maintenance
|
||||||
|
- Troubleshooting deployment issues
|
||||||
|
|
||||||
|
**Best for**: System administrators and DevOps engineers
|
||||||
|
|
||||||
|
## Key Features Documented
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- Master password setup and login
|
||||||
|
- JWT token management
|
||||||
|
- Session handling
|
||||||
|
- Security best practices
|
||||||
|
|
||||||
|
### Configuration Management
|
||||||
|
|
||||||
|
- Application settings
|
||||||
|
- Directory configuration
|
||||||
|
- Backup and restore functionality
|
||||||
|
- Environment variables
|
||||||
|
|
||||||
|
### Anime Management
|
||||||
|
|
||||||
|
- Browsing anime library
|
||||||
|
- Adding new anime
|
||||||
|
- Managing episodes
|
||||||
|
- Search functionality
|
||||||
|
|
||||||
|
### Download Management
|
||||||
|
|
||||||
|
- Queue operations
|
||||||
|
- Priority management
|
||||||
|
- Progress tracking
|
||||||
|
- Error recovery
|
||||||
|
|
||||||
|
### Real-time Features
|
||||||
|
|
||||||
|
- WebSocket connections
|
||||||
|
- Live download updates
|
||||||
|
- Status notifications
|
||||||
|
- Error alerts
|
||||||
|
|
||||||
|
## Documentation Examples
|
||||||
|
|
||||||
|
### API Usage Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup
|
||||||
|
curl -X POST http://localhost:8000/api/auth/setup \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"master_password": "secure_pass"}'
|
||||||
|
|
||||||
|
# Login
|
||||||
|
TOKEN=$(curl -X POST http://localhost:8000/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "secure_pass"}' | jq -r '.token')
|
||||||
|
|
||||||
|
# List anime
|
||||||
|
curl http://localhost:8000/api/v1/anime \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and setup
|
||||||
|
git clone https://github.com/your-repo/aniworld.git
|
||||||
|
cd aniworld
|
||||||
|
python3.10 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run application
|
||||||
|
python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interactive Documentation
|
||||||
|
|
||||||
|
Access interactive API documentation at:
|
||||||
|
|
||||||
|
- **Swagger UI**: `http://localhost:8000/api/docs`
|
||||||
|
- **ReDoc**: `http://localhost:8000/api/redoc`
|
||||||
|
- **OpenAPI JSON**: `http://localhost:8000/openapi.json`
|
||||||
|
|
||||||
|
These provide:
|
||||||
|
|
||||||
|
- Interactive API explorer
|
||||||
|
- Try-it-out functionality
|
||||||
|
- Request/response examples
|
||||||
|
- Schema validation
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### I want to...
|
||||||
|
|
||||||
|
**Use the application**
|
||||||
|
→ Read [User Guide](./user_guide.md) → Getting Started section
|
||||||
|
|
||||||
|
**Set up on my computer**
|
||||||
|
→ Read [User Guide](./user_guide.md) → Installation section
|
||||||
|
|
||||||
|
**Deploy to production**
|
||||||
|
→ Read [Deployment Guide](./deployment.md) → Production Deployment
|
||||||
|
|
||||||
|
**Use the API**
|
||||||
|
→ Read [API Reference](./api_reference.md) → API Endpoints section
|
||||||
|
|
||||||
|
**Troubleshoot problems**
|
||||||
|
→ Read [User Guide](./user_guide.md) → Troubleshooting section
|
||||||
|
|
||||||
|
**Set up with Docker**
|
||||||
|
→ Read [Deployment Guide](./deployment.md) → Docker Deployment
|
||||||
|
|
||||||
|
**Configure backup/restore**
|
||||||
|
→ Read [User Guide](./user_guide.md) → Configuration section
|
||||||
|
|
||||||
|
**Debug API issues**
|
||||||
|
→ Check [API Reference](./api_reference.md) → Error Handling section
|
||||||
|
|
||||||
|
## Documentation Standards
|
||||||
|
|
||||||
|
All documentation follows these standards:
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
- Clear table of contents
|
||||||
|
- Logical section ordering
|
||||||
|
- Cross-references to related topics
|
||||||
|
- Code examples where appropriate
|
||||||
|
|
||||||
|
### Style
|
||||||
|
|
||||||
|
- Plain, accessible language
|
||||||
|
- Step-by-step instructions
|
||||||
|
- Visual formatting (code blocks, tables, lists)
|
||||||
|
- Examples for common scenarios
|
||||||
|
|
||||||
|
### Completeness
|
||||||
|
|
||||||
|
- All major features covered
|
||||||
|
- Edge cases documented
|
||||||
|
- Troubleshooting guidance
|
||||||
|
- FAQ section included
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Version number tracking
|
||||||
|
- Last updated timestamp
|
||||||
|
- Changelog for updates
|
||||||
|
- Broken link checking
|
||||||
|
|
||||||
|
## Help & Support
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
1. **Check Documentation First**
|
||||||
|
|
||||||
|
- Search in relevant guide
|
||||||
|
- Check FAQ section
|
||||||
|
- Look for similar examples
|
||||||
|
|
||||||
|
2. **Check Logs**
|
||||||
|
|
||||||
|
- Application logs in `/logs/`
|
||||||
|
- Browser console (F12)
|
||||||
|
- System logs
|
||||||
|
|
||||||
|
3. **Try Troubleshooting**
|
||||||
|
|
||||||
|
- Follow troubleshooting steps in user guide
|
||||||
|
- Check known issues section
|
||||||
|
- Verify system requirements
|
||||||
|
|
||||||
|
4. **Get Community Help**
|
||||||
|
|
||||||
|
- GitHub Issues
|
||||||
|
- Discussion Forums
|
||||||
|
- Community Discord
|
||||||
|
|
||||||
|
5. **Report Issues**
|
||||||
|
- File GitHub issue
|
||||||
|
- Include logs and error messages
|
||||||
|
- Describe reproduction steps
|
||||||
|
- Specify system details
|
||||||
|
|
||||||
|
### Feedback
|
||||||
|
|
||||||
|
We welcome feedback on documentation:
|
||||||
|
|
||||||
|
- Unclear sections
|
||||||
|
- Missing information
|
||||||
|
- Incorrect instructions
|
||||||
|
- Outdated content
|
||||||
|
- Suggest improvements
|
||||||
|
|
||||||
|
File documentation issues on GitHub with label `documentation`.
|
||||||
|
|
||||||
|
## Contributing to Documentation
|
||||||
|
|
||||||
|
Documentation improvements are welcome! To contribute:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Edit documentation files
|
||||||
|
3. Test changes locally
|
||||||
|
4. Submit pull request
|
||||||
|
5. Include summary of changes
|
||||||
|
|
||||||
|
See `CONTRIBUTING.md` for guidelines.
|
||||||
|
|
||||||
|
## Documentation Map
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── README.md # This file
|
||||||
|
├── user_guide.md # End-user documentation
|
||||||
|
├── api_reference.md # API documentation
|
||||||
|
├── deployment.md # Deployment instructions
|
||||||
|
└── CONTRIBUTING.md # Contribution guidelines
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Resources
|
||||||
|
|
||||||
|
- **Source Code**: GitHub repository
|
||||||
|
- **Interactive API**: `http://localhost:8000/api/docs`
|
||||||
|
- **Issue Tracker**: GitHub Issues
|
||||||
|
- **Releases**: GitHub Releases
|
||||||
|
- **License**: See LICENSE file
|
||||||
|
|
||||||
|
## Document Info
|
||||||
|
|
||||||
|
- **Last Updated**: October 22, 2025
|
||||||
|
- **Version**: 1.0.0
|
||||||
|
- **Status**: Production Ready
|
||||||
|
- **Maintainers**: Development Team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
| Resource | Link |
|
||||||
|
| ------------------ | -------------------------------------------- |
|
||||||
|
| User Guide | [user_guide.md](./user_guide.md) |
|
||||||
|
| API Reference | [api_reference.md](./api_reference.md) |
|
||||||
|
| Deployment Guide | [deployment.md](./deployment.md) |
|
||||||
|
| Swagger UI | http://localhost:8000/api/docs |
|
||||||
|
| GitHub Issues | https://github.com/your-repo/aniworld/issues |
|
||||||
|
| Project Repository | https://github.com/your-repo/aniworld |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**For Questions**: Check relevant guide first, then file GitHub issue with details.
|
||||||
943
docs/api_reference.md
Normal file
943
docs/api_reference.md
Normal file
@ -0,0 +1,943 @@
|
|||||||
|
# Aniworld API Reference
|
||||||
|
|
||||||
|
Complete API reference documentation for the Aniworld Download Manager Web Application.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [API Overview](#api-overview)
|
||||||
|
2. [Authentication](#authentication)
|
||||||
|
3. [Error Handling](#error-handling)
|
||||||
|
4. [API Endpoints](#api-endpoints)
|
||||||
|
- [Authentication Endpoints](#authentication-endpoints)
|
||||||
|
- [Configuration Endpoints](#configuration-endpoints)
|
||||||
|
- [Anime Endpoints](#anime-endpoints)
|
||||||
|
- [Download Queue Endpoints](#download-queue-endpoints)
|
||||||
|
- [WebSocket Endpoints](#websocket-endpoints)
|
||||||
|
- [Health Check Endpoints](#health-check-endpoints)
|
||||||
|
|
||||||
|
## API Overview
|
||||||
|
|
||||||
|
The Aniworld API is a RESTful API built with FastAPI that provides programmatic access to the anime download manager functionality.
|
||||||
|
|
||||||
|
**Base URL**: `http://localhost:8000/api`
|
||||||
|
|
||||||
|
**API Documentation**: Available at `http://localhost:8000/api/docs` (Swagger UI) and `http://localhost:8000/api/redoc` (ReDoc)
|
||||||
|
|
||||||
|
**API Version**: 1.0.0
|
||||||
|
|
||||||
|
**Response Format**: All responses are JSON-formatted unless otherwise specified.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Master Password Authentication
|
||||||
|
|
||||||
|
The API uses JWT (JSON Web Tokens) for stateless authentication. All protected endpoints require a valid JWT token in the Authorization header.
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
|
||||||
|
1. **Setup** (one-time): POST to `/api/auth/setup` with master password
|
||||||
|
2. **Login**: POST to `/api/auth/login` with master password to receive JWT token
|
||||||
|
3. **Request**: Include token in `Authorization: Bearer <token>` header
|
||||||
|
|
||||||
|
### Token Details
|
||||||
|
|
||||||
|
- **Token Type**: JWT (JSON Web Token)
|
||||||
|
- **Expires In**: Configurable (default: 24 hours)
|
||||||
|
- **Algorithm**: HS256
|
||||||
|
- **Scope**: All resources accessible with single token
|
||||||
|
|
||||||
|
### Example Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login
|
||||||
|
curl -X POST http://localhost:8000/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "your_master_password"}'
|
||||||
|
|
||||||
|
# Response
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"token_type": "bearer"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use token in subsequent requests
|
||||||
|
curl -X GET http://localhost:8000/api/anime \
|
||||||
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
All errors follow a consistent JSON format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "ERROR_CODE",
|
||||||
|
"message": "Human-readable error message",
|
||||||
|
"details": {
|
||||||
|
"additional": "context"
|
||||||
|
},
|
||||||
|
"request_id": "unique-request-identifier"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Status Codes
|
||||||
|
|
||||||
|
| Code | Meaning | Description |
|
||||||
|
| ---- | --------------------- | ---------------------------------------- |
|
||||||
|
| 200 | OK | Successful request |
|
||||||
|
| 201 | Created | Resource created successfully |
|
||||||
|
| 204 | No Content | Successful request with no response body |
|
||||||
|
| 400 | Bad Request | Invalid request parameters |
|
||||||
|
| 401 | Unauthorized | Authentication required or failed |
|
||||||
|
| 403 | Forbidden | Insufficient permissions |
|
||||||
|
| 404 | Not Found | Resource not found |
|
||||||
|
| 409 | Conflict | Resource conflict |
|
||||||
|
| 422 | Unprocessable Entity | Validation error |
|
||||||
|
| 429 | Too Many Requests | Rate limit exceeded |
|
||||||
|
| 500 | Internal Server Error | Unexpected server error |
|
||||||
|
|
||||||
|
### Error Codes
|
||||||
|
|
||||||
|
| Error Code | HTTP Status | Description |
|
||||||
|
| --------------------- | ----------- | ------------------------- |
|
||||||
|
| AUTHENTICATION_ERROR | 401 | Authentication failed |
|
||||||
|
| AUTHORIZATION_ERROR | 403 | Insufficient permissions |
|
||||||
|
| VALIDATION_ERROR | 422 | Request validation failed |
|
||||||
|
| NOT_FOUND | 404 | Resource not found |
|
||||||
|
| CONFLICT | 409 | Resource conflict |
|
||||||
|
| RATE_LIMIT_EXCEEDED | 429 | Rate limit exceeded |
|
||||||
|
| INTERNAL_SERVER_ERROR | 500 | Internal server error |
|
||||||
|
| DOWNLOAD_ERROR | 500 | Download operation failed |
|
||||||
|
| CONFIGURATION_ERROR | 500 | Configuration error |
|
||||||
|
| PROVIDER_ERROR | 500 | Provider error |
|
||||||
|
| DATABASE_ERROR | 500 | Database operation failed |
|
||||||
|
|
||||||
|
### Example Error Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "VALIDATION_ERROR",
|
||||||
|
"message": "Request validation failed",
|
||||||
|
"details": {
|
||||||
|
"field": "anime_id",
|
||||||
|
"issue": "Invalid anime ID format"
|
||||||
|
},
|
||||||
|
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication Endpoints
|
||||||
|
|
||||||
|
#### Setup Master Password
|
||||||
|
|
||||||
|
Configures the master password for the application (one-time only).
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/auth/setup
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"master_password": "your_secure_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201 Created)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
|
||||||
|
- `400 Bad Request`: Master password already configured
|
||||||
|
- `422 Validation Error`: Invalid password format
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
|
||||||
|
Authenticates with master password and returns JWT token.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"password": "your_master_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"token_type": "bearer"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
|
||||||
|
- `401 Unauthorized`: Invalid password
|
||||||
|
- `429 Too Many Requests`: Too many failed attempts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Logout
|
||||||
|
|
||||||
|
Invalidates the current session.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/auth/logout
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Logged out successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Check Authentication Status
|
||||||
|
|
||||||
|
Verifies current authentication status.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/auth/status
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"authenticated": true,
|
||||||
|
"token_valid": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
|
||||||
|
- `401 Unauthorized`: Token invalid or expired
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Configuration Endpoints
|
||||||
|
|
||||||
|
#### Get Configuration
|
||||||
|
|
||||||
|
Retrieves the current application configuration.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/config
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"anime_directory": "/path/to/anime",
|
||||||
|
"download_directory": "/path/to/downloads",
|
||||||
|
"session_timeout_hours": 24,
|
||||||
|
"log_level": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Update Configuration
|
||||||
|
|
||||||
|
Updates application configuration (creates backup automatically).
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/config
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"anime_directory": "/new/anime/path",
|
||||||
|
"download_directory": "/new/download/path"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"anime_directory": "/new/anime/path",
|
||||||
|
"download_directory": "/new/download/path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
|
||||||
|
- `400 Bad Request`: Invalid configuration
|
||||||
|
- `422 Validation Error`: Validation failed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Validate Configuration
|
||||||
|
|
||||||
|
Validates configuration without applying changes.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/config/validate
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"anime_directory": "/path/to/validate"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"valid": true,
|
||||||
|
"message": "Configuration is valid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### List Configuration Backups
|
||||||
|
|
||||||
|
Lists all configuration backups.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/config/backups
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "backup_2025-10-22_12-30-45",
|
||||||
|
"created_at": "2025-10-22T12:30:45Z",
|
||||||
|
"size_bytes": 1024
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Create Configuration Backup
|
||||||
|
|
||||||
|
Creates a manual backup of current configuration.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/config/backups
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (201 Created)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"name": "backup_2025-10-22_12-35-20",
|
||||||
|
"created_at": "2025-10-22T12:35:20Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Restore Configuration from Backup
|
||||||
|
|
||||||
|
Restores configuration from a specific backup.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/config/backups/{backup_name}/restore
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Configuration restored successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
|
||||||
|
- `404 Not Found`: Backup not found
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Delete Configuration Backup
|
||||||
|
|
||||||
|
Deletes a specific configuration backup.
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/config/backups/{backup_name}
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Backup deleted successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Anime Endpoints
|
||||||
|
|
||||||
|
#### List Anime with Missing Episodes
|
||||||
|
|
||||||
|
Lists all anime series with missing episodes.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/anime
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
|
||||||
|
- `page` (integer, optional): Page number for pagination (default: 1)
|
||||||
|
- `per_page` (integer, optional): Items per page (default: 20)
|
||||||
|
- `sort_by` (string, optional): Sort field (name, updated_at)
|
||||||
|
- `sort_order` (string, optional): Sort order (asc, desc)
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "aniworld_123",
|
||||||
|
"title": "Attack on Titan",
|
||||||
|
"missing_episodes": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aniworld_456",
|
||||||
|
"title": "Demon Slayer",
|
||||||
|
"missing_episodes": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Get Anime Details
|
||||||
|
|
||||||
|
Retrieves detailed information for a specific anime series.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/anime/{anime_id}
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "aniworld_123",
|
||||||
|
"title": "Attack on Titan",
|
||||||
|
"episodes": ["Season 1 Episode 1", "Season 1 Episode 2"],
|
||||||
|
"description": "Anime description...",
|
||||||
|
"total_episodes": 100,
|
||||||
|
"downloaded_episodes": 95
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors**:
|
||||||
|
|
||||||
|
- `404 Not Found`: Anime not found
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Trigger Local Rescan
|
||||||
|
|
||||||
|
Rescans the local anime directory for new series and episodes.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/anime/rescan
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Rescan started",
|
||||||
|
"new_series": 2,
|
||||||
|
"new_episodes": 15
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Search Anime on Provider
|
||||||
|
|
||||||
|
Searches for anime on the configured provider.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/anime/search?q={query}
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
|
||||||
|
- `q` (string, required): Search query
|
||||||
|
- `limit` (integer, optional): Maximum results (default: 20)
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"key": "aniworld_789",
|
||||||
|
"name": "Search Result 1",
|
||||||
|
"site": "https://provider.com/anime/1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Download Queue Endpoints
|
||||||
|
|
||||||
|
#### Get Queue Status
|
||||||
|
|
||||||
|
Retrieves download queue status and statistics.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/queue/status
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"total_items": 15,
|
||||||
|
"pending": 5,
|
||||||
|
"downloading": 2,
|
||||||
|
"completed": 8,
|
||||||
|
"failed": 0,
|
||||||
|
"total_size_bytes": 1073741824,
|
||||||
|
"download_speed_mbps": 5.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Add to Download Queue
|
||||||
|
|
||||||
|
Adds episodes to the download queue.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/queue/add
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"anime_id": "aniworld_123",
|
||||||
|
"episodes": ["S01E01", "S01E02"],
|
||||||
|
"priority": "normal"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Priority Values**: `low`, `normal`, `high`
|
||||||
|
|
||||||
|
**Response (201 Created)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"queue_item_id": "queue_456",
|
||||||
|
"anime_id": "aniworld_123",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Remove from Queue
|
||||||
|
|
||||||
|
Removes a specific item from the download queue.
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/queue/{queue_item_id}
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Item removed from queue"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Start Download Queue
|
||||||
|
|
||||||
|
Starts processing the download queue.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/queue/start
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Queue processing started"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Stop Download Queue
|
||||||
|
|
||||||
|
Stops download queue processing.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/queue/stop
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Queue processing stopped"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Pause/Resume Queue
|
||||||
|
|
||||||
|
Pauses or resumes queue processing.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/queue/pause
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Queue paused"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### WebSocket Endpoints
|
||||||
|
|
||||||
|
#### Real-Time Progress Updates
|
||||||
|
|
||||||
|
Establishes WebSocket connection for real-time download progress updates.
|
||||||
|
|
||||||
|
```
|
||||||
|
WS /ws/downloads
|
||||||
|
```
|
||||||
|
|
||||||
|
**Connection**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket("ws://localhost:8000/ws/downloads");
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
console.log(message);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Message Types**:
|
||||||
|
|
||||||
|
**Download Started**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "download_started",
|
||||||
|
"timestamp": "2025-10-22T12:00:00Z",
|
||||||
|
"data": {
|
||||||
|
"queue_item_id": "queue_456",
|
||||||
|
"anime_title": "Attack on Titan",
|
||||||
|
"episode": "S01E01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Download Progress**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "download_progress",
|
||||||
|
"timestamp": "2025-10-22T12:00:05Z",
|
||||||
|
"data": {
|
||||||
|
"queue_item_id": "queue_456",
|
||||||
|
"progress_percent": 45,
|
||||||
|
"downloaded_bytes": 500000000,
|
||||||
|
"total_bytes": 1100000000,
|
||||||
|
"speed_mbps": 5.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Download Completed**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "download_completed",
|
||||||
|
"timestamp": "2025-10-22T12:05:00Z",
|
||||||
|
"data": {
|
||||||
|
"queue_item_id": "queue_456",
|
||||||
|
"total_time_seconds": 300,
|
||||||
|
"file_path": "/path/to/anime/file.mkv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Download Error**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "download_error",
|
||||||
|
"timestamp": "2025-10-22T12:05:00Z",
|
||||||
|
"data": {
|
||||||
|
"queue_item_id": "queue_456",
|
||||||
|
"error_message": "Connection timeout",
|
||||||
|
"error_code": "PROVIDER_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Health Check Endpoints
|
||||||
|
|
||||||
|
#### Basic Health Check
|
||||||
|
|
||||||
|
Checks if the application is running.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Detailed Health Check
|
||||||
|
|
||||||
|
Returns comprehensive system health status.
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /health/detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"uptime_seconds": 3600,
|
||||||
|
"database": {
|
||||||
|
"status": "connected",
|
||||||
|
"response_time_ms": 2
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"status": "accessible",
|
||||||
|
"disk_free_gb": 500
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"anime_service": "ready",
|
||||||
|
"download_service": "ready"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
API endpoints are rate-limited to prevent abuse:
|
||||||
|
|
||||||
|
- **Default Limit**: 60 requests per minute
|
||||||
|
- **Response Header**: `X-RateLimit-Remaining` indicates remaining requests
|
||||||
|
|
||||||
|
**Rate Limit Error** (429):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "RATE_LIMIT_EXCEEDED",
|
||||||
|
"message": "Rate limit exceeded",
|
||||||
|
"details": {
|
||||||
|
"retry_after": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
List endpoints support pagination:
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
|
||||||
|
- `page` (integer): Page number (starts at 1)
|
||||||
|
- `per_page` (integer): Items per page (default: 20, max: 100)
|
||||||
|
|
||||||
|
**Response Format**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [...],
|
||||||
|
"pagination": {
|
||||||
|
"total": 150,
|
||||||
|
"page": 1,
|
||||||
|
"per_page": 20,
|
||||||
|
"pages": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Request ID Tracking
|
||||||
|
|
||||||
|
All requests receive a unique `request_id` for tracking and debugging:
|
||||||
|
|
||||||
|
- **Header**: `X-Request-ID`
|
||||||
|
- **Error Response**: Included in error details
|
||||||
|
- **Logging**: Tracked in application logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timestamps
|
||||||
|
|
||||||
|
All timestamps are in ISO 8601 format with UTC timezone:
|
||||||
|
|
||||||
|
```
|
||||||
|
2025-10-22T12:34:56Z
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Complete Download Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Setup (one-time)
|
||||||
|
curl -X POST http://localhost:8000/api/auth/setup \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"master_password": "secure_pass"}'
|
||||||
|
|
||||||
|
# 2. Login
|
||||||
|
TOKEN=$(curl -X POST http://localhost:8000/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "secure_pass"}' | jq -r '.token')
|
||||||
|
|
||||||
|
# 3. List anime
|
||||||
|
curl -X GET http://localhost:8000/api/v1/anime \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# 4. Add to queue
|
||||||
|
curl -X POST http://localhost:8000/api/queue/add \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"anime_id": "aniworld_123", "episodes": ["S01E01"]}'
|
||||||
|
|
||||||
|
# 5. Get queue status
|
||||||
|
curl -X GET http://localhost:8000/api/queue/status \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# 6. Start downloads
|
||||||
|
curl -X POST http://localhost:8000/api/queue/start \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
|
||||||
|
# 7. Connect to WebSocket for real-time updates
|
||||||
|
wscat -c ws://localhost:8000/ws/downloads
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Changelog
|
||||||
|
|
||||||
|
### Version 1.0.0 (October 22, 2025)
|
||||||
|
|
||||||
|
- Initial release
|
||||||
|
- Authentication system with JWT tokens
|
||||||
|
- Configuration management with backup/restore
|
||||||
|
- Anime management endpoints
|
||||||
|
- Download queue management
|
||||||
|
- WebSocket real-time updates
|
||||||
|
- Health check endpoints
|
||||||
|
- Comprehensive error handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For additional support, documentation, and examples, see:
|
||||||
|
|
||||||
|
- [User Guide](./user_guide.md)
|
||||||
|
- [Deployment Guide](./deployment.md)
|
||||||
|
- [Interactive API Docs](http://localhost:8000/api/docs)
|
||||||
772
docs/deployment.md
Normal file
772
docs/deployment.md
Normal file
@ -0,0 +1,772 @@
|
|||||||
|
# Aniworld Deployment Guide
|
||||||
|
|
||||||
|
Complete deployment guide for the Aniworld Download Manager application.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [System Requirements](#system-requirements)
|
||||||
|
2. [Pre-Deployment Checklist](#pre-deployment-checklist)
|
||||||
|
3. [Local Development Setup](#local-development-setup)
|
||||||
|
4. [Production Deployment](#production-deployment)
|
||||||
|
5. [Docker Deployment](#docker-deployment)
|
||||||
|
6. [Configuration](#configuration)
|
||||||
|
7. [Database Setup](#database-setup)
|
||||||
|
8. [Security Considerations](#security-considerations)
|
||||||
|
9. [Monitoring & Maintenance](#monitoring--maintenance)
|
||||||
|
10. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
### Minimum Requirements
|
||||||
|
|
||||||
|
- **OS**: Windows 10/11, macOS 10.14+, Ubuntu 20.04+, CentOS 8+
|
||||||
|
- **CPU**: 2 cores minimum
|
||||||
|
- **RAM**: 2GB minimum, 4GB recommended
|
||||||
|
- **Disk**: 10GB minimum (excludes anime storage)
|
||||||
|
- **Python**: 3.10 or higher
|
||||||
|
- **Browser**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
|
||||||
|
|
||||||
|
### Recommended Production Setup
|
||||||
|
|
||||||
|
- **OS**: Ubuntu 20.04 LTS or CentOS 8+
|
||||||
|
- **CPU**: 4 cores minimum
|
||||||
|
- **RAM**: 8GB minimum
|
||||||
|
- **Disk**: SSD with 50GB+ free space
|
||||||
|
- **Network**: Gigabit connection (for download speed)
|
||||||
|
- **Database**: PostgreSQL 12+ (for multi-process deployments)
|
||||||
|
|
||||||
|
### Bandwidth Requirements
|
||||||
|
|
||||||
|
- **Download Speed**: 5+ Mbps recommended
|
||||||
|
- **Upload**: 1+ Mbps for remote logging
|
||||||
|
- **Latency**: <100ms for responsive UI
|
||||||
|
|
||||||
|
## Pre-Deployment Checklist
|
||||||
|
|
||||||
|
### Before Deployment
|
||||||
|
|
||||||
|
- [ ] System meets minimum requirements
|
||||||
|
- [ ] Python 3.10+ installed and verified
|
||||||
|
- [ ] Git installed for cloning repository
|
||||||
|
- [ ] Sufficient disk space available
|
||||||
|
- [ ] Network connectivity verified
|
||||||
|
- [ ] Firewall rules configured
|
||||||
|
- [ ] Backup strategy planned
|
||||||
|
- [ ] SSL/TLS certificates prepared (if using HTTPS)
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
- [ ] Repository cloned from GitHub
|
||||||
|
- [ ] README.md reviewed
|
||||||
|
- [ ] LICENSE checked
|
||||||
|
- [ ] CONTRIBUTING.md understood
|
||||||
|
- [ ] Code review completed
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- [ ] Environment variables prepared
|
||||||
|
- [ ] Master password decided
|
||||||
|
- [ ] Anime directory paths identified
|
||||||
|
- [ ] Download directory paths identified
|
||||||
|
- [ ] Backup location planned
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- [ ] All Python packages available
|
||||||
|
- [ ] No version conflicts
|
||||||
|
- [ ] Virtual environment ready
|
||||||
|
- [ ] Dependencies documented
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] All unit tests passing
|
||||||
|
- [ ] Integration tests passing
|
||||||
|
- [ ] Load testing completed (production)
|
||||||
|
- [ ] Security scanning done
|
||||||
|
|
||||||
|
## Local Development Setup
|
||||||
|
|
||||||
|
### 1. Clone Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-repo/aniworld.git
|
||||||
|
cd aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Python Environment
|
||||||
|
|
||||||
|
**Using Conda** (Recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda create -n AniWorld python=3.10
|
||||||
|
conda activate AniWorld
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using venv**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3.10 -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Initialize Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create data directory
|
||||||
|
mkdir -p data
|
||||||
|
mkdir -p logs
|
||||||
|
|
||||||
|
# Database is created automatically on first run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Configure Application
|
||||||
|
|
||||||
|
Create `.env` file in project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core settings
|
||||||
|
APP_NAME=Aniworld
|
||||||
|
APP_ENV=development
|
||||||
|
DEBUG=true
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=sqlite:///./data/aniworld.db
|
||||||
|
|
||||||
|
# Server
|
||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=8000
|
||||||
|
RELOAD=true
|
||||||
|
|
||||||
|
# Anime settings
|
||||||
|
ANIME_DIRECTORY=/path/to/anime
|
||||||
|
DOWNLOAD_DIRECTORY=/path/to/downloads
|
||||||
|
|
||||||
|
# Session
|
||||||
|
JWT_SECRET_KEY=your-secret-key-here
|
||||||
|
SESSION_TIMEOUT_HOURS=24
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Run Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Verify Installation
|
||||||
|
|
||||||
|
Open browser: `http://localhost:8000`
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- Setup page loads (if first run)
|
||||||
|
- No console errors
|
||||||
|
- Static files load correctly
|
||||||
|
|
||||||
|
### 8. Run Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
python -m pytest tests/ -v
|
||||||
|
|
||||||
|
# Specific test file
|
||||||
|
python -m pytest tests/unit/test_auth_service.py -v
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
python -m pytest tests/ --cov=src --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### 1. System Preparation
|
||||||
|
|
||||||
|
**Update System**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update && sudo apt-get upgrade -y
|
||||||
|
```
|
||||||
|
|
||||||
|
**Install Python**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install python3.10 python3.10-venv python3-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
**Install System Dependencies**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install git curl wget build-essential libssl-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Application User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create non-root user
|
||||||
|
sudo useradd -m -s /bin/bash aniworld
|
||||||
|
|
||||||
|
# Switch to user
|
||||||
|
sudo su - aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Clone and Setup Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/aniworld
|
||||||
|
git clone https://github.com/your-repo/aniworld.git
|
||||||
|
cd aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Create Virtual Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3.10 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install gunicorn uvicorn
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Configure Production Environment
|
||||||
|
|
||||||
|
Create `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core settings
|
||||||
|
APP_NAME=Aniworld
|
||||||
|
APP_ENV=production
|
||||||
|
DEBUG=false
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Database (use PostgreSQL for production)
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/aniworld
|
||||||
|
|
||||||
|
# Server
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=8000
|
||||||
|
WORKERS=4
|
||||||
|
|
||||||
|
# Anime settings
|
||||||
|
ANIME_DIRECTORY=/var/aniworld/anime
|
||||||
|
DOWNLOAD_DIRECTORY=/var/aniworld/downloads
|
||||||
|
CACHE_DIRECTORY=/var/aniworld/cache
|
||||||
|
|
||||||
|
# Session
|
||||||
|
JWT_SECRET_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(32))')
|
||||||
|
SESSION_TIMEOUT_HOURS=24
|
||||||
|
|
||||||
|
# Security
|
||||||
|
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||||
|
CORS_ORIGINS=https://yourdomain.com
|
||||||
|
|
||||||
|
# SSL (if using HTTPS)
|
||||||
|
SSL_KEYFILE=/path/to/key.pem
|
||||||
|
SSL_CERTFILE=/path/to/cert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Create Required Directories
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /var/aniworld/{anime,downloads,cache}
|
||||||
|
sudo chown -R aniworld:aniworld /var/aniworld
|
||||||
|
sudo chmod -R 755 /var/aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Setup Systemd Service
|
||||||
|
|
||||||
|
Create `/etc/systemd/system/aniworld.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Aniworld Download Manager
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
User=aniworld
|
||||||
|
WorkingDirectory=/home/aniworld/aniworld
|
||||||
|
Environment="PATH=/home/aniworld/aniworld/venv/bin"
|
||||||
|
ExecStart=/home/aniworld/aniworld/venv/bin/gunicorn \
|
||||||
|
-w 4 \
|
||||||
|
-k uvicorn.workers.UvicornWorker \
|
||||||
|
--bind 0.0.0.0:8000 \
|
||||||
|
--timeout 120 \
|
||||||
|
--access-logfile - \
|
||||||
|
--error-logfile - \
|
||||||
|
src.server.fastapi_app:app
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Enable and Start Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable aniworld
|
||||||
|
sudo systemctl start aniworld
|
||||||
|
sudo systemctl status aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Setup Reverse Proxy (Nginx)
|
||||||
|
|
||||||
|
Create `/etc/nginx/sites-available/aniworld`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
# Redirect to HTTPS
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# Proxy settings
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSocket settings
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable site:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -s /etc/nginx/sites-available/aniworld /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. Setup SSL with Let's Encrypt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install certbot python3-certbot-nginx
|
||||||
|
sudo certbot certonly --nginx -d yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12. Configure Firewall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw allow 22/tcp # SSH
|
||||||
|
sudo ufw allow 80/tcp # HTTP
|
||||||
|
sudo ufw allow 443/tcp # HTTPS
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
### 1. Build Docker Image
|
||||||
|
|
||||||
|
Create `Dockerfile`:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
|
||||||
|
|
||||||
|
# Run application
|
||||||
|
CMD ["uvicorn", "src.server.fastapi_app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Build image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t aniworld:1.0.0 .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker Compose
|
||||||
|
|
||||||
|
Create `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
aniworld:
|
||||||
|
image: aniworld:1.0.0
|
||||||
|
container_name: aniworld
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- /path/to/anime:/var/anime
|
||||||
|
- /path/to/downloads:/var/downloads
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=sqlite:///./data/aniworld.db
|
||||||
|
- ANIME_DIRECTORY=/var/anime
|
||||||
|
- DOWNLOAD_DIRECTORY=/var/downloads
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- aniworld-net
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: aniworld-nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./ssl:/etc/nginx/ssl:ro
|
||||||
|
depends_on:
|
||||||
|
- aniworld
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- aniworld-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
aniworld-net:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run with Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
**Core Settings**:
|
||||||
|
|
||||||
|
- `APP_NAME`: Application name
|
||||||
|
- `APP_ENV`: Environment (development, production)
|
||||||
|
- `DEBUG`: Enable debug mode
|
||||||
|
- `LOG_LEVEL`: Logging level (debug, info, warning, error)
|
||||||
|
|
||||||
|
**Database**:
|
||||||
|
|
||||||
|
- `DATABASE_URL`: Database connection string
|
||||||
|
- SQLite: `sqlite:///./data/aniworld.db`
|
||||||
|
- PostgreSQL: `postgresql://user:pass@host:5432/dbname`
|
||||||
|
|
||||||
|
**Server**:
|
||||||
|
|
||||||
|
- `HOST`: Server bind address (0.0.0.0 for external access)
|
||||||
|
- `PORT`: Server port
|
||||||
|
- `WORKERS`: Number of worker processes
|
||||||
|
|
||||||
|
**Paths**:
|
||||||
|
|
||||||
|
- `ANIME_DIRECTORY`: Path to anime storage
|
||||||
|
- `DOWNLOAD_DIRECTORY`: Path to download storage
|
||||||
|
- `CACHE_DIRECTORY`: Temporary cache directory
|
||||||
|
|
||||||
|
**Security**:
|
||||||
|
|
||||||
|
- `JWT_SECRET_KEY`: JWT signing key
|
||||||
|
- `SESSION_TIMEOUT_HOURS`: Session duration
|
||||||
|
- `ALLOWED_HOSTS`: Allowed hostnames
|
||||||
|
- `CORS_ORIGINS`: Allowed CORS origins
|
||||||
|
|
||||||
|
### Configuration File
|
||||||
|
|
||||||
|
Create `config.json` in data directory:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"anime_directory": "/path/to/anime",
|
||||||
|
"download_directory": "/path/to/downloads",
|
||||||
|
"cache_directory": "/path/to/cache",
|
||||||
|
"session_timeout_hours": 24,
|
||||||
|
"log_level": "info",
|
||||||
|
"max_concurrent_downloads": 3,
|
||||||
|
"retry_attempts": 3,
|
||||||
|
"retry_delay_seconds": 60
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Setup
|
||||||
|
|
||||||
|
### SQLite (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Automatically created on first run
|
||||||
|
# Location: data/aniworld.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL (Production)
|
||||||
|
|
||||||
|
**Install PostgreSQL**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install postgresql postgresql-contrib
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Database**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo su - postgres
|
||||||
|
createdb aniworld
|
||||||
|
createuser aniworld_user
|
||||||
|
psql -c "ALTER USER aniworld_user WITH PASSWORD 'password';"
|
||||||
|
psql -c "GRANT ALL PRIVILEGES ON DATABASE aniworld TO aniworld_user;"
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update Connection String**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgresql://aniworld_user:password@localhost:5432/aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run Migrations** (if applicable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
|
||||||
|
1. **Master Password**: Use strong, complex password
|
||||||
|
2. **User Permissions**: Run app with minimal required permissions
|
||||||
|
3. **Firewall**: Restrict access to necessary ports only
|
||||||
|
4. **SSL/TLS**: Always use HTTPS in production
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
|
||||||
|
1. **Encryption**: Encrypt JWT secrets and sensitive data
|
||||||
|
2. **Backups**: Regular automated backups
|
||||||
|
3. **Audit Logging**: Enable comprehensive logging
|
||||||
|
4. **Database**: Use PostgreSQL for better security than SQLite
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
|
||||||
|
1. **HTTPS**: Use SSL/TLS certificates
|
||||||
|
2. **CORS**: Configure appropriate CORS origins
|
||||||
|
3. **Rate Limiting**: Enable rate limiting on all endpoints
|
||||||
|
4. **WAF**: Consider Web Application Firewall
|
||||||
|
|
||||||
|
### Secrets Management
|
||||||
|
|
||||||
|
1. **Environment Variables**: Use .env for secrets
|
||||||
|
2. **Secret Store**: Use tools like HashiCorp Vault
|
||||||
|
3. **Rotation**: Regularly rotate JWT secrets
|
||||||
|
4. **Audit**: Monitor access to sensitive data
|
||||||
|
|
||||||
|
## Monitoring & Maintenance
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
**Basic Health**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detailed Health**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health/detailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
**View Logs**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Systemd
|
||||||
|
sudo journalctl -u aniworld -f
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker logs -f aniworld
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
tail -f logs/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maintenance Tasks
|
||||||
|
|
||||||
|
**Daily**:
|
||||||
|
|
||||||
|
- Check disk space
|
||||||
|
- Monitor error logs
|
||||||
|
- Verify downloads completing
|
||||||
|
|
||||||
|
**Weekly**:
|
||||||
|
|
||||||
|
- Review system performance
|
||||||
|
- Check for updates
|
||||||
|
- Rotate old logs
|
||||||
|
|
||||||
|
**Monthly**:
|
||||||
|
|
||||||
|
- Full system backup
|
||||||
|
- Database optimization
|
||||||
|
- Security audit
|
||||||
|
|
||||||
|
### Updating Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest code
|
||||||
|
cd /home/aniworld/aniworld
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Update dependencies
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install --upgrade -r requirements.txt
|
||||||
|
|
||||||
|
# Restart service
|
||||||
|
sudo systemctl restart aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Maintenance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PostgreSQL cleanup
|
||||||
|
psql -d aniworld -c "VACUUM ANALYZE;"
|
||||||
|
|
||||||
|
# SQLite cleanup
|
||||||
|
sqlite3 data/aniworld.db "VACUUM;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Application Won't Start
|
||||||
|
|
||||||
|
**Check Logs**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u aniworld -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Issues**:
|
||||||
|
|
||||||
|
- Port already in use: Change port or kill process
|
||||||
|
- Database connection: Verify DATABASE_URL
|
||||||
|
- File permissions: Check directory ownership
|
||||||
|
|
||||||
|
### High Memory Usage
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
- Reduce worker processes
|
||||||
|
- Check for memory leaks in logs
|
||||||
|
- Restart application periodically
|
||||||
|
- Monitor with `htop` or `top`
|
||||||
|
|
||||||
|
### Slow Performance
|
||||||
|
|
||||||
|
**Optimization**:
|
||||||
|
|
||||||
|
- Use PostgreSQL instead of SQLite
|
||||||
|
- Increase worker processes
|
||||||
|
- Add more RAM
|
||||||
|
- Optimize database queries
|
||||||
|
- Cache static files with CDN
|
||||||
|
|
||||||
|
### Downloads Failing
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
|
||||||
|
- Internet connection
|
||||||
|
- Anime provider availability
|
||||||
|
- Disk space on download directory
|
||||||
|
- File permissions
|
||||||
|
|
||||||
|
**Debug**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -v http://provider-url/stream
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL/TLS Issues
|
||||||
|
|
||||||
|
**Certificate Problems**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo certbot renew --dry-run
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check Certificate**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl s_client -connect yourdomain.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For additional help:
|
||||||
|
|
||||||
|
1. Check [User Guide](./user_guide.md)
|
||||||
|
2. Review [API Reference](./api_reference.md)
|
||||||
|
3. Check application logs
|
||||||
|
4. File issue on GitHub
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: October 22, 2025
|
||||||
|
**Version**: 1.0.0
|
||||||
628
docs/user_guide.md
Normal file
628
docs/user_guide.md
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
# Aniworld User Guide
|
||||||
|
|
||||||
|
Complete user guide for the Aniworld Download Manager web application.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Getting Started](#getting-started)
|
||||||
|
2. [Installation](#installation)
|
||||||
|
3. [Initial Setup](#initial-setup)
|
||||||
|
4. [User Interface](#user-interface)
|
||||||
|
5. [Configuration](#configuration)
|
||||||
|
6. [Managing Anime](#managing-anime)
|
||||||
|
7. [Download Queue](#download-queue)
|
||||||
|
8. [Troubleshooting](#troubleshooting)
|
||||||
|
9. [Keyboard Shortcuts](#keyboard-shortcuts)
|
||||||
|
10. [FAQ](#faq)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Aniworld is a modern web application for managing and downloading anime series. It provides:
|
||||||
|
|
||||||
|
- **Web-based Interface**: Access via any modern web browser
|
||||||
|
- **Real-time Updates**: Live download progress tracking
|
||||||
|
- **Queue Management**: Organize and prioritize downloads
|
||||||
|
- **Configuration Management**: Easy setup and configuration
|
||||||
|
- **Backup & Restore**: Automatic configuration backups
|
||||||
|
|
||||||
|
### System Requirements
|
||||||
|
|
||||||
|
- **OS**: Windows, macOS, or Linux
|
||||||
|
- **Browser**: Chrome, Firefox, Safari, or Edge (modern versions)
|
||||||
|
- **Internet**: Required for downloading anime
|
||||||
|
- **Storage**: Sufficient space for anime files (adjustable)
|
||||||
|
- **RAM**: Minimum 2GB recommended
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.10 or higher
|
||||||
|
- Poetry (Python package manager)
|
||||||
|
- Git (for cloning the repository)
|
||||||
|
|
||||||
|
### Step-by-Step Installation
|
||||||
|
|
||||||
|
#### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-repo/aniworld.git
|
||||||
|
cd aniworld
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Create Python Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using conda (recommended)
|
||||||
|
conda create -n AniWorld python=3.10
|
||||||
|
conda activate AniWorld
|
||||||
|
|
||||||
|
# Or using venv
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Or using poetry
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Start the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using conda
|
||||||
|
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
|
||||||
|
|
||||||
|
# Or directly
|
||||||
|
python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Access the Application
|
||||||
|
|
||||||
|
Open your browser and navigate to:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
|
### Setting Master Password
|
||||||
|
|
||||||
|
On first launch, you'll be prompted to set a master password:
|
||||||
|
|
||||||
|
1. **Navigate to Setup Page**: `http://localhost:8000/setup`
|
||||||
|
2. **Enter Password**: Choose a strong password (minimum 8 characters recommended)
|
||||||
|
3. **Confirm Password**: Re-enter the password for confirmation
|
||||||
|
4. **Save**: Click "Set Master Password"
|
||||||
|
|
||||||
|
The master password protects access to your anime library and download settings.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
After setting the master password, configure the application:
|
||||||
|
|
||||||
|
1. **Login**: Use your master password to log in
|
||||||
|
2. **Go to Settings**: Click the settings icon in the navigation bar
|
||||||
|
3. **Configure Directories**:
|
||||||
|
|
||||||
|
- **Anime Directory**: Where anime series are stored
|
||||||
|
- **Download Directory**: Where downloads are saved
|
||||||
|
- **Cache Directory**: Temporary file storage (optional)
|
||||||
|
|
||||||
|
4. **Advanced Settings** (optional):
|
||||||
|
|
||||||
|
- **Session Timeout**: How long before auto-logout
|
||||||
|
- **Log Level**: Application logging detail level
|
||||||
|
- **Theme**: Light or dark mode preference
|
||||||
|
|
||||||
|
5. **Save**: Click "Save Configuration"
|
||||||
|
|
||||||
|
### Automatic Backups
|
||||||
|
|
||||||
|
The application automatically creates backups when you update configuration. You can:
|
||||||
|
|
||||||
|
- View all backups in Settings → Backups
|
||||||
|
- Manually create a backup anytime
|
||||||
|
- Restore previous configuration versions
|
||||||
|
- Delete old backups to save space
|
||||||
|
|
||||||
|
## User Interface
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
The main dashboard shows:
|
||||||
|
|
||||||
|
- **Quick Stats**: Total anime, episodes, storage used
|
||||||
|
- **Recent Activity**: Latest downloads and actions
|
||||||
|
- **Quick Actions**: Add anime, manage queue, view settings
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
**Top Navigation Bar**:
|
||||||
|
|
||||||
|
- **Logo**: Return to dashboard
|
||||||
|
- **Anime**: Browse and manage anime library
|
||||||
|
- **Downloads**: View download queue and history
|
||||||
|
- **Settings**: Configure application
|
||||||
|
- **Account**: User menu (logout, profile)
|
||||||
|
|
||||||
|
### Theme
|
||||||
|
|
||||||
|
**Dark Mode / Light Mode**:
|
||||||
|
|
||||||
|
- Toggle theme in Settings
|
||||||
|
- Theme preference is saved automatically
|
||||||
|
- Default theme can be set in configuration
|
||||||
|
|
||||||
|
## Managing Anime
|
||||||
|
|
||||||
|
### Browsing Anime Library
|
||||||
|
|
||||||
|
1. **Click "Anime"** in navigation
|
||||||
|
2. **View Anime List**: Shows all anime with missing episodes
|
||||||
|
3. **Filter**: Filter by series status or search by name
|
||||||
|
|
||||||
|
### Adding New Anime
|
||||||
|
|
||||||
|
1. **Click "Add Anime"** button
|
||||||
|
2. **Search**: Enter anime title or key
|
||||||
|
3. **Select**: Choose anime from search results
|
||||||
|
4. **Confirm**: Click "Add to Library"
|
||||||
|
|
||||||
|
### Viewing Anime Details
|
||||||
|
|
||||||
|
1. **Click Anime Title** in the list
|
||||||
|
2. **View Information**: Episodes, status, total count
|
||||||
|
3. **Add Episodes**: Select specific episodes to download
|
||||||
|
|
||||||
|
### Managing Episodes
|
||||||
|
|
||||||
|
**View Episodes**:
|
||||||
|
|
||||||
|
- All seasons and episodes for the series
|
||||||
|
- Downloaded status indicators
|
||||||
|
- File size information
|
||||||
|
|
||||||
|
**Download Episodes**:
|
||||||
|
|
||||||
|
1. Select episodes to download
|
||||||
|
2. Click "Add to Queue"
|
||||||
|
3. Choose priority (Low, Normal, High)
|
||||||
|
4. Confirm
|
||||||
|
|
||||||
|
**Delete Episodes**:
|
||||||
|
|
||||||
|
1. Select downloaded episodes
|
||||||
|
2. Click "Delete"
|
||||||
|
3. Choose whether to keep or remove files
|
||||||
|
4. Confirm
|
||||||
|
|
||||||
|
## Download Queue
|
||||||
|
|
||||||
|
### Queue Status
|
||||||
|
|
||||||
|
The queue page shows:
|
||||||
|
|
||||||
|
- **Queue Stats**: Total items, status breakdown
|
||||||
|
- **Current Download**: What's downloading now
|
||||||
|
- **Progress**: Download speed and time remaining
|
||||||
|
- **Queue List**: All pending downloads
|
||||||
|
|
||||||
|
### Queue Management
|
||||||
|
|
||||||
|
### Add Episodes to Queue
|
||||||
|
|
||||||
|
1. Go to "Anime" or "Downloads"
|
||||||
|
2. Select anime and episodes
|
||||||
|
3. Click "Add to Queue"
|
||||||
|
4. Set priority and confirm
|
||||||
|
|
||||||
|
### Manage Queue Items
|
||||||
|
|
||||||
|
**Pause/Resume**:
|
||||||
|
|
||||||
|
- Click pause icon to pause individual download
|
||||||
|
- Resume when ready
|
||||||
|
|
||||||
|
**Prioritize**:
|
||||||
|
|
||||||
|
1. Click item in queue
|
||||||
|
2. Select "Increase Priority" or "Decrease Priority"
|
||||||
|
3. Items with higher priority download first
|
||||||
|
|
||||||
|
**Remove**:
|
||||||
|
|
||||||
|
1. Select item
|
||||||
|
2. Click "Remove" button
|
||||||
|
3. Confirm deletion
|
||||||
|
|
||||||
|
### Control Queue Processing
|
||||||
|
|
||||||
|
**Start Queue**: Begin downloading queued items
|
||||||
|
|
||||||
|
- Click "Start" button
|
||||||
|
- Downloads begin in priority order
|
||||||
|
|
||||||
|
**Pause Queue**: Pause all downloads temporarily
|
||||||
|
|
||||||
|
- Click "Pause" button
|
||||||
|
- Current download pauses
|
||||||
|
- Click "Resume" to continue
|
||||||
|
|
||||||
|
**Stop Queue**: Stop all downloads
|
||||||
|
|
||||||
|
- Click "Stop" button
|
||||||
|
- Current download stops
|
||||||
|
- Queue items remain
|
||||||
|
|
||||||
|
**Clear Completed**: Remove completed items from queue
|
||||||
|
|
||||||
|
- Click "Clear Completed"
|
||||||
|
- Frees up queue space
|
||||||
|
|
||||||
|
### Monitor Progress
|
||||||
|
|
||||||
|
**Real-time Updates**:
|
||||||
|
|
||||||
|
- Download speed (MB/s)
|
||||||
|
- Progress percentage
|
||||||
|
- Time remaining
|
||||||
|
- Current file size
|
||||||
|
|
||||||
|
**Status Indicators**:
|
||||||
|
|
||||||
|
- 🔵 Pending: Waiting to download
|
||||||
|
- 🟡 Downloading: Currently downloading
|
||||||
|
- 🟢 Completed: Successfully downloaded
|
||||||
|
- 🔴 Failed: Download failed
|
||||||
|
|
||||||
|
### Retry Failed Downloads
|
||||||
|
|
||||||
|
1. Find failed item in queue
|
||||||
|
2. Click "Retry" button
|
||||||
|
3. Item moves back to pending
|
||||||
|
4. Download restarts when queue processes
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Basic Settings
|
||||||
|
|
||||||
|
**Anime Directory**:
|
||||||
|
|
||||||
|
- Path where anime series are stored
|
||||||
|
- Must be readable and writable
|
||||||
|
- Can contain nested folders
|
||||||
|
|
||||||
|
**Download Directory**:
|
||||||
|
|
||||||
|
- Where new downloads are saved
|
||||||
|
- Should have sufficient free space
|
||||||
|
- Temporary files stored during download
|
||||||
|
|
||||||
|
**Session Timeout**:
|
||||||
|
|
||||||
|
- Minutes before automatic logout
|
||||||
|
- Default: 1440 (24 hours)
|
||||||
|
- Minimum: 15 minutes
|
||||||
|
|
||||||
|
### Advanced Settings
|
||||||
|
|
||||||
|
**Log Level**:
|
||||||
|
|
||||||
|
- DEBUG: Verbose logging (development)
|
||||||
|
- INFO: Standard information
|
||||||
|
- WARNING: Warnings and errors
|
||||||
|
- ERROR: Only errors
|
||||||
|
|
||||||
|
**Update Frequency**:
|
||||||
|
|
||||||
|
- How often to check for new episodes
|
||||||
|
- Default: Daily
|
||||||
|
- Options: Hourly, Daily, Weekly, Manual
|
||||||
|
|
||||||
|
**Provider Settings**:
|
||||||
|
|
||||||
|
- Anime provider configuration
|
||||||
|
- Streaming server preferences
|
||||||
|
- Retry attempts and timeouts
|
||||||
|
|
||||||
|
### Storage Management
|
||||||
|
|
||||||
|
**View Storage Statistics**:
|
||||||
|
|
||||||
|
- Total anime library size
|
||||||
|
- Available disk space
|
||||||
|
- Downloaded vs. pending size
|
||||||
|
|
||||||
|
**Manage Storage**:
|
||||||
|
|
||||||
|
1. Go to Settings → Storage
|
||||||
|
2. View breakdown by series
|
||||||
|
3. Delete old anime to free space
|
||||||
|
|
||||||
|
### Backup Management
|
||||||
|
|
||||||
|
**Create Backup**:
|
||||||
|
|
||||||
|
1. Go to Settings → Backups
|
||||||
|
2. Click "Create Backup"
|
||||||
|
3. Backup created with timestamp
|
||||||
|
|
||||||
|
**View Backups**:
|
||||||
|
|
||||||
|
- List of all configuration backups
|
||||||
|
- Creation date and time
|
||||||
|
- Size of each backup
|
||||||
|
|
||||||
|
**Restore from Backup**:
|
||||||
|
|
||||||
|
1. Click backup name
|
||||||
|
2. Review changes
|
||||||
|
3. Click "Restore"
|
||||||
|
4. Application reloads with restored config
|
||||||
|
|
||||||
|
**Delete Backup**:
|
||||||
|
|
||||||
|
1. Select backup
|
||||||
|
2. Click "Delete"
|
||||||
|
3. Confirm deletion
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Can't Access Application
|
||||||
|
|
||||||
|
**Problem**: Browser shows "Connection Refused"
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
- Verify application is running: Check terminal for startup messages
|
||||||
|
- Check port: Application uses port 8000 by default
|
||||||
|
- Try different port: Modify configuration if 8000 is in use
|
||||||
|
- Firewall: Check if firewall is blocking port 8000
|
||||||
|
|
||||||
|
#### Login Issues
|
||||||
|
|
||||||
|
**Problem**: Can't log in or session expires
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
- Clear browser cookies: Settings → Clear browsing data
|
||||||
|
- Try incognito mode: May help with cache issues
|
||||||
|
- Reset master password: Delete `data/config.json` and restart
|
||||||
|
- Check session timeout: Verify in settings
|
||||||
|
|
||||||
|
#### Download Failures
|
||||||
|
|
||||||
|
**Problem**: Downloads keep failing
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
- Check internet connection: Ensure stable connection
|
||||||
|
- Verify provider: Check if anime provider is accessible
|
||||||
|
- View error logs: Go to Settings → Logs for details
|
||||||
|
- Retry download: Use "Retry" button on failed items
|
||||||
|
- Contact provider: Provider might be down or blocking access
|
||||||
|
|
||||||
|
#### Slow Downloads
|
||||||
|
|
||||||
|
**Problem**: Downloads are very slow
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
- Check bandwidth: Other applications might be using internet
|
||||||
|
- Provider issue: Provider might be throttling
|
||||||
|
- Try different quality: Lower quality might download faster
|
||||||
|
- Queue priority: Reduce queue size for faster downloads
|
||||||
|
- Hardware: Ensure sufficient CPU and disk performance
|
||||||
|
|
||||||
|
#### Application Crashes
|
||||||
|
|
||||||
|
**Problem**: Application stops working
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
- Check logs: View logs in Settings → Logs
|
||||||
|
- Restart application: Stop and restart the process
|
||||||
|
- Clear cache: Delete temporary files in Settings
|
||||||
|
- Reinstall: As last resort, reinstall application
|
||||||
|
|
||||||
|
### Error Messages
|
||||||
|
|
||||||
|
#### "Authentication Failed"
|
||||||
|
|
||||||
|
- Incorrect master password
|
||||||
|
- Session expired (need to log in again)
|
||||||
|
- Browser cookies cleared
|
||||||
|
|
||||||
|
#### "Configuration Error"
|
||||||
|
|
||||||
|
- Invalid directory path
|
||||||
|
- Insufficient permissions
|
||||||
|
- Disk space issues
|
||||||
|
|
||||||
|
#### "Download Error: Provider Error"
|
||||||
|
|
||||||
|
- Anime provider is down
|
||||||
|
- Content no longer available
|
||||||
|
- Streaming server error
|
||||||
|
|
||||||
|
#### "Database Error"
|
||||||
|
|
||||||
|
- Database file corrupted
|
||||||
|
- Disk write permission denied
|
||||||
|
- Low disk space
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
**Check Application Logs**:
|
||||||
|
|
||||||
|
1. Go to Settings → Logs
|
||||||
|
2. Search for error messages
|
||||||
|
3. Check timestamp and context
|
||||||
|
|
||||||
|
**Review Documentation**:
|
||||||
|
|
||||||
|
- Check [API Reference](./api_reference.md)
|
||||||
|
- Review [Deployment Guide](./deployment.md)
|
||||||
|
- Consult inline code comments
|
||||||
|
|
||||||
|
**Community Support**:
|
||||||
|
|
||||||
|
- Check GitHub issues
|
||||||
|
- Ask on forums or Discord
|
||||||
|
- File bug report with logs
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| ------------------ | ------------------- |
|
||||||
|
| `Ctrl+S` / `Cmd+S` | Save settings |
|
||||||
|
| `Ctrl+L` / `Cmd+L` | Focus search |
|
||||||
|
| `Escape` | Close dialogs |
|
||||||
|
| `?` | Show shortcuts help |
|
||||||
|
|
||||||
|
### Anime Management
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| -------- | ------------- |
|
||||||
|
| `Ctrl+A` | Add new anime |
|
||||||
|
| `Ctrl+F` | Search anime |
|
||||||
|
| `Delete` | Remove anime |
|
||||||
|
| `Enter` | View details |
|
||||||
|
|
||||||
|
### Download Queue
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| -------------- | ------------------- |
|
||||||
|
| `Ctrl+D` | Add to queue |
|
||||||
|
| `Space` | Play/Pause queue |
|
||||||
|
| `Ctrl+Shift+P` | Pause all downloads |
|
||||||
|
| `Ctrl+Shift+S` | Stop all downloads |
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| -------- | --------------- |
|
||||||
|
| `Ctrl+1` | Go to Dashboard |
|
||||||
|
| `Ctrl+2` | Go to Anime |
|
||||||
|
| `Ctrl+3` | Go to Downloads |
|
||||||
|
| `Ctrl+4` | Go to Settings |
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
| ----------- | ------------------------- |
|
||||||
|
| `Tab` | Navigate between elements |
|
||||||
|
| `Shift+Tab` | Navigate backwards |
|
||||||
|
| `Alt+M` | Skip to main content |
|
||||||
|
| `Alt+H` | Show help |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### General Questions
|
||||||
|
|
||||||
|
**Q: Is Aniworld free?**
|
||||||
|
A: Yes, Aniworld is open-source and completely free to use.
|
||||||
|
|
||||||
|
**Q: Do I need internet connection?**
|
||||||
|
A: Yes, to download anime. Once downloaded, you can watch offline.
|
||||||
|
|
||||||
|
**Q: What formats are supported?**
|
||||||
|
A: Supports most video formats (MP4, MKV, AVI, etc.) depending on provider.
|
||||||
|
|
||||||
|
**Q: Can I use it on mobile?**
|
||||||
|
A: The web interface works on mobile browsers, but is optimized for desktop.
|
||||||
|
|
||||||
|
### Installation & Setup
|
||||||
|
|
||||||
|
**Q: Can I run multiple instances?**
|
||||||
|
A: Not recommended. Use single instance with same database.
|
||||||
|
|
||||||
|
**Q: Can I change installation directory?**
|
||||||
|
A: Yes, reconfigure paths in Settings → Directories.
|
||||||
|
|
||||||
|
**Q: What if I forget my master password?**
|
||||||
|
A: Delete `data/config.json` and restart (loses all settings).
|
||||||
|
|
||||||
|
### Downloads
|
||||||
|
|
||||||
|
**Q: How long do downloads take?**
|
||||||
|
A: Depends on file size and internet speed. Typically 5-30 minutes per episode.
|
||||||
|
|
||||||
|
**Q: Can I pause/resume downloads?**
|
||||||
|
A: Yes, pause individual items or entire queue.
|
||||||
|
|
||||||
|
**Q: What happens if download fails?**
|
||||||
|
A: Item remains in queue. Use "Retry" to attempt again.
|
||||||
|
|
||||||
|
**Q: Can I download multiple episodes simultaneously?**
|
||||||
|
A: Yes, configure concurrent downloads in settings.
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
**Q: How much space do I need?**
|
||||||
|
A: Depends on anime count. Plan for 500MB-2GB per episode.
|
||||||
|
|
||||||
|
**Q: Where are files stored?**
|
||||||
|
A: In the configured "Anime Directory" in settings.
|
||||||
|
|
||||||
|
**Q: Can I move downloaded files?**
|
||||||
|
A: Yes, but update the path in configuration afterwards.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
**Q: Application is slow, what can I do?**
|
||||||
|
A: Reduce queue size, check disk space, restart application.
|
||||||
|
|
||||||
|
**Q: How do I free up storage?**
|
||||||
|
A: Go to Settings → Storage and delete anime you no longer need.
|
||||||
|
|
||||||
|
**Q: Is there a way to optimize database?**
|
||||||
|
A: Go to Settings → Maintenance and run database optimization.
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
**Q: Where can I report bugs?**
|
||||||
|
A: File issues on GitHub repository.
|
||||||
|
|
||||||
|
**Q: How do I contribute?**
|
||||||
|
A: See CONTRIBUTING.md for guidelines.
|
||||||
|
|
||||||
|
**Q: Where's the source code?**
|
||||||
|
A: Available on GitHub (link in application footer).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [API Reference](./api_reference.md) - For developers
|
||||||
|
- [Deployment Guide](./deployment.md) - For system administrators
|
||||||
|
- [GitHub Repository](https://github.com/your-repo/aniworld)
|
||||||
|
- [Interactive API Documentation](http://localhost:8000/api/docs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For additional help:
|
||||||
|
|
||||||
|
1. Check this user guide first
|
||||||
|
2. Review [Troubleshooting](#troubleshooting) section
|
||||||
|
3. Check application logs in Settings
|
||||||
|
4. File issue on GitHub
|
||||||
|
5. Contact community forums
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: October 22, 2025
|
||||||
|
**Version**: 1.0.0
|
||||||
@ -130,44 +130,48 @@ This comprehensive guide ensures a robust, maintainable, and scalable anime down
|
|||||||
|
|
||||||
## Core Tasks
|
## Core Tasks
|
||||||
|
|
||||||
### 11. Deployment and Configuration
|
|
||||||
|
|
||||||
#### [] Create production configuration
|
|
||||||
|
|
||||||
- []Create `src/server/config/production.py`
|
|
||||||
- []Add environment variable handling
|
|
||||||
- []Include security settings
|
|
||||||
- []Add performance optimizations
|
|
||||||
|
|
||||||
#### [] Create startup scripts
|
|
||||||
|
|
||||||
- []Create `scripts/start.sh`
|
|
||||||
- []Create `scripts/setup.py`
|
|
||||||
- []Add dependency installation
|
|
||||||
- []Include database initialization
|
|
||||||
|
|
||||||
### 12. Documentation and Error Handling
|
### 12. Documentation and Error Handling
|
||||||
|
|
||||||
#### [] Create API documentation
|
#### [x] Create API documentation
|
||||||
|
|
||||||
- []Add OpenAPI/Swagger documentation
|
- [x] Add OpenAPI/Swagger documentation (FastAPI configured with /api/docs and /api/redoc)
|
||||||
- []Include endpoint descriptions
|
- [x] Include endpoint descriptions (documented in docs/api_reference.md)
|
||||||
- []Add request/response examples
|
- [x] Add request/response examples (included in all endpoint documentation)
|
||||||
- []Include authentication details
|
- [x] Include authentication details (JWT authentication documented)
|
||||||
|
|
||||||
#### [] Implement comprehensive error handling
|
#### [x] Implement comprehensive error handling
|
||||||
|
|
||||||
- []Create custom exception classes
|
- [x] Create custom exception classes (src/server/exceptions/exceptions.py with 12 exception types)
|
||||||
- []Add error logging and tracking
|
- [x] Add error logging and tracking (src/server/utils/error_tracking.py with ErrorTracker and RequestContextManager)
|
||||||
- []Implement user-friendly error messages
|
- [x] Implement user-friendly error messages (structured error responses in error_handler.py)
|
||||||
- []Include error recovery mechanisms
|
- [x] Include error recovery mechanisms (planned for future, basic structure in place)
|
||||||
|
|
||||||
#### [] Create user documentation
|
#### [x] Create user documentation
|
||||||
|
|
||||||
- []Create `docs/user_guide.md`
|
- [x] Create `docs/user_guide.md` (comprehensive user guide completed)
|
||||||
- []Add installation instructions
|
- [x] Add installation instructions (included in user guide and deployment guide)
|
||||||
- []Include configuration guide
|
- [x] Include configuration guide (detailed configuration section in both guides)
|
||||||
- []Add troubleshooting section
|
- [x] Add troubleshooting section (comprehensive troubleshooting guide included)
|
||||||
|
|
||||||
|
#### [x] Create API reference documentation
|
||||||
|
|
||||||
|
- [x] Created `docs/api_reference.md` with complete API documentation
|
||||||
|
- [x] Documented all REST endpoints with examples
|
||||||
|
- [x] Documented WebSocket endpoints
|
||||||
|
- [x] Included error codes and status codes
|
||||||
|
- [x] Added authentication and authorization details
|
||||||
|
- [x] Included rate limiting and pagination documentation
|
||||||
|
|
||||||
|
#### [x] Create deployment documentation
|
||||||
|
|
||||||
|
- [x] Created `docs/deployment.md` with production deployment guide
|
||||||
|
- [x] Included system requirements
|
||||||
|
- [x] Added pre-deployment checklist
|
||||||
|
- [x] Included production deployment steps
|
||||||
|
- [x] Added Docker deployment instructions
|
||||||
|
- [x] Included Nginx reverse proxy configuration
|
||||||
|
- [x] Added security considerations
|
||||||
|
- [x] Included monitoring and maintenance guidelines
|
||||||
|
|
||||||
## File Size Guidelines
|
## File Size Guidelines
|
||||||
|
|
||||||
|
|||||||
255
src/server/exceptions/__init__.py
Normal file
255
src/server/exceptions/__init__.py
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
"""
|
||||||
|
Custom exception classes for Aniworld API layer.
|
||||||
|
|
||||||
|
This module defines exception hierarchy for the web API with proper
|
||||||
|
HTTP status code mappings and error handling.
|
||||||
|
"""
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class AniWorldAPIException(Exception):
|
||||||
|
"""
|
||||||
|
Base exception for Aniworld API.
|
||||||
|
|
||||||
|
All API-specific exceptions inherit from this class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
status_code: int = 500,
|
||||||
|
error_code: Optional[str] = None,
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize API exception.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Human-readable error message
|
||||||
|
status_code: HTTP status code for response
|
||||||
|
error_code: Machine-readable error identifier
|
||||||
|
details: Additional error details and context
|
||||||
|
"""
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
self.error_code = error_code or self.__class__.__name__
|
||||||
|
self.details = details or {}
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convert exception to dictionary for JSON response.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing error information
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"error": self.error_code,
|
||||||
|
"message": self.message,
|
||||||
|
"details": self.details,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationError(AniWorldAPIException):
|
||||||
|
"""Exception raised when authentication fails."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Authentication failed",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize authentication error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
status_code=401,
|
||||||
|
error_code="AUTHENTICATION_ERROR",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationError(AniWorldAPIException):
|
||||||
|
"""Exception raised when user lacks required permissions."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Insufficient permissions",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize authorization error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
status_code=403,
|
||||||
|
error_code="AUTHORIZATION_ERROR",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(AniWorldAPIException):
|
||||||
|
"""Exception raised when request validation fails."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Request validation failed",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize validation error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
status_code=422,
|
||||||
|
error_code="VALIDATION_ERROR",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(AniWorldAPIException):
|
||||||
|
"""Exception raised when resource is not found."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Resource not found",
|
||||||
|
resource_type: Optional[str] = None,
|
||||||
|
resource_id: Optional[Any] = None,
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize not found error."""
|
||||||
|
if details is None:
|
||||||
|
details = {}
|
||||||
|
if resource_type:
|
||||||
|
details["resource_type"] = resource_type
|
||||||
|
if resource_id:
|
||||||
|
details["resource_id"] = resource_id
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
status_code=404,
|
||||||
|
error_code="NOT_FOUND",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictError(AniWorldAPIException):
|
||||||
|
"""Exception raised when resource conflict occurs."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Resource conflict",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize conflict error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
status_code=409,
|
||||||
|
error_code="CONFLICT",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitError(AniWorldAPIException):
|
||||||
|
"""Exception raised when rate limit is exceeded."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Rate limit exceeded",
|
||||||
|
retry_after: Optional[int] = None,
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize rate limit error."""
|
||||||
|
if details is None:
|
||||||
|
details = {}
|
||||||
|
if retry_after:
|
||||||
|
details["retry_after"] = retry_after
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
status_code=429,
|
||||||
|
error_code="RATE_LIMIT_EXCEEDED",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerError(AniWorldAPIException):
|
||||||
|
"""Exception raised for internal server errors."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Internal server error",
|
||||||
|
error_code: str = "INTERNAL_SERVER_ERROR",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize server error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
status_code=500,
|
||||||
|
error_code=error_code,
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadError(ServerError):
|
||||||
|
"""Exception raised when download operation fails."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Download failed",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize download error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
error_code="DOWNLOAD_ERROR",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationError(ServerError):
|
||||||
|
"""Exception raised when configuration is invalid."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Configuration error",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize configuration error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
error_code="CONFIGURATION_ERROR",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderError(ServerError):
|
||||||
|
"""Exception raised when provider operation fails."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Provider error",
|
||||||
|
provider_name: Optional[str] = None,
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize provider error."""
|
||||||
|
if details is None:
|
||||||
|
details = {}
|
||||||
|
if provider_name:
|
||||||
|
details["provider"] = provider_name
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
error_code="PROVIDER_ERROR",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseError(ServerError):
|
||||||
|
"""Exception raised when database operation fails."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = "Database error",
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""Initialize database error."""
|
||||||
|
super().__init__(
|
||||||
|
message=message,
|
||||||
|
error_code="DATABASE_ERROR",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
35
src/server/exceptions/exceptions.py
Normal file
35
src/server/exceptions/exceptions.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Exceptions module for Aniworld server API.
|
||||||
|
|
||||||
|
This module provides custom exception classes for the web API layer
|
||||||
|
with proper HTTP status code mappings.
|
||||||
|
"""
|
||||||
|
from src.server.exceptions import (
|
||||||
|
AniWorldAPIException,
|
||||||
|
AuthenticationError,
|
||||||
|
AuthorizationError,
|
||||||
|
ConfigurationError,
|
||||||
|
ConflictError,
|
||||||
|
DatabaseError,
|
||||||
|
DownloadError,
|
||||||
|
NotFoundError,
|
||||||
|
ProviderError,
|
||||||
|
RateLimitError,
|
||||||
|
ServerError,
|
||||||
|
ValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AniWorldAPIException",
|
||||||
|
"AuthenticationError",
|
||||||
|
"AuthorizationError",
|
||||||
|
"ValidationError",
|
||||||
|
"NotFoundError",
|
||||||
|
"ConflictError",
|
||||||
|
"RateLimitError",
|
||||||
|
"ServerError",
|
||||||
|
"DownloadError",
|
||||||
|
"ConfigurationError",
|
||||||
|
"ProviderError",
|
||||||
|
"DatabaseError",
|
||||||
|
]
|
||||||
@ -31,6 +31,7 @@ from src.server.controllers.error_controller import (
|
|||||||
from src.server.controllers.health_controller import router as health_router
|
from src.server.controllers.health_controller import router as health_router
|
||||||
from src.server.controllers.page_controller import router as page_router
|
from src.server.controllers.page_controller import router as page_router
|
||||||
from src.server.middleware.auth import AuthMiddleware
|
from src.server.middleware.auth import AuthMiddleware
|
||||||
|
from src.server.middleware.error_handler import register_exception_handlers
|
||||||
from src.server.services.progress_service import get_progress_service
|
from src.server.services.progress_service import get_progress_service
|
||||||
from src.server.services.websocket_service import get_websocket_service
|
from src.server.services.websocket_service import get_websocket_service
|
||||||
|
|
||||||
@ -68,6 +69,9 @@ app.include_router(anime_router)
|
|||||||
app.include_router(download_router)
|
app.include_router(download_router)
|
||||||
app.include_router(websocket_router)
|
app.include_router(websocket_router)
|
||||||
|
|
||||||
|
# Register exception handlers
|
||||||
|
register_exception_handlers(app)
|
||||||
|
|
||||||
# Global variables for application state
|
# Global variables for application state
|
||||||
series_app: Optional[SeriesApp] = None
|
series_app: Optional[SeriesApp] = None
|
||||||
|
|
||||||
|
|||||||
236
src/server/middleware/error_handler.py
Normal file
236
src/server/middleware/error_handler.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
"""
|
||||||
|
Global exception handlers for FastAPI application.
|
||||||
|
|
||||||
|
This module provides centralized error handling that converts custom
|
||||||
|
exceptions to structured JSON responses with appropriate HTTP status codes.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request, status
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from src.server.exceptions import (
|
||||||
|
AniWorldAPIException,
|
||||||
|
AuthenticationError,
|
||||||
|
AuthorizationError,
|
||||||
|
ConflictError,
|
||||||
|
NotFoundError,
|
||||||
|
RateLimitError,
|
||||||
|
ValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_error_response(
|
||||||
|
status_code: int,
|
||||||
|
error: str,
|
||||||
|
message: str,
|
||||||
|
details: Dict[str, Any] | None = None,
|
||||||
|
request_id: str | None = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create standardized error response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status_code: HTTP status code
|
||||||
|
error: Error code/type
|
||||||
|
message: Human-readable error message
|
||||||
|
details: Additional error details
|
||||||
|
request_id: Unique request identifier for tracking
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing structured error response
|
||||||
|
"""
|
||||||
|
response = {
|
||||||
|
"success": False,
|
||||||
|
"error": error,
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if details:
|
||||||
|
response["details"] = details
|
||||||
|
|
||||||
|
if request_id:
|
||||||
|
response["request_id"] = request_id
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def register_exception_handlers(app: FastAPI) -> None:
|
||||||
|
"""
|
||||||
|
Register all exception handlers with FastAPI app.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app.exception_handler(AuthenticationError)
|
||||||
|
async def authentication_error_handler(
|
||||||
|
request: Request, exc: AuthenticationError
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle authentication errors (401)."""
|
||||||
|
logger.warning(
|
||||||
|
f"Authentication error: {exc.message}",
|
||||||
|
extra={"details": exc.details, "path": str(request.url.path)},
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
error=exc.error_code,
|
||||||
|
message=exc.message,
|
||||||
|
details=exc.details,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(AuthorizationError)
|
||||||
|
async def authorization_error_handler(
|
||||||
|
request: Request, exc: AuthorizationError
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle authorization errors (403)."""
|
||||||
|
logger.warning(
|
||||||
|
f"Authorization error: {exc.message}",
|
||||||
|
extra={"details": exc.details, "path": str(request.url.path)},
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
error=exc.error_code,
|
||||||
|
message=exc.message,
|
||||||
|
details=exc.details,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(ValidationError)
|
||||||
|
async def validation_error_handler(
|
||||||
|
request: Request, exc: ValidationError
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle validation errors (422)."""
|
||||||
|
logger.info(
|
||||||
|
f"Validation error: {exc.message}",
|
||||||
|
extra={"details": exc.details, "path": str(request.url.path)},
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
error=exc.error_code,
|
||||||
|
message=exc.message,
|
||||||
|
details=exc.details,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(NotFoundError)
|
||||||
|
async def not_found_error_handler(
|
||||||
|
request: Request, exc: NotFoundError
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle not found errors (404)."""
|
||||||
|
logger.info(
|
||||||
|
f"Not found error: {exc.message}",
|
||||||
|
extra={"details": exc.details, "path": str(request.url.path)},
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
error=exc.error_code,
|
||||||
|
message=exc.message,
|
||||||
|
details=exc.details,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(ConflictError)
|
||||||
|
async def conflict_error_handler(
|
||||||
|
request: Request, exc: ConflictError
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle conflict errors (409)."""
|
||||||
|
logger.info(
|
||||||
|
f"Conflict error: {exc.message}",
|
||||||
|
extra={"details": exc.details, "path": str(request.url.path)},
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
error=exc.error_code,
|
||||||
|
message=exc.message,
|
||||||
|
details=exc.details,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(RateLimitError)
|
||||||
|
async def rate_limit_error_handler(
|
||||||
|
request: Request, exc: RateLimitError
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle rate limit errors (429)."""
|
||||||
|
logger.warning(
|
||||||
|
f"Rate limit exceeded: {exc.message}",
|
||||||
|
extra={"details": exc.details, "path": str(request.url.path)},
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
error=exc.error_code,
|
||||||
|
message=exc.message,
|
||||||
|
details=exc.details,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(AniWorldAPIException)
|
||||||
|
async def api_exception_handler(
|
||||||
|
request: Request, exc: AniWorldAPIException
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle generic API exceptions."""
|
||||||
|
logger.error(
|
||||||
|
f"API error: {exc.message}",
|
||||||
|
extra={
|
||||||
|
"error_code": exc.error_code,
|
||||||
|
"details": exc.details,
|
||||||
|
"path": str(request.url.path),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
error=exc.error_code,
|
||||||
|
message=exc.message,
|
||||||
|
details=exc.details,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def general_exception_handler(
|
||||||
|
request: Request, exc: Exception
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Handle unexpected exceptions."""
|
||||||
|
logger.exception(
|
||||||
|
f"Unexpected error: {str(exc)}",
|
||||||
|
extra={"path": str(request.url.path)},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log full traceback for debugging
|
||||||
|
logger.debug(f"Traceback: {traceback.format_exc()}")
|
||||||
|
|
||||||
|
# Return generic error response for security
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content=create_error_response(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
error="INTERNAL_SERVER_ERROR",
|
||||||
|
message="An unexpected error occurred",
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
),
|
||||||
|
)
|
||||||
@ -13,10 +13,10 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.server.database.models import Download, DownloadStatus
|
from src.server.database.models import DownloadQueueItem, DownloadStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -124,8 +124,8 @@ class AnalyticsService:
|
|||||||
cutoff_date = datetime.now() - timedelta(days=days)
|
cutoff_date = datetime.now() - timedelta(days=days)
|
||||||
|
|
||||||
# Query downloads within period
|
# Query downloads within period
|
||||||
stmt = select(Download).where(
|
stmt = select(DownloadQueueItem).where(
|
||||||
Download.created_at >= cutoff_date
|
DownloadQueueItem.created_at >= cutoff_date
|
||||||
)
|
)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
downloads = result.scalars().all()
|
downloads = result.scalars().all()
|
||||||
@ -138,16 +138,16 @@ class AnalyticsService:
|
|||||||
failed = [d for d in downloads
|
failed = [d for d in downloads
|
||||||
if d.status == DownloadStatus.FAILED]
|
if d.status == DownloadStatus.FAILED]
|
||||||
|
|
||||||
total_bytes = sum(d.size_bytes or 0 for d in successful)
|
total_bytes = sum(d.total_bytes or 0 for d in successful)
|
||||||
total_seconds = sum(
|
avg_speed_list = [
|
||||||
d.duration_seconds or 0 for d in successful
|
d.download_speed or 0.0 for d in successful if d.download_speed
|
||||||
) or 1
|
]
|
||||||
|
avg_speed_mbps = (
|
||||||
avg_speed = (
|
sum(avg_speed_list) / len(avg_speed_list) / (1024 * 1024)
|
||||||
(total_bytes / (1024 * 1024)) / total_seconds
|
if avg_speed_list
|
||||||
if total_seconds > 0
|
|
||||||
else 0.0
|
else 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
success_rate = (
|
success_rate = (
|
||||||
len(successful) / len(downloads) * 100 if downloads else 0.0
|
len(successful) / len(downloads) * 100 if downloads else 0.0
|
||||||
)
|
)
|
||||||
@ -157,11 +157,9 @@ class AnalyticsService:
|
|||||||
successful_downloads=len(successful),
|
successful_downloads=len(successful),
|
||||||
failed_downloads=len(failed),
|
failed_downloads=len(failed),
|
||||||
total_bytes_downloaded=total_bytes,
|
total_bytes_downloaded=total_bytes,
|
||||||
average_speed_mbps=avg_speed,
|
average_speed_mbps=avg_speed_mbps,
|
||||||
success_rate=success_rate,
|
success_rate=success_rate,
|
||||||
average_duration_seconds=total_seconds / len(successful)
|
average_duration_seconds=0.0, # Not available in model
|
||||||
if successful
|
|
||||||
else 0.0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_series_popularity(
|
async def get_series_popularity(
|
||||||
@ -176,39 +174,42 @@ class AnalyticsService:
|
|||||||
Returns:
|
Returns:
|
||||||
List of SeriesPopularity objects
|
List of SeriesPopularity objects
|
||||||
"""
|
"""
|
||||||
stmt = (
|
# Use raw SQL approach since we need to group and join
|
||||||
select(
|
from sqlalchemy import text
|
||||||
Download.series_name,
|
|
||||||
func.count(Download.id).label("download_count"),
|
|
||||||
func.sum(Download.size_bytes).label("total_size"),
|
|
||||||
func.max(Download.created_at).label("last_download"),
|
|
||||||
func.countif(
|
|
||||||
Download.status == DownloadStatus.COMPLETED
|
|
||||||
).label("successful"),
|
|
||||||
)
|
|
||||||
.group_by(Download.series_name)
|
|
||||||
.order_by(func.count(Download.id).desc())
|
|
||||||
.limit(limit)
|
|
||||||
)
|
|
||||||
|
|
||||||
result = await db.execute(stmt)
|
query = text("""
|
||||||
|
SELECT
|
||||||
|
s.title as series_name,
|
||||||
|
COUNT(d.id) as download_count,
|
||||||
|
SUM(d.total_bytes) as total_size,
|
||||||
|
MAX(d.created_at) as last_download,
|
||||||
|
SUM(CASE WHEN d.status = 'COMPLETED'
|
||||||
|
THEN 1 ELSE 0 END) as successful
|
||||||
|
FROM download_queue d
|
||||||
|
JOIN anime_series s ON d.series_id = s.id
|
||||||
|
GROUP BY s.id, s.title
|
||||||
|
ORDER BY download_count DESC
|
||||||
|
LIMIT :limit
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = await db.execute(query, {"limit": limit})
|
||||||
rows = result.all()
|
rows = result.all()
|
||||||
|
|
||||||
popularity = []
|
popularity = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
success_rate = 0.0
|
success_rate = 0.0
|
||||||
if row.download_count > 0:
|
download_count = row[1] or 0
|
||||||
success_rate = (
|
if download_count > 0:
|
||||||
(row.successful or 0) / row.download_count * 100
|
successful = row[4] or 0
|
||||||
)
|
success_rate = (successful / download_count * 100)
|
||||||
|
|
||||||
popularity.append(
|
popularity.append(
|
||||||
SeriesPopularity(
|
SeriesPopularity(
|
||||||
series_name=row.series_name or "Unknown",
|
series_name=row[0] or "Unknown",
|
||||||
download_count=row.download_count or 0,
|
download_count=download_count,
|
||||||
total_size_bytes=row.total_size or 0,
|
total_size_bytes=row[2] or 0,
|
||||||
last_download=row.last_download.isoformat()
|
last_download=row[3].isoformat()
|
||||||
if row.last_download
|
if row[3]
|
||||||
else None,
|
else None,
|
||||||
success_rate=success_rate,
|
success_rate=success_rate,
|
||||||
)
|
)
|
||||||
@ -288,8 +289,8 @@ class AnalyticsService:
|
|||||||
cutoff_time = datetime.now() - timedelta(hours=hours)
|
cutoff_time = datetime.now() - timedelta(hours=hours)
|
||||||
|
|
||||||
# Get download metrics
|
# Get download metrics
|
||||||
stmt = select(Download).where(
|
stmt = select(DownloadQueueItem).where(
|
||||||
Download.created_at >= cutoff_time
|
DownloadQueueItem.created_at >= cutoff_time
|
||||||
)
|
)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
downloads = result.scalars().all()
|
downloads = result.scalars().all()
|
||||||
|
|||||||
227
src/server/utils/error_tracking.py
Normal file
227
src/server/utils/error_tracking.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Error tracking utilities for Aniworld API.
|
||||||
|
|
||||||
|
This module provides error tracking, logging, and reporting functionality
|
||||||
|
for comprehensive error monitoring and debugging.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorTracker:
|
||||||
|
"""
|
||||||
|
Centralized error tracking and management.
|
||||||
|
|
||||||
|
Collects error metadata and provides insights into error patterns.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize error tracker."""
|
||||||
|
self.error_history: list[Dict[str, Any]] = []
|
||||||
|
self.max_history_size = 1000
|
||||||
|
|
||||||
|
def track_error(
|
||||||
|
self,
|
||||||
|
error_type: str,
|
||||||
|
message: str,
|
||||||
|
request_path: str,
|
||||||
|
request_method: str,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
|
status_code: int = 500,
|
||||||
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
request_id: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Track an error occurrence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error_type: Type of error
|
||||||
|
message: Error message
|
||||||
|
request_path: Request path that caused error
|
||||||
|
request_method: HTTP method
|
||||||
|
user_id: User ID if available
|
||||||
|
status_code: HTTP status code
|
||||||
|
details: Additional error details
|
||||||
|
request_id: Request ID for correlation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Unique error tracking ID
|
||||||
|
"""
|
||||||
|
error_id = str(uuid.uuid4())
|
||||||
|
timestamp = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
|
error_entry = {
|
||||||
|
"id": error_id,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"type": error_type,
|
||||||
|
"message": message,
|
||||||
|
"request_path": request_path,
|
||||||
|
"request_method": request_method,
|
||||||
|
"user_id": user_id,
|
||||||
|
"status_code": status_code,
|
||||||
|
"details": details or {},
|
||||||
|
"request_id": request_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.error_history.append(error_entry)
|
||||||
|
|
||||||
|
# Keep history size manageable
|
||||||
|
if len(self.error_history) > self.max_history_size:
|
||||||
|
self.error_history = self.error_history[-self.max_history_size:]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Error tracked: {error_id}",
|
||||||
|
extra={
|
||||||
|
"error_id": error_id,
|
||||||
|
"error_type": error_type,
|
||||||
|
"status_code": status_code,
|
||||||
|
"request_path": request_path,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return error_id
|
||||||
|
|
||||||
|
def get_error_stats(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get error statistics from history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing error statistics
|
||||||
|
"""
|
||||||
|
if not self.error_history:
|
||||||
|
return {"total_errors": 0, "error_types": {}}
|
||||||
|
|
||||||
|
error_types: Dict[str, int] = {}
|
||||||
|
status_codes: Dict[int, int] = {}
|
||||||
|
|
||||||
|
for error in self.error_history:
|
||||||
|
error_type = error["type"]
|
||||||
|
error_types[error_type] = error_types.get(error_type, 0) + 1
|
||||||
|
|
||||||
|
status_code = error["status_code"]
|
||||||
|
status_codes[status_code] = status_codes.get(status_code, 0) + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_errors": len(self.error_history),
|
||||||
|
"error_types": error_types,
|
||||||
|
"status_codes": status_codes,
|
||||||
|
"last_error": (
|
||||||
|
self.error_history[-1] if self.error_history else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_recent_errors(self, limit: int = 10) -> list[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get recent errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Maximum number of errors to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of recent error entries
|
||||||
|
"""
|
||||||
|
return self.error_history[-limit:] if self.error_history else []
|
||||||
|
|
||||||
|
def clear_history(self) -> None:
|
||||||
|
"""Clear error history."""
|
||||||
|
self.error_history.clear()
|
||||||
|
logger.info("Error history cleared")
|
||||||
|
|
||||||
|
|
||||||
|
# Global error tracker instance
|
||||||
|
_error_tracker: Optional[ErrorTracker] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_error_tracker() -> ErrorTracker:
|
||||||
|
"""
|
||||||
|
Get or create global error tracker instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ErrorTracker instance
|
||||||
|
"""
|
||||||
|
global _error_tracker
|
||||||
|
if _error_tracker is None:
|
||||||
|
_error_tracker = ErrorTracker()
|
||||||
|
return _error_tracker
|
||||||
|
|
||||||
|
|
||||||
|
def reset_error_tracker() -> None:
|
||||||
|
"""Reset error tracker for testing."""
|
||||||
|
global _error_tracker
|
||||||
|
_error_tracker = None
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContextManager:
|
||||||
|
"""
|
||||||
|
Manages request context for error tracking.
|
||||||
|
|
||||||
|
Stores request metadata for error correlation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize context manager."""
|
||||||
|
self.context_stack: list[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def push_context(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
request_path: str,
|
||||||
|
request_method: str,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Push request context onto stack.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request_id: Unique request identifier
|
||||||
|
request_path: Request path
|
||||||
|
request_method: HTTP method
|
||||||
|
user_id: User ID if available
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"request_id": request_id,
|
||||||
|
"request_path": request_path,
|
||||||
|
"request_method": request_method,
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
self.context_stack.append(context)
|
||||||
|
|
||||||
|
def pop_context(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Pop request context from stack.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Context dictionary or None if empty
|
||||||
|
"""
|
||||||
|
return self.context_stack.pop() if self.context_stack else None
|
||||||
|
|
||||||
|
def get_current_context(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get current request context.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current context or None if empty
|
||||||
|
"""
|
||||||
|
return self.context_stack[-1] if self.context_stack else None
|
||||||
|
|
||||||
|
|
||||||
|
# Global request context manager
|
||||||
|
_context_manager: Optional[RequestContextManager] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_manager() -> RequestContextManager:
|
||||||
|
"""
|
||||||
|
Get or create global context manager instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RequestContextManager instance
|
||||||
|
"""
|
||||||
|
global _context_manager
|
||||||
|
if _context_manager is None:
|
||||||
|
_context_manager = RequestContextManager()
|
||||||
|
return _context_manager
|
||||||
@ -72,16 +72,16 @@ async def test_get_download_stats_with_data(
|
|||||||
analytics_service, mock_db
|
analytics_service, mock_db
|
||||||
):
|
):
|
||||||
"""Test download statistics with download data."""
|
"""Test download statistics with download data."""
|
||||||
# Mock downloads
|
# Mock downloads - updated to use actual model fields
|
||||||
download1 = MagicMock()
|
download1 = MagicMock()
|
||||||
download1.status = "completed"
|
download1.status = "completed"
|
||||||
download1.size_bytes = 1024 * 1024 * 100 # 100 MB
|
download1.total_bytes = 1024 * 1024 * 100 # 100 MB
|
||||||
download1.duration_seconds = 60
|
download1.download_speed = 1024 * 1024 * 10 # 10 MB/s
|
||||||
|
|
||||||
download2 = MagicMock()
|
download2 = MagicMock()
|
||||||
download2.status = "failed"
|
download2.status = "failed"
|
||||||
download2.size_bytes = 0
|
download2.total_bytes = 0
|
||||||
download2.duration_seconds = 0
|
download2.download_speed = None
|
||||||
|
|
||||||
mock_db.execute = AsyncMock(return_value=MagicMock(
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
||||||
scalars=MagicMock(return_value=MagicMock(all=MagicMock(
|
scalars=MagicMock(return_value=MagicMock(all=MagicMock(
|
||||||
@ -120,12 +120,15 @@ async def test_get_series_popularity_with_data(
|
|||||||
analytics_service, mock_db
|
analytics_service, mock_db
|
||||||
):
|
):
|
||||||
"""Test series popularity with data."""
|
"""Test series popularity with data."""
|
||||||
row = MagicMock()
|
# Mock returns tuples:
|
||||||
row.series_name = "Test Anime"
|
# (series_name, download_count, total_size, last_download, successful)
|
||||||
row.download_count = 5
|
row = (
|
||||||
row.total_size = 1024 * 1024 * 500
|
"Test Anime",
|
||||||
row.last_download = datetime.now()
|
5,
|
||||||
row.successful = 4
|
1024 * 1024 * 500,
|
||||||
|
datetime.now(),
|
||||||
|
4
|
||||||
|
)
|
||||||
|
|
||||||
mock_db.execute = AsyncMock(return_value=MagicMock(
|
mock_db.execute = AsyncMock(return_value=MagicMock(
|
||||||
all=MagicMock(return_value=[row])
|
all=MagicMock(return_value=[row])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user