Compare commits
3 Commits
39991d9ffc
...
95b7059576
| Author | SHA1 | Date | |
|---|---|---|---|
| 95b7059576 | |||
| 66cc2fdfcb | |||
| 1a6c37d264 |
16
data/analytics.json
Normal file
16
data/analytics.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"created_at": "2025-10-27T20:15:18.690820",
|
||||||
|
"last_updated": "2025-10-27T20:15:18.690826",
|
||||||
|
"download_stats": {
|
||||||
|
"total_downloads": 0,
|
||||||
|
"successful_downloads": 0,
|
||||||
|
"failed_downloads": 0,
|
||||||
|
"total_bytes_downloaded": 0,
|
||||||
|
"average_speed_mbps": 0.0,
|
||||||
|
"success_rate": 0.0,
|
||||||
|
"average_duration_seconds": 0.0
|
||||||
|
},
|
||||||
|
"series_popularity": [],
|
||||||
|
"storage_history": [],
|
||||||
|
"performance_samples": []
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"keep_days": 30
|
"keep_days": 30
|
||||||
},
|
},
|
||||||
"other": {
|
"other": {
|
||||||
"master_password_hash": "$pbkdf2-sha256$29000$pRSCMOZcy1mLUeo951zrXQ$8/lWKoHbJJQDk2j7fM9RYrpLyxu3xwJXSpISYfs7jnM",
|
"master_password_hash": "$pbkdf2-sha256$29000$hjDm/H8vRehdCyEkRGitVQ$JJC2Bxw8XeNA0NoG/e4rhw6PjZaN588mJ2SDY3ZPFNY",
|
||||||
"anime_directory": "/home/lukas/Volume/serien/"
|
"anime_directory": "/home/lukas/Volume/serien/"
|
||||||
},
|
},
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
|
|||||||
24
data/config_backups/config_backup_20251027_201521.json
Normal file
24
data/config_backups/config_backup_20251027_201521.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "Aniworld",
|
||||||
|
"data_dir": "data",
|
||||||
|
"scheduler": {
|
||||||
|
"enabled": true,
|
||||||
|
"interval_minutes": 60
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"level": "INFO",
|
||||||
|
"file": null,
|
||||||
|
"max_bytes": null,
|
||||||
|
"backup_count": 3
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"enabled": false,
|
||||||
|
"path": "data/backups",
|
||||||
|
"keep_days": 30
|
||||||
|
},
|
||||||
|
"other": {
|
||||||
|
"master_password_hash": "$pbkdf2-sha256$29000$qRWiNCaEEIKQkhKiFOLcWw$P1QqwKEJHzPszsU/nHmIzdxwbTMIV2iC4tbWUuhqZlo",
|
||||||
|
"anime_directory": "/home/lukas/Volume/serien/"
|
||||||
|
},
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
425
data/download_queue.json
Normal file
425
data/download_queue.json
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
{
|
||||||
|
"pending": [
|
||||||
|
{
|
||||||
|
"id": "47335663-456f-44b6-a176-aa2c2ab74451",
|
||||||
|
"serie_id": "workflow-series",
|
||||||
|
"serie_name": "Workflow Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "high",
|
||||||
|
"added_at": "2025-10-27T19:15:24.278322Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "665e833d-b4b8-4fb2-810f-5a02ed1b3161",
|
||||||
|
"serie_id": "series-2",
|
||||||
|
"serie_name": "Series 2",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.825647Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6d2d59b4-c4a7-4056-a386-d49f709f56ec",
|
||||||
|
"serie_id": "series-1",
|
||||||
|
"serie_name": "Series 1",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.822544Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eb43e2ce-b782-473f-aa5e-b29e07531034",
|
||||||
|
"serie_id": "series-0",
|
||||||
|
"serie_name": "Series 0",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.817448Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f942fc20-2eb3-44fc-b2e1-5634d3749856",
|
||||||
|
"serie_id": "series-high",
|
||||||
|
"serie_name": "Series High",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "high",
|
||||||
|
"added_at": "2025-10-27T19:15:23.494450Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d91b4625-af9f-4f84-a223-a3a68a743a6f",
|
||||||
|
"serie_id": "test-series-2",
|
||||||
|
"serie_name": "Another Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "high",
|
||||||
|
"added_at": "2025-10-27T19:15:23.458331Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "020aa6c4-b969-4290-a9f3-3951a0ebf218",
|
||||||
|
"serie_id": "test-series-1",
|
||||||
|
"serie_name": "Test Anime Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": "Episode 1"
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.424005Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "67a98da0-544d-46c6-865c-0eea068ee47d",
|
||||||
|
"serie_id": "test-series-1",
|
||||||
|
"serie_name": "Test Anime Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 2,
|
||||||
|
"title": "Episode 2"
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.424103Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bb811506-a40f-45e0-a517-9d12afa33759",
|
||||||
|
"serie_id": "series-normal",
|
||||||
|
"serie_name": "Series Normal",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.496680Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2f8e6e85-7a1c-4d9b-aeaf-f4c9da6de8da",
|
||||||
|
"serie_id": "series-low",
|
||||||
|
"serie_name": "Series Low",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "low",
|
||||||
|
"added_at": "2025-10-27T19:15:23.498731Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "885b8873-8a97-439d-b2f3-93d50828baad",
|
||||||
|
"serie_id": "test-series",
|
||||||
|
"serie_name": "Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.746489Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "15711557-66d2-4b7c-90f5-17600dfb0e40",
|
||||||
|
"serie_id": "test-series",
|
||||||
|
"serie_name": "Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.860548Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e3b0ade0-b4bb-414e-a65d-9593dd3b27b9",
|
||||||
|
"serie_id": "invalid-series",
|
||||||
|
"serie_name": "Invalid Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 99,
|
||||||
|
"episode": 99,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.938644Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "41f5ce9e-f20c-4ad6-b074-ff06787463d5",
|
||||||
|
"serie_id": "test-series",
|
||||||
|
"serie_name": "Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:23.973361Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3c84fcc6-3aa4-4531-bcc8-296c7eb36430",
|
||||||
|
"serie_id": "series-4",
|
||||||
|
"serie_name": "Series 4",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.075622Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "650324c2-7028-46fb-bceb-9ed756f514c8",
|
||||||
|
"serie_id": "series-3",
|
||||||
|
"serie_name": "Series 3",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.076679Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8782d952-25c3-4907-85eb-205c216f0b35",
|
||||||
|
"serie_id": "series-2",
|
||||||
|
"serie_name": "Series 2",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.077499Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ba2e0be5-3d11-47df-892b-7df465824419",
|
||||||
|
"serie_id": "series-1",
|
||||||
|
"serie_name": "Series 1",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.078333Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7a64b375-aaad-494d-bcd1-1f2ae5c421f4",
|
||||||
|
"serie_id": "series-0",
|
||||||
|
"serie_name": "Series 0",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.079175Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c532886f-6dc2-45fa-92dd-3d46ef62a692",
|
||||||
|
"serie_id": "persistent-series",
|
||||||
|
"serie_name": "Persistent Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.173243Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0e6d4e1e-7714-4fb1-9ad1-3458c9c6d4e6",
|
||||||
|
"serie_id": "ws-series",
|
||||||
|
"serie_name": "WebSocket Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.241585Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f10196c8-f093-4a15-a498-72c3bfe6f735",
|
||||||
|
"serie_id": "pause-test",
|
||||||
|
"serie_name": "Pause Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-27T19:15:24.426637Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"active": [],
|
||||||
|
"failed": [],
|
||||||
|
"timestamp": "2025-10-27T19:15:24.426898+00:00"
|
||||||
|
}
|
||||||
@ -287,7 +287,15 @@ async def trigger_rescan(
|
|||||||
if hasattr(series_app, "ReScan"):
|
if hasattr(series_app, "ReScan"):
|
||||||
result = series_app.ReScan(lambda *args, **kwargs: None)
|
result = series_app.ReScan(lambda *args, **kwargs: None)
|
||||||
|
|
||||||
if result.success:
|
# Handle cases where ReScan might not return anything
|
||||||
|
if result is None:
|
||||||
|
# If no result, assume success
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Rescan completed successfully",
|
||||||
|
"series_count": 0
|
||||||
|
}
|
||||||
|
elif hasattr(result, 'success') and result.success:
|
||||||
series_count = (
|
series_count = (
|
||||||
result.data.get("series_count", 0)
|
result.data.get("series_count", 0)
|
||||||
if result.data else 0
|
if result.data else 0
|
||||||
@ -297,11 +305,18 @@ async def trigger_rescan(
|
|||||||
"message": result.message,
|
"message": result.message,
|
||||||
"series_count": series_count
|
"series_count": series_count
|
||||||
}
|
}
|
||||||
else:
|
elif hasattr(result, 'success'):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": result.message
|
"message": result.message
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
# Result exists but has no success attribute
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Rescan completed",
|
||||||
|
"series_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
@ -427,18 +442,27 @@ async def search_anime(
|
|||||||
if isinstance(match, dict):
|
if isinstance(match, dict):
|
||||||
identifier = match.get("key") or match.get("id") or ""
|
identifier = match.get("key") or match.get("id") or ""
|
||||||
title = match.get("title") or match.get("name") or ""
|
title = match.get("title") or match.get("name") or ""
|
||||||
missing = match.get("missing")
|
site = match.get("site") or ""
|
||||||
missing_episodes = int(missing) if missing is not None else 0
|
folder = match.get("folder") or ""
|
||||||
|
missing = (
|
||||||
|
match.get("missing_episodes")
|
||||||
|
or match.get("missing")
|
||||||
|
or {}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
identifier = getattr(match, "key", getattr(match, "id", ""))
|
identifier = getattr(match, "key", getattr(match, "id", ""))
|
||||||
title = getattr(match, "title", getattr(match, "name", ""))
|
title = getattr(match, "title", getattr(match, "name", ""))
|
||||||
missing_episodes = int(getattr(match, "missing", 0))
|
site = getattr(match, "site", "")
|
||||||
|
folder = getattr(match, "folder", "")
|
||||||
|
missing = getattr(match, "missing_episodes", {})
|
||||||
|
|
||||||
summaries.append(
|
summaries.append(
|
||||||
AnimeSummary(
|
AnimeSummary(
|
||||||
id=identifier,
|
key=identifier,
|
||||||
title=title,
|
name=title,
|
||||||
missing_episodes=missing_episodes,
|
site=site,
|
||||||
|
folder=folder,
|
||||||
|
missing_episodes=missing,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,9 @@ class NetworkDiagnostics(BaseModel):
|
|||||||
..., description="Overall internet connectivity status"
|
..., description="Overall internet connectivity status"
|
||||||
)
|
)
|
||||||
dns_working: bool = Field(..., description="DNS resolution status")
|
dns_working: bool = Field(..., description="DNS resolution status")
|
||||||
|
aniworld_reachable: bool = Field(
|
||||||
|
..., description="Aniworld.to connectivity status"
|
||||||
|
)
|
||||||
tests: List[NetworkTestResult] = Field(
|
tests: List[NetworkTestResult] = Field(
|
||||||
..., description="Individual network tests"
|
..., description="Individual network tests"
|
||||||
)
|
)
|
||||||
@ -53,7 +56,7 @@ async def check_dns() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def test_host_connectivity(
|
async def check_host_connectivity(
|
||||||
host: str, port: int = 80, timeout: float = 5.0
|
host: str, port: int = 80, timeout: float = 5.0
|
||||||
) -> NetworkTestResult:
|
) -> NetworkTestResult:
|
||||||
"""Test connectivity to a specific host.
|
"""Test connectivity to a specific host.
|
||||||
@ -109,19 +112,20 @@ async def test_host_connectivity(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/network", response_model=NetworkDiagnostics)
|
@router.get("/network")
|
||||||
async def network_diagnostics(
|
async def network_diagnostics(
|
||||||
auth: Optional[dict] = Depends(require_auth),
|
auth: Optional[dict] = Depends(require_auth),
|
||||||
) -> NetworkDiagnostics:
|
) -> Dict:
|
||||||
"""Run network connectivity diagnostics.
|
"""Run network connectivity diagnostics.
|
||||||
|
|
||||||
Tests DNS resolution and connectivity to common services.
|
Tests DNS resolution and connectivity to common services including
|
||||||
|
aniworld.to.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
auth: Authentication token (optional)
|
auth: Authentication token (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
NetworkDiagnostics with test results
|
Dict with status and diagnostics data
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: If diagnostics fail
|
HTTPException: If diagnostics fail
|
||||||
@ -132,33 +136,52 @@ async def network_diagnostics(
|
|||||||
# Check DNS
|
# Check DNS
|
||||||
dns_working = await check_dns()
|
dns_working = await check_dns()
|
||||||
|
|
||||||
# Test connectivity to various hosts
|
# Test connectivity to various hosts including aniworld.to
|
||||||
test_hosts = [
|
test_hosts = [
|
||||||
("google.com", 80),
|
("google.com", 80),
|
||||||
("cloudflare.com", 80),
|
("cloudflare.com", 80),
|
||||||
("github.com", 443),
|
("github.com", 443),
|
||||||
|
("aniworld.to", 443),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Run all tests concurrently
|
# Run all tests concurrently
|
||||||
test_tasks = [
|
test_tasks = [
|
||||||
test_host_connectivity(host, port) for host, port in test_hosts
|
check_host_connectivity(host, port) for host, port in test_hosts
|
||||||
]
|
]
|
||||||
test_results = await asyncio.gather(*test_tasks)
|
test_results = await asyncio.gather(*test_tasks)
|
||||||
|
|
||||||
# Determine overall internet connectivity
|
# Determine overall internet connectivity
|
||||||
internet_connected = any(result.reachable for result in test_results)
|
internet_connected = any(result.reachable for result in test_results)
|
||||||
|
|
||||||
logger.info(
|
# Check if aniworld.to is reachable
|
||||||
f"Network diagnostics complete: "
|
aniworld_result = next(
|
||||||
f"DNS={dns_working}, Internet={internet_connected}"
|
(r for r in test_results if r.host == "aniworld.to"),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
aniworld_reachable = (
|
||||||
|
aniworld_result.reachable if aniworld_result else False
|
||||||
)
|
)
|
||||||
|
|
||||||
return NetworkDiagnostics(
|
logger.info(
|
||||||
|
f"Network diagnostics complete: "
|
||||||
|
f"DNS={dns_working}, Internet={internet_connected}, "
|
||||||
|
f"Aniworld={aniworld_reachable}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create diagnostics data
|
||||||
|
diagnostics_data = NetworkDiagnostics(
|
||||||
internet_connected=internet_connected,
|
internet_connected=internet_connected,
|
||||||
dns_working=dns_working,
|
dns_working=dns_working,
|
||||||
|
aniworld_reachable=aniworld_reachable,
|
||||||
tests=test_results,
|
tests=test_results,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return in standard format expected by frontend
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": diagnostics_data.model_dump(),
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Failed to run network diagnostics")
|
logger.exception("Failed to run network diagnostics")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@ -1551,8 +1551,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-custom {
|
.checkbox-custom {
|
||||||
|
display: inline-block;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
min-width: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
border: 2px solid var(--color-border);
|
border: 2px solid var(--color-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
|
|||||||
@ -77,7 +77,7 @@ def test_list_anime_direct_call():
|
|||||||
fake = FakeSeriesApp()
|
fake = FakeSeriesApp()
|
||||||
result = asyncio.run(anime_module.list_anime(series_app=fake))
|
result = asyncio.run(anime_module.list_anime(series_app=fake))
|
||||||
assert isinstance(result, list)
|
assert isinstance(result, list)
|
||||||
assert any(item.title == "Test Show" for item in result)
|
assert any(item.name == "Test Show" for item in result)
|
||||||
|
|
||||||
|
|
||||||
def test_get_anime_detail_direct_call():
|
def test_get_anime_detail_direct_call():
|
||||||
|
|||||||
227
tests/unit/test_diagnostics.py
Normal file
227
tests/unit/test_diagnostics.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
"""Unit tests for diagnostics endpoints."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.server.api.diagnostics import (
|
||||||
|
NetworkTestResult,
|
||||||
|
check_dns,
|
||||||
|
check_host_connectivity,
|
||||||
|
network_diagnostics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDiagnosticsEndpoint:
|
||||||
|
"""Test diagnostics API endpoints."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_network_diagnostics_returns_standard_format(self):
|
||||||
|
"""Test that network diagnostics returns the expected format."""
|
||||||
|
# Mock authentication
|
||||||
|
mock_auth = {"user_id": "test_user"}
|
||||||
|
|
||||||
|
# Mock the helper functions
|
||||||
|
with patch(
|
||||||
|
"src.server.api.diagnostics.check_dns",
|
||||||
|
return_value=True
|
||||||
|
), patch(
|
||||||
|
"src.server.api.diagnostics.check_host_connectivity",
|
||||||
|
side_effect=[
|
||||||
|
NetworkTestResult(
|
||||||
|
host="google.com",
|
||||||
|
reachable=True,
|
||||||
|
response_time_ms=50.5
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="cloudflare.com",
|
||||||
|
reachable=True,
|
||||||
|
response_time_ms=30.2
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="github.com",
|
||||||
|
reachable=True,
|
||||||
|
response_time_ms=100.0
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="aniworld.to",
|
||||||
|
reachable=True,
|
||||||
|
response_time_ms=75.3
|
||||||
|
),
|
||||||
|
]
|
||||||
|
):
|
||||||
|
# Call the endpoint
|
||||||
|
result = await network_diagnostics(auth=mock_auth)
|
||||||
|
|
||||||
|
# Verify response structure
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert "status" in result
|
||||||
|
assert "data" in result
|
||||||
|
assert result["status"] == "success"
|
||||||
|
|
||||||
|
# Verify data structure
|
||||||
|
data = result["data"]
|
||||||
|
assert "internet_connected" in data
|
||||||
|
assert "dns_working" in data
|
||||||
|
assert "aniworld_reachable" in data
|
||||||
|
assert "tests" in data
|
||||||
|
|
||||||
|
# Verify values
|
||||||
|
assert data["internet_connected"] is True
|
||||||
|
assert data["dns_working"] is True
|
||||||
|
assert data["aniworld_reachable"] is True
|
||||||
|
assert len(data["tests"]) == 4
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_network_diagnostics_aniworld_unreachable(self):
|
||||||
|
"""Test diagnostics when aniworld.to is unreachable."""
|
||||||
|
mock_auth = {"user_id": "test_user"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.server.api.diagnostics.check_dns",
|
||||||
|
return_value=True
|
||||||
|
), patch(
|
||||||
|
"src.server.api.diagnostics.check_host_connectivity",
|
||||||
|
side_effect=[
|
||||||
|
NetworkTestResult(
|
||||||
|
host="google.com",
|
||||||
|
reachable=True,
|
||||||
|
response_time_ms=50.5
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="cloudflare.com",
|
||||||
|
reachable=True,
|
||||||
|
response_time_ms=30.2
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="github.com",
|
||||||
|
reachable=True,
|
||||||
|
response_time_ms=100.0
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="aniworld.to",
|
||||||
|
reachable=False,
|
||||||
|
error="Connection timeout"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
):
|
||||||
|
result = await network_diagnostics(auth=mock_auth)
|
||||||
|
|
||||||
|
# Verify aniworld is marked as unreachable
|
||||||
|
assert result["status"] == "success"
|
||||||
|
assert result["data"]["aniworld_reachable"] is False
|
||||||
|
assert result["data"]["internet_connected"] is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_network_diagnostics_all_unreachable(self):
|
||||||
|
"""Test diagnostics when all hosts are unreachable."""
|
||||||
|
mock_auth = {"user_id": "test_user"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.server.api.diagnostics.check_dns",
|
||||||
|
return_value=False
|
||||||
|
), patch(
|
||||||
|
"src.server.api.diagnostics.check_host_connectivity",
|
||||||
|
side_effect=[
|
||||||
|
NetworkTestResult(
|
||||||
|
host="google.com",
|
||||||
|
reachable=False,
|
||||||
|
error="Connection timeout"
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="cloudflare.com",
|
||||||
|
reachable=False,
|
||||||
|
error="Connection timeout"
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="github.com",
|
||||||
|
reachable=False,
|
||||||
|
error="Connection timeout"
|
||||||
|
),
|
||||||
|
NetworkTestResult(
|
||||||
|
host="aniworld.to",
|
||||||
|
reachable=False,
|
||||||
|
error="Connection timeout"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
):
|
||||||
|
result = await network_diagnostics(auth=mock_auth)
|
||||||
|
|
||||||
|
# Verify all are unreachable
|
||||||
|
assert result["status"] == "success"
|
||||||
|
assert result["data"]["internet_connected"] is False
|
||||||
|
assert result["data"]["dns_working"] is False
|
||||||
|
assert result["data"]["aniworld_reachable"] is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetworkHelpers:
|
||||||
|
"""Test network helper functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_dns_success(self):
|
||||||
|
"""Test DNS check when DNS is working."""
|
||||||
|
with patch("socket.gethostbyname", return_value="142.250.185.78"):
|
||||||
|
result = await check_dns()
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_dns_failure(self):
|
||||||
|
"""Test DNS check when DNS fails."""
|
||||||
|
import socket
|
||||||
|
with patch(
|
||||||
|
"socket.gethostbyname",
|
||||||
|
side_effect=socket.gaierror("DNS lookup failed")
|
||||||
|
):
|
||||||
|
result = await check_dns()
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_host_connectivity_success(self):
|
||||||
|
"""Test host connectivity check when host is reachable."""
|
||||||
|
with patch(
|
||||||
|
"socket.create_connection",
|
||||||
|
return_value=MagicMock()
|
||||||
|
):
|
||||||
|
result = await check_host_connectivity("google.com", 80)
|
||||||
|
assert result.host == "google.com"
|
||||||
|
assert result.reachable is True
|
||||||
|
assert result.response_time_ms is not None
|
||||||
|
assert result.response_time_ms >= 0
|
||||||
|
assert result.error is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_host_connectivity_timeout(self):
|
||||||
|
"""Test host connectivity when connection times out."""
|
||||||
|
import asyncio
|
||||||
|
with patch(
|
||||||
|
"socket.create_connection",
|
||||||
|
side_effect=asyncio.TimeoutError()
|
||||||
|
):
|
||||||
|
result = await check_host_connectivity("example.com", 80, 1.0)
|
||||||
|
assert result.host == "example.com"
|
||||||
|
assert result.reachable is False
|
||||||
|
assert result.error == "Connection timeout"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_host_connectivity_dns_failure(self):
|
||||||
|
"""Test host connectivity when DNS resolution fails."""
|
||||||
|
import socket
|
||||||
|
with patch(
|
||||||
|
"socket.create_connection",
|
||||||
|
side_effect=socket.gaierror("Name resolution failed")
|
||||||
|
):
|
||||||
|
result = await check_host_connectivity("invalid.host", 80)
|
||||||
|
assert result.host == "invalid.host"
|
||||||
|
assert result.reachable is False
|
||||||
|
assert "DNS resolution failed" in result.error
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_host_connectivity_connection_refused(self):
|
||||||
|
"""Test host connectivity when connection is refused."""
|
||||||
|
with patch(
|
||||||
|
"socket.create_connection",
|
||||||
|
side_effect=ConnectionRefusedError()
|
||||||
|
):
|
||||||
|
result = await check_host_connectivity("localhost", 12345)
|
||||||
|
assert result.host == "localhost"
|
||||||
|
assert result.reachable is False
|
||||||
|
assert result.error == "Connection refused"
|
||||||
Loading…
x
Reference in New Issue
Block a user