Compare commits
No commits in common. "39991d9ffc6a2a45c5c774f37c04ed2dc21292c8" and "a3651e0e475f1dbad601cdd829cefc09ba95179d" have entirely different histories.
39991d9ffc
...
a3651e0e47
16
data/analytics.json
Normal file
16
data/analytics.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"created_at": "2025-10-23T20:54:38.147564",
|
||||||
|
"last_updated": "2025-10-23T20:54:38.147574",
|
||||||
|
"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,8 +17,8 @@
|
|||||||
"keep_days": 30
|
"keep_days": 30
|
||||||
},
|
},
|
||||||
"other": {
|
"other": {
|
||||||
"master_password_hash": "$pbkdf2-sha256$29000$pRSCMOZcy1mLUeo951zrXQ$8/lWKoHbJJQDk2j7fM9RYrpLyxu3xwJXSpISYfs7jnM",
|
"anime_directory": "/home/lukas/Volume/serien/",
|
||||||
"anime_directory": "/home/lukas/Volume/serien/"
|
"master_password_hash": "$pbkdf2-sha256$29000$ZWwtJaQ0ZkxpLUWolRJijA$QcfgTBqgM3ABu9N93/w8naBLdfCKmKFc65Cn/f4fP84"
|
||||||
},
|
},
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
425
data/download_queue.json
Normal file
425
data/download_queue.json
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
{
|
||||||
|
"pending": [
|
||||||
|
{
|
||||||
|
"id": "7cc643ca-0b4e-4769-8d25-c99ce539b434",
|
||||||
|
"serie_id": "workflow-series",
|
||||||
|
"serie_name": "Workflow Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "high",
|
||||||
|
"added_at": "2025-10-24T17:23:26.098284Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6a017a0d-78e2-4123-9715-80b540e03c41",
|
||||||
|
"serie_id": "series-2",
|
||||||
|
"serie_name": "Series 2",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.819219Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e31ecefa-470a-4ea6-aaa0-c16d38d5ab8b",
|
||||||
|
"serie_id": "series-1",
|
||||||
|
"serie_name": "Series 1",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.816100Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e3b9418c-7b1e-47dc-928c-3746059a0fa8",
|
||||||
|
"serie_id": "series-0",
|
||||||
|
"serie_name": "Series 0",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.812680Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "77083b3b-8b7b-4e02-a4c9-0e95652b1865",
|
||||||
|
"serie_id": "series-high",
|
||||||
|
"serie_name": "Series High",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "high",
|
||||||
|
"added_at": "2025-10-24T17:23:25.591277Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "03fa75a1-0641-41e8-be69-c274383d6198",
|
||||||
|
"serie_id": "test-series-2",
|
||||||
|
"serie_name": "Another Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "high",
|
||||||
|
"added_at": "2025-10-24T17:23:25.567577Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bbfa8dd3-0f28-43f3-9f42-03595684e873",
|
||||||
|
"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-24T17:23:25.543811Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4d462a39-e705-4dd4-a968-e6d995471615",
|
||||||
|
"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-24T17:23:25.543911Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "04e5ce5d-ce4c-4776-a1be-b0c78c17d651",
|
||||||
|
"serie_id": "series-normal",
|
||||||
|
"serie_name": "Series Normal",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.593205Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8a8da509-9bec-4979-aa01-22f726e298ef",
|
||||||
|
"serie_id": "series-low",
|
||||||
|
"serie_name": "Series Low",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "low",
|
||||||
|
"added_at": "2025-10-24T17:23:25.595371Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b07b9e02-3517-4066-aba0-2ee6b2349580",
|
||||||
|
"serie_id": "test-series",
|
||||||
|
"serie_name": "Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.760199Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9577295e-7ac6-4786-8601-ac13267aba9f",
|
||||||
|
"serie_id": "test-series",
|
||||||
|
"serie_name": "Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.850731Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "562ce52c-2979-4107-b630-999ff6c095e9",
|
||||||
|
"serie_id": "invalid-series",
|
||||||
|
"serie_name": "Invalid Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 99,
|
||||||
|
"episode": 99,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.902493Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1684fe7f-5755-4064-86ed-a78831e8dc0f",
|
||||||
|
"serie_id": "test-series",
|
||||||
|
"serie_name": "Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.926933Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c4fe86cb-e6f7-4303-a8b6-2e76c51d7c40",
|
||||||
|
"serie_id": "series-4",
|
||||||
|
"serie_name": "Series 4",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.965540Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "94d7d85c-911e-495b-9203-065324594c74",
|
||||||
|
"serie_id": "series-0",
|
||||||
|
"serie_name": "Series 0",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.966417Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1d8e1cda-ff78-4ab8-a040-2f325d53666a",
|
||||||
|
"serie_id": "series-3",
|
||||||
|
"serie_name": "Series 3",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.967083Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f9b4174e-f809-4272-bcd8-f9bd44238d3c",
|
||||||
|
"serie_id": "series-2",
|
||||||
|
"serie_name": "Series 2",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.967759Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b41f4c2a-40d6-4205-b769-c3a77df8df5e",
|
||||||
|
"serie_id": "series-1",
|
||||||
|
"serie_name": "Series 1",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:25.968503Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ae4e67dd-b77f-4fbe-8d4c-19fe979f6783",
|
||||||
|
"serie_id": "persistent-series",
|
||||||
|
"serie_name": "Persistent Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:26.027365Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5dc0b529-627c-47ed-8f2a-55112d78de93",
|
||||||
|
"serie_id": "ws-series",
|
||||||
|
"serie_name": "WebSocket Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:26.073822Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "44f479fd-61f7-4279-ace1-5fbf31dad243",
|
||||||
|
"serie_id": "pause-test",
|
||||||
|
"serie_name": "Pause Test Series",
|
||||||
|
"episode": {
|
||||||
|
"season": 1,
|
||||||
|
"episode": 1,
|
||||||
|
"title": null
|
||||||
|
},
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "normal",
|
||||||
|
"added_at": "2025-10-24T17:23:26.227077Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": null,
|
||||||
|
"progress": null,
|
||||||
|
"error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"source_url": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"active": [],
|
||||||
|
"failed": [],
|
||||||
|
"timestamp": "2025-10-24T17:23:26.227320+00:00"
|
||||||
|
}
|
||||||
155
docs/logging.md
155
docs/logging.md
@ -1,155 +0,0 @@
|
|||||||
# Logging Configuration
|
|
||||||
|
|
||||||
This document describes the logging setup for the Aniworld FastAPI application.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The application uses Python's built-in `logging` module with both console and file output. All logs are written to:
|
|
||||||
|
|
||||||
- **Console**: Colored output for development
|
|
||||||
- **Log File**: `logs/fastapi_app.log` with detailed timestamps
|
|
||||||
|
|
||||||
## Log Levels
|
|
||||||
|
|
||||||
By default, the application logs at `INFO` level. You can change this by setting the `LOG_LEVEL` environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export LOG_LEVEL=DEBUG # More verbose
|
|
||||||
export LOG_LEVEL=INFO # Default
|
|
||||||
export LOG_LEVEL=WARNING # Less verbose
|
|
||||||
export LOG_LEVEL=ERROR # Errors only
|
|
||||||
```
|
|
||||||
|
|
||||||
Or in your `.env` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the Server
|
|
||||||
|
|
||||||
### Option 1: Using the run_server.py script (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
conda run -n AniWorld python run_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
This script uses the custom uvicorn logging configuration that ensures proper console and file logging.
|
|
||||||
|
|
||||||
### Option 2: Using the shell script
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./start_server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 3: Using uvicorn directly
|
|
||||||
|
|
||||||
```bash
|
|
||||||
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: When using `conda run`, console output may not be visible in real-time. The logs will still be written to the file.
|
|
||||||
|
|
||||||
## Log File Location
|
|
||||||
|
|
||||||
All logs are written to: `logs/fastapi_app.log`
|
|
||||||
|
|
||||||
To view logs in real-time:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f logs/fastapi_app.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## Log Format
|
|
||||||
|
|
||||||
### Console Output
|
|
||||||
|
|
||||||
```
|
|
||||||
INFO: Starting FastAPI application...
|
|
||||||
INFO: Server running on http://127.0.0.1:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### File Output
|
|
||||||
|
|
||||||
```
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Starting FastAPI application...
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Server running on http://127.0.0.1:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
## What Gets Logged
|
|
||||||
|
|
||||||
The application logs:
|
|
||||||
|
|
||||||
- **Startup/Shutdown**: Application lifecycle events
|
|
||||||
- **Configuration**: Loaded settings and configuration
|
|
||||||
- **HTTP Requests**: Via uvicorn.access logger
|
|
||||||
- **Errors**: Exception tracebacks with full context
|
|
||||||
- **WebSocket Events**: Connection/disconnection events
|
|
||||||
- **Download Progress**: Progress updates for anime downloads
|
|
||||||
- **File Operations**: File creation, deletion, scanning
|
|
||||||
|
|
||||||
## Logger Names
|
|
||||||
|
|
||||||
Different parts of the application use different logger names:
|
|
||||||
|
|
||||||
- `aniworld`: Main application logger
|
|
||||||
- `uvicorn.error`: Uvicorn server errors
|
|
||||||
- `uvicorn.access`: HTTP request logs
|
|
||||||
- `src.core.SeriesApp`: Core anime logic
|
|
||||||
- `src.core.SerieScanner`: File scanning operations
|
|
||||||
- `src.server.*`: Web API endpoints and services
|
|
||||||
|
|
||||||
## Programmatic Usage
|
|
||||||
|
|
||||||
To use logging in your code:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from src.infrastructure.logging import get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
logger.info("This is an info message")
|
|
||||||
logger.warning("This is a warning")
|
|
||||||
logger.error("This is an error", exc_info=True) # Includes traceback
|
|
||||||
```
|
|
||||||
|
|
||||||
## Log Rotation
|
|
||||||
|
|
||||||
Log files can grow large over time. Consider implementing log rotation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Archive old logs
|
|
||||||
mkdir -p logs/archived
|
|
||||||
mv logs/fastapi_app.log logs/archived/fastapi_app_$(date +%Y%m%d_%H%M%S).log
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use Python's `RotatingFileHandler` (can be added to `src/infrastructure/logging/logger.py`).
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### No console output when using `conda run`
|
|
||||||
|
|
||||||
This is a known limitation of `conda run`. The logs are still being written to the file. To see console output:
|
|
||||||
|
|
||||||
1. Use the log file: `tail -f logs/fastapi_app.log`
|
|
||||||
2. Or run without conda: `python run_server.py` (after activating environment with `conda activate AniWorld`)
|
|
||||||
|
|
||||||
### Log file not created
|
|
||||||
|
|
||||||
- Check that the `logs/` directory exists (it's created automatically)
|
|
||||||
- Verify write permissions on the `logs/` directory
|
|
||||||
- Check the `LOG_LEVEL` environment variable
|
|
||||||
|
|
||||||
### Too much logging
|
|
||||||
|
|
||||||
Set a higher log level:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export LOG_LEVEL=WARNING
|
|
||||||
```
|
|
||||||
|
|
||||||
### Missing logs
|
|
||||||
|
|
||||||
- Check that you're using the logger, not `print()`
|
|
||||||
- Verify the log level is appropriate for your messages
|
|
||||||
- Ensure the logger is properly configured (should happen automatically on startup)
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
# Logging Implementation Summary
|
|
||||||
|
|
||||||
## What Was Implemented
|
|
||||||
|
|
||||||
### 1. Core Logging Infrastructure (`src/infrastructure/logging/`)
|
|
||||||
|
|
||||||
- **`logger.py`**: Main logging configuration module
|
|
||||||
|
|
||||||
- `setup_logging()`: Configures both console and file handlers
|
|
||||||
- `get_logger()`: Retrieves logger instances for specific modules
|
|
||||||
- Follows Python logging best practices with proper formatters
|
|
||||||
|
|
||||||
- **`uvicorn_config.py`**: Uvicorn-specific logging configuration
|
|
||||||
|
|
||||||
- Custom logging configuration dictionary for uvicorn
|
|
||||||
- Ensures uvicorn logs are captured in both console and file
|
|
||||||
- Configures multiple loggers (uvicorn, uvicorn.error, uvicorn.access, aniworld)
|
|
||||||
|
|
||||||
- **`__init__.py`**: Package initialization
|
|
||||||
- Exports public API: `setup_logging`, `get_logger`, `get_uvicorn_log_config`
|
|
||||||
|
|
||||||
### 2. FastAPI Integration
|
|
||||||
|
|
||||||
Updated `src/server/fastapi_app.py` to:
|
|
||||||
|
|
||||||
- Import and use the logging infrastructure
|
|
||||||
- Call `setup_logging()` during application startup (in `lifespan()`)
|
|
||||||
- Replace all `print()` statements with proper logger calls
|
|
||||||
- Use lazy formatting (`logger.info("Message: %s", value)`)
|
|
||||||
|
|
||||||
### 3. Startup Scripts
|
|
||||||
|
|
||||||
- **`run_server.py`**: Python startup script
|
|
||||||
|
|
||||||
- Uses the custom uvicorn logging configuration
|
|
||||||
- Recommended way to start the server
|
|
||||||
|
|
||||||
- **`start_server.sh`**: Bash startup script
|
|
||||||
- Wrapper around `run_server.py`
|
|
||||||
- Made executable with proper shebang
|
|
||||||
|
|
||||||
### 4. Documentation
|
|
||||||
|
|
||||||
- **`docs/logging.md`**: Comprehensive logging guide
|
|
||||||
- How to run the server
|
|
||||||
- Log file locations
|
|
||||||
- Log format examples
|
|
||||||
- Troubleshooting guide
|
|
||||||
- Programmatic usage examples
|
|
||||||
|
|
||||||
## Log Outputs
|
|
||||||
|
|
||||||
### Console Output
|
|
||||||
|
|
||||||
```
|
|
||||||
INFO: Starting FastAPI application...
|
|
||||||
INFO: Loaded anime_directory from config: /home/lukas/Volume/serien/
|
|
||||||
INFO: Server running on http://127.0.0.1:8000
|
|
||||||
INFO: API documentation available at http://127.0.0.1:8000/api/docs
|
|
||||||
```
|
|
||||||
|
|
||||||
### File Output (`logs/fastapi_app.log`)
|
|
||||||
|
|
||||||
```
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - ============================================================
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Logging configured successfully
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Log level: INFO
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Log file: /home/lukas/Volume/repo/Aniworld/logs/fastapi_app.log
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - ============================================================
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Starting FastAPI application...
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Loaded anime_directory from config: /home/lukas/Volume/serien/
|
|
||||||
2025-10-25 17:31:19 - src.core.SeriesApp - INFO - Initializing SeriesApp...
|
|
||||||
2025-10-25 17:31:19 - src.core.SerieScanner - INFO - Initialized SerieScanner...
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - SeriesApp initialized with directory: /home/lukas/Volume/serien/
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - FastAPI application started successfully
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - Server running on http://127.0.0.1:8000
|
|
||||||
2025-10-25 17:31:19 - aniworld - INFO - API documentation available at http://127.0.0.1:8000/api/docs
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
### Starting the Server
|
|
||||||
|
|
||||||
**Recommended:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
conda run -n AniWorld python run_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Alternative:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./start_server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**View logs in real-time:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f logs/fastapi_app.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### In Code
|
|
||||||
|
|
||||||
```python
|
|
||||||
from src.infrastructure.logging import get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
logger.info("Message: %s", value)
|
|
||||||
logger.warning("Warning: %s", warning_msg)
|
|
||||||
logger.error("Error occurred", exc_info=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Set log level via environment variable or `.env` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export LOG_LEVEL=INFO # or DEBUG, WARNING, ERROR
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
✅ **Console logging**: Colored, easy-to-read format
|
|
||||||
✅ **File logging**: Detailed with timestamps and logger names
|
|
||||||
✅ **Automatic log directory creation**: `logs/` created if missing
|
|
||||||
✅ **Uvicorn integration**: All uvicorn logs captured
|
|
||||||
✅ **Multiple loggers**: Different loggers for different modules
|
|
||||||
✅ **Configurable log level**: Via environment variable
|
|
||||||
✅ **Proper formatting**: Uses lazy formatting for performance
|
|
||||||
✅ **Startup/shutdown logging**: Clear application lifecycle logs
|
|
||||||
✅ **Error tracebacks**: Full exception context with `exc_info=True`
|
|
||||||
|
|
||||||
## Files Created/Modified
|
|
||||||
|
|
||||||
### Created:
|
|
||||||
|
|
||||||
- `src/infrastructure/logging/logger.py`
|
|
||||||
- `src/infrastructure/logging/uvicorn_config.py`
|
|
||||||
- `src/infrastructure/logging/__init__.py`
|
|
||||||
- `run_server.py`
|
|
||||||
- `start_server.sh`
|
|
||||||
- `docs/logging.md`
|
|
||||||
- `docs/logging_implementation_summary.md` (this file)
|
|
||||||
|
|
||||||
### Modified:
|
|
||||||
|
|
||||||
- `src/server/fastapi_app.py`: Integrated logging throughout
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
The implementation has been tested and verified:
|
|
||||||
|
|
||||||
- ✅ Log file created at `logs/fastapi_app.log`
|
|
||||||
- ✅ Startup messages logged correctly
|
|
||||||
- ✅ Application configuration loaded and logged
|
|
||||||
- ✅ Uvicorn logs captured
|
|
||||||
- ✅ File watching events logged
|
|
||||||
- ✅ Shutdown messages logged
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
Consider adding:
|
|
||||||
|
|
||||||
1. **Log rotation**: Use `RotatingFileHandler` to prevent log files from growing too large
|
|
||||||
2. **Structured logging**: Use `structlog` for JSON-formatted logs
|
|
||||||
3. **Log aggregation**: Send logs to a centralized logging service
|
|
||||||
4. **Performance monitoring**: Add timing logs for slow operations
|
|
||||||
5. **Request logging middleware**: Log all HTTP requests/responses
|
|
||||||
0
logs/no_key_found.log
Normal file
0
logs/no_key_found.log
Normal file
@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Startup script for the Aniworld FastAPI application.
|
|
||||||
|
|
||||||
This script starts the application with proper logging configuration.
|
|
||||||
"""
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
from src.infrastructure.logging.uvicorn_config import get_uvicorn_log_config
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Get logging configuration
|
|
||||||
log_config = get_uvicorn_log_config()
|
|
||||||
|
|
||||||
# Run the application with logging.
|
|
||||||
# Only watch .py files in src/, explicitly exclude __pycache__.
|
|
||||||
# This prevents reload loops from .pyc compilation.
|
|
||||||
uvicorn.run(
|
|
||||||
"src.server.fastapi_app:app",
|
|
||||||
host="127.0.0.1",
|
|
||||||
port=8000,
|
|
||||||
reload=True,
|
|
||||||
reload_dirs=["src"],
|
|
||||||
reload_includes=["*.py"],
|
|
||||||
reload_excludes=["*/__pycache__/*", "*.pyc"],
|
|
||||||
log_config=log_config,
|
|
||||||
)
|
|
||||||
@ -23,10 +23,9 @@ from src.core.interfaces.callbacks import (
|
|||||||
ProgressPhase,
|
ProgressPhase,
|
||||||
)
|
)
|
||||||
from src.core.providers.base_provider import Loader
|
from src.core.providers.base_provider import Loader
|
||||||
|
from src.infrastructure.logging.GlobalLogger import error_logger, noKeyFound_logger
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
error_logger = logging.getLogger("error")
|
|
||||||
no_key_found_logger = logging.getLogger("series.nokey")
|
|
||||||
|
|
||||||
|
|
||||||
class SerieScanner:
|
class SerieScanner:
|
||||||
@ -175,7 +174,7 @@ class SerieScanner:
|
|||||||
# remote metadata, yielding missing episodes per
|
# remote metadata, yielding missing episodes per
|
||||||
# season. Results are saved back to disk so that both
|
# season. Results are saved back to disk so that both
|
||||||
# CLI and API consumers see consistent state.
|
# CLI and API consumers see consistent state.
|
||||||
missing_episodes, _site = (
|
missing_episodes, site = (
|
||||||
self.__get_missing_episodes_and_season(
|
self.__get_missing_episodes_and_season(
|
||||||
serie.key, mp4_files
|
serie.key, mp4_files
|
||||||
)
|
)
|
||||||
@ -193,14 +192,14 @@ class SerieScanner:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.folderDict[serie.key] = serie
|
self.folderDict[serie.key] = serie
|
||||||
no_key_found_logger.info(
|
noKeyFound_logger.info(
|
||||||
"Saved Serie: '%s'", str(serie)
|
"Saved Serie: '%s'", str(serie)
|
||||||
)
|
)
|
||||||
|
|
||||||
except NoKeyFoundException as nkfe:
|
except NoKeyFoundException as nkfe:
|
||||||
# Log error and notify via callback
|
# Log error and notify via callback
|
||||||
error_msg = f"Error processing folder '{folder}': {nkfe}"
|
error_msg = f"Error processing folder '{folder}': {nkfe}"
|
||||||
logger.error(error_msg)
|
NoKeyFoundException.error(error_msg)
|
||||||
|
|
||||||
self._callback_manager.notify_error(
|
self._callback_manager.notify_error(
|
||||||
ErrorContext(
|
ErrorContext(
|
||||||
|
|||||||
@ -46,7 +46,12 @@ if not download_error_logger.handlers:
|
|||||||
download_error_handler.setLevel(logging.ERROR)
|
download_error_handler.setLevel(logging.ERROR)
|
||||||
download_error_logger.addHandler(download_error_handler)
|
download_error_logger.addHandler(download_error_handler)
|
||||||
|
|
||||||
noKeyFound_logger = logging.getLogger()
|
noKeyFound_logger = logging.getLogger("NoKeyFound")
|
||||||
|
if not noKeyFound_logger.handlers:
|
||||||
|
log_path = _logs_dir / "no_key_found.log"
|
||||||
|
noKeyFound_handler = logging.FileHandler(str(log_path))
|
||||||
|
noKeyFound_handler.setLevel(logging.ERROR)
|
||||||
|
noKeyFound_logger.addHandler(noKeyFound_handler)
|
||||||
|
|
||||||
|
|
||||||
class AniworldLoader(Loader):
|
class AniworldLoader(Loader):
|
||||||
|
|||||||
40
src/infrastructure/logging/GlobalLogger.py
Normal file
40
src/infrastructure/logging/GlobalLogger.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
console_handler = None
|
||||||
|
error_logger = None
|
||||||
|
noKeyFound_logger = None
|
||||||
|
noGerFound_logger = None
|
||||||
|
def setupLogger():
|
||||||
|
global console_handler, error_logger, noKeyFound_logger, noGerFound_logger
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s')
|
||||||
|
if (console_handler is None):
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.INFO)
|
||||||
|
console_handler.setFormatter(logging.Formatter(
|
||||||
|
"%(asctime)s - %(levelname)s - %(funcName)s - %(message)s")
|
||||||
|
)
|
||||||
|
logging.getLogger().addHandler(console_handler)
|
||||||
|
logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO)
|
||||||
|
logging.getLogger('charset_normalizer').setLevel(logging.INFO)
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
|
if (error_logger is None):
|
||||||
|
error_logger = logging.getLogger("ErrorLog")
|
||||||
|
error_handler = logging.FileHandler("../errors.log")
|
||||||
|
error_handler.setLevel(logging.ERROR)
|
||||||
|
error_logger.addHandler(error_handler)
|
||||||
|
|
||||||
|
if (noKeyFound_logger is None):
|
||||||
|
noKeyFound_logger = logging.getLogger("NoKeyFound")
|
||||||
|
noKeyFound_handler = logging.FileHandler("../NoKeyFound.log")
|
||||||
|
noKeyFound_handler.setLevel(logging.ERROR)
|
||||||
|
noKeyFound_logger.addHandler(noKeyFound_handler)
|
||||||
|
|
||||||
|
if (noGerFound_logger is None):
|
||||||
|
noGerFound_logger = logging.getLogger("noGerFound")
|
||||||
|
noGerFound_handler = logging.FileHandler("../noGerFound.log")
|
||||||
|
noGerFound_handler.setLevel(logging.ERROR)
|
||||||
|
noGerFound_logger.addHandler(noGerFound_handler)
|
||||||
|
|
||||||
|
setupLogger()
|
||||||
@ -1,7 +0,0 @@
|
|||||||
"""
|
|
||||||
Logging infrastructure for the Aniworld application.
|
|
||||||
"""
|
|
||||||
from src.infrastructure.logging.logger import get_logger, setup_logging
|
|
||||||
from src.infrastructure.logging.uvicorn_config import get_uvicorn_log_config
|
|
||||||
|
|
||||||
__all__ = ["setup_logging", "get_logger", "get_uvicorn_log_config"]
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
"""
|
|
||||||
Logging configuration for the Aniworld application.
|
|
||||||
|
|
||||||
This module provides a centralized logging setup with both console and file
|
|
||||||
logging, following Python logging best practices.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from src.config.settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(
|
|
||||||
log_file: Optional[str] = None,
|
|
||||||
log_level: Optional[str] = None,
|
|
||||||
log_dir: Optional[Path] = None
|
|
||||||
) -> logging.Logger:
|
|
||||||
"""
|
|
||||||
Configure application logging with console and file handlers.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
log_file: Name of the log file (default: "fastapi_app.log")
|
|
||||||
log_level: Logging level (default: from settings or "INFO")
|
|
||||||
log_dir: Directory for log files (default: "logs" in project root)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured logger instance
|
|
||||||
"""
|
|
||||||
# Determine log level
|
|
||||||
level_name = log_level or settings.log_level or "INFO"
|
|
||||||
level = getattr(logging, level_name.upper(), logging.INFO)
|
|
||||||
|
|
||||||
# Determine log directory and file
|
|
||||||
if log_dir is None:
|
|
||||||
# Default to logs directory in project root
|
|
||||||
log_dir = Path(__file__).parent.parent.parent.parent / "logs"
|
|
||||||
|
|
||||||
log_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
if log_file is None:
|
|
||||||
log_file = "fastapi_app.log"
|
|
||||||
|
|
||||||
log_path = log_dir / log_file
|
|
||||||
|
|
||||||
# Create formatters
|
|
||||||
detailed_formatter = logging.Formatter(
|
|
||||||
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
|
|
||||||
console_formatter = logging.Formatter(
|
|
||||||
fmt="%(levelname)s: %(message)s"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure root logger
|
|
||||||
root_logger = logging.getLogger()
|
|
||||||
root_logger.setLevel(level)
|
|
||||||
|
|
||||||
# Remove existing handlers to avoid duplicates
|
|
||||||
root_logger.handlers.clear()
|
|
||||||
|
|
||||||
# Console handler (stdout)
|
|
||||||
console_handler = logging.StreamHandler(sys.stdout)
|
|
||||||
console_handler.setLevel(level)
|
|
||||||
console_handler.setFormatter(console_formatter)
|
|
||||||
root_logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
# File handler
|
|
||||||
file_handler = logging.FileHandler(log_path, mode='a', encoding='utf-8')
|
|
||||||
file_handler.setLevel(level)
|
|
||||||
file_handler.setFormatter(detailed_formatter)
|
|
||||||
root_logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
# Create application logger
|
|
||||||
logger = logging.getLogger("aniworld")
|
|
||||||
logger.setLevel(level)
|
|
||||||
|
|
||||||
# Log startup information
|
|
||||||
logger.info("=" * 60)
|
|
||||||
logger.info("Logging configured successfully")
|
|
||||||
logger.info("Log level: %s", level_name.upper())
|
|
||||||
logger.info("Log file: %s", log_path)
|
|
||||||
logger.info("=" * 60)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name: str) -> logging.Logger:
|
|
||||||
"""
|
|
||||||
Get a logger instance for a specific module.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Name of the logger (typically __name__)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Logger instance
|
|
||||||
"""
|
|
||||||
return logging.getLogger(name)
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
"""
|
|
||||||
Uvicorn logging configuration for the Aniworld application.
|
|
||||||
|
|
||||||
This configuration ensures that uvicorn logs are properly formatted and
|
|
||||||
written to both console and file.
|
|
||||||
"""
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Get the logs directory
|
|
||||||
LOGS_DIR = Path(__file__).parent.parent.parent.parent / "logs"
|
|
||||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
LOG_FILE = LOGS_DIR / "fastapi_app.log"
|
|
||||||
|
|
||||||
LOGGING_CONFIG = {
|
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
|
||||||
"formatters": {
|
|
||||||
"default": {
|
|
||||||
"()": "uvicorn.logging.DefaultFormatter",
|
|
||||||
"fmt": "%(levelprefix)s %(message)s",
|
|
||||||
"use_colors": None,
|
|
||||||
},
|
|
||||||
"access": {
|
|
||||||
"()": "uvicorn.logging.AccessFormatter",
|
|
||||||
"fmt": (
|
|
||||||
'%(levelprefix)s %(client_addr)s - '
|
|
||||||
'"%(request_line)s" %(status_code)s'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"detailed": {
|
|
||||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"handlers": {
|
|
||||||
"console": {
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
"level": "INFO",
|
|
||||||
"formatter": "default",
|
|
||||||
"stream": "ext://sys.stdout",
|
|
||||||
},
|
|
||||||
"file": {
|
|
||||||
"class": "logging.FileHandler",
|
|
||||||
"level": "INFO",
|
|
||||||
"formatter": "detailed",
|
|
||||||
"filename": str(LOG_FILE),
|
|
||||||
"mode": "a",
|
|
||||||
"encoding": "utf-8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"loggers": {
|
|
||||||
"uvicorn": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": "INFO",
|
|
||||||
"propagate": False,
|
|
||||||
},
|
|
||||||
"uvicorn.error": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": "INFO",
|
|
||||||
"propagate": False,
|
|
||||||
},
|
|
||||||
"uvicorn.access": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": "INFO",
|
|
||||||
"propagate": False,
|
|
||||||
},
|
|
||||||
"watchfiles.main": {
|
|
||||||
"handlers": ["console"],
|
|
||||||
"level": "WARNING",
|
|
||||||
"propagate": False,
|
|
||||||
},
|
|
||||||
"aniworld": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": "INFO",
|
|
||||||
"propagate": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"handlers": ["console", "file"],
|
|
||||||
"level": "INFO",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_uvicorn_log_config() -> dict:
|
|
||||||
"""
|
|
||||||
Get the uvicorn logging configuration dictionary.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary containing logging configuration
|
|
||||||
"""
|
|
||||||
return LOGGING_CONFIG
|
|
||||||
@ -95,24 +95,9 @@ async def get_process_locks(
|
|||||||
|
|
||||||
|
|
||||||
class AnimeSummary(BaseModel):
|
class AnimeSummary(BaseModel):
|
||||||
"""Summary of an anime series with missing episodes."""
|
id: str
|
||||||
key: str # Unique identifier (used as id in frontend)
|
title: str
|
||||||
name: str # Series name (can be empty)
|
missing_episodes: int
|
||||||
site: str # Provider site
|
|
||||||
folder: str # Local folder name
|
|
||||||
missing_episodes: dict # Episode dictionary: {season: [episode_numbers]}
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Pydantic model configuration."""
|
|
||||||
json_schema_extra = {
|
|
||||||
"example": {
|
|
||||||
"key": "beheneko-the-elf-girls-cat",
|
|
||||||
"name": "Beheneko",
|
|
||||||
"site": "aniworld.to",
|
|
||||||
"folder": "beheneko the elf girls cat (2025)",
|
|
||||||
"missing_episodes": {"1": [1, 2, 3, 4]}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AnimeDetail(BaseModel):
|
class AnimeDetail(BaseModel):
|
||||||
@ -218,41 +203,26 @@ async def list_anime(
|
|||||||
series = series_app.List.GetMissingEpisode()
|
series = series_app.List.GetMissingEpisode()
|
||||||
summaries: List[AnimeSummary] = []
|
summaries: List[AnimeSummary] = []
|
||||||
for serie in series:
|
for serie in series:
|
||||||
# Get all properties from the serie object
|
episodes_dict = getattr(serie, "episodeDict", {}) or {}
|
||||||
key = getattr(serie, "key", "")
|
missing_episodes = len(episodes_dict)
|
||||||
name = getattr(serie, "name", "")
|
key = getattr(serie, "key", getattr(serie, "folder", ""))
|
||||||
site = getattr(serie, "site", "")
|
title = getattr(serie, "name", "")
|
||||||
folder = getattr(serie, "folder", "")
|
|
||||||
episode_dict = getattr(serie, "episodeDict", {}) or {}
|
|
||||||
|
|
||||||
# Convert episode dict keys to strings for JSON serialization
|
|
||||||
missing_episodes = {str(k): v for k, v in episode_dict.items()}
|
|
||||||
|
|
||||||
summaries.append(
|
summaries.append(
|
||||||
AnimeSummary(
|
AnimeSummary(
|
||||||
key=key,
|
id=key,
|
||||||
name=name,
|
title=title,
|
||||||
site=site,
|
|
||||||
folder=folder,
|
|
||||||
missing_episodes=missing_episodes,
|
missing_episodes=missing_episodes,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply sorting if requested
|
# Apply sorting if requested
|
||||||
if sort_by:
|
if sort_by:
|
||||||
if sort_by in ["title", "name"]:
|
if sort_by == "title":
|
||||||
summaries.sort(key=lambda x: x.name or x.key)
|
summaries.sort(key=lambda x: x.title)
|
||||||
elif sort_by == "id":
|
elif sort_by == "id":
|
||||||
summaries.sort(key=lambda x: x.key)
|
summaries.sort(key=lambda x: x.id)
|
||||||
elif sort_by == "missing_episodes":
|
elif sort_by == "missing_episodes":
|
||||||
# Sort by total number of missing episodes
|
summaries.sort(key=lambda x: x.missing_episodes, reverse=True)
|
||||||
# (count all episodes across all seasons)
|
|
||||||
summaries.sort(
|
|
||||||
key=lambda x: sum(
|
|
||||||
len(eps) for eps in x.missing_episodes.values()
|
|
||||||
),
|
|
||||||
reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return summaries
|
return summaries
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
@ -269,15 +239,15 @@ async def trigger_rescan(
|
|||||||
_auth: dict = Depends(require_auth),
|
_auth: dict = Depends(require_auth),
|
||||||
series_app: Any = Depends(get_series_app),
|
series_app: Any = Depends(get_series_app),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Kick off a rescan of the local library.
|
"""Kick off a background rescan of the local library.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
_auth: Ensures the caller is authenticated (value unused)
|
_auth: Ensures the caller is authenticated (value unused)
|
||||||
series_app: Core `SeriesApp` instance provided via dependency.
|
series_app: Core `SeriesApp` instance provided via dependency.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: Status payload with scan results including
|
Dict[str, Any]: Status payload communicating whether the rescan
|
||||||
number of series found.
|
launched successfully.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: If the rescan command is unsupported or fails.
|
HTTPException: If the rescan command is unsupported or fails.
|
||||||
@ -285,23 +255,8 @@ async def trigger_rescan(
|
|||||||
try:
|
try:
|
||||||
# SeriesApp.ReScan expects a callback; pass a no-op
|
# SeriesApp.ReScan expects a callback; pass a no-op
|
||||||
if hasattr(series_app, "ReScan"):
|
if hasattr(series_app, "ReScan"):
|
||||||
result = series_app.ReScan(lambda *args, **kwargs: None)
|
series_app.ReScan(lambda *args, **kwargs: None)
|
||||||
|
return {"success": True, "message": "Rescan started"}
|
||||||
if result.success:
|
|
||||||
series_count = (
|
|
||||||
result.data.get("series_count", 0)
|
|
||||||
if result.data else 0
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": result.message,
|
|
||||||
"series_count": series_count
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": result.message
|
|
||||||
}
|
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
|||||||
@ -1,122 +0,0 @@
|
|||||||
"""
|
|
||||||
Logging configuration for the FastAPI server.
|
|
||||||
|
|
||||||
This module provides comprehensive logging setup with both console and file
|
|
||||||
handlers, ensuring all server activity is properly logged.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from src.config.settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging() -> Dict[str, logging.Logger]:
|
|
||||||
"""
|
|
||||||
Configure logging for the FastAPI application.
|
|
||||||
|
|
||||||
Creates:
|
|
||||||
- Console handler for real-time output
|
|
||||||
- File handler for server.log (general logs)
|
|
||||||
- File handler for error.log (errors only)
|
|
||||||
- File handler for access.log (request logs)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict containing configured loggers
|
|
||||||
"""
|
|
||||||
# Create logs directory if it doesn't exist
|
|
||||||
log_dir = Path("logs")
|
|
||||||
log_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# Define log file paths
|
|
||||||
server_log_file = log_dir / "server.log"
|
|
||||||
error_log_file = log_dir / "error.log"
|
|
||||||
access_log_file = log_dir / "access.log"
|
|
||||||
|
|
||||||
# Define log format
|
|
||||||
detailed_format = logging.Formatter(
|
|
||||||
fmt="%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s",
|
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
|
|
||||||
simple_format = logging.Formatter(
|
|
||||||
fmt="%(asctime)s - %(levelname)s - %(message)s",
|
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure root logger
|
|
||||||
root_logger = logging.getLogger()
|
|
||||||
root_logger.setLevel(getattr(logging, settings.log_level.upper(), logging.INFO))
|
|
||||||
|
|
||||||
# Remove existing handlers to avoid duplicates
|
|
||||||
root_logger.handlers.clear()
|
|
||||||
|
|
||||||
# Console handler - visible in terminal
|
|
||||||
console_handler = logging.StreamHandler(sys.stdout)
|
|
||||||
console_handler.setLevel(logging.INFO)
|
|
||||||
console_handler.setFormatter(simple_format)
|
|
||||||
root_logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
# File handler for general server logs
|
|
||||||
server_file_handler = logging.FileHandler(server_log_file, mode='a', encoding='utf-8')
|
|
||||||
server_file_handler.setLevel(logging.DEBUG)
|
|
||||||
server_file_handler.setFormatter(detailed_format)
|
|
||||||
root_logger.addHandler(server_file_handler)
|
|
||||||
|
|
||||||
# File handler for errors only
|
|
||||||
error_file_handler = logging.FileHandler(error_log_file, mode='a', encoding='utf-8')
|
|
||||||
error_file_handler.setLevel(logging.ERROR)
|
|
||||||
error_file_handler.setFormatter(detailed_format)
|
|
||||||
root_logger.addHandler(error_file_handler)
|
|
||||||
|
|
||||||
# Configure uvicorn loggers
|
|
||||||
uvicorn_logger = logging.getLogger("uvicorn")
|
|
||||||
uvicorn_logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
uvicorn_access_logger = logging.getLogger("uvicorn.access")
|
|
||||||
uvicorn_access_logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Access log file handler
|
|
||||||
access_file_handler = logging.FileHandler(access_log_file, mode='a', encoding='utf-8')
|
|
||||||
access_file_handler.setLevel(logging.INFO)
|
|
||||||
access_file_handler.setFormatter(simple_format)
|
|
||||||
uvicorn_access_logger.addHandler(access_file_handler)
|
|
||||||
|
|
||||||
# Configure FastAPI logger
|
|
||||||
fastapi_logger = logging.getLogger("fastapi")
|
|
||||||
fastapi_logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Reduce noise from third-party libraries
|
|
||||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
||||||
logging.getLogger("charset_normalizer").setLevel(logging.WARNING)
|
|
||||||
logging.getLogger("multipart").setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
# Log initial setup
|
|
||||||
root_logger.info("=" * 80)
|
|
||||||
root_logger.info("FastAPI Server Logging Initialized")
|
|
||||||
root_logger.info(f"Log Level: {settings.log_level.upper()}")
|
|
||||||
root_logger.info(f"Server Log: {server_log_file.absolute()}")
|
|
||||||
root_logger.info(f"Error Log: {error_log_file.absolute()}")
|
|
||||||
root_logger.info(f"Access Log: {access_log_file.absolute()}")
|
|
||||||
root_logger.info("=" * 80)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"root": root_logger,
|
|
||||||
"uvicorn": uvicorn_logger,
|
|
||||||
"uvicorn.access": uvicorn_access_logger,
|
|
||||||
"fastapi": fastapi_logger,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name: str) -> logging.Logger:
|
|
||||||
"""
|
|
||||||
Get a logger instance for a specific module.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Name of the module/logger
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured logger instance
|
|
||||||
"""
|
|
||||||
return logging.getLogger(name)
|
|
||||||
@ -5,7 +5,6 @@ This module provides the main FastAPI application with proper CORS
|
|||||||
configuration, middleware setup, static file serving, and Jinja2 template
|
configuration, middleware setup, static file serving, and Jinja2 template
|
||||||
integration.
|
integration.
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -19,7 +18,6 @@ from src.config.settings import settings
|
|||||||
|
|
||||||
# Import core functionality
|
# Import core functionality
|
||||||
from src.core.SeriesApp import SeriesApp
|
from src.core.SeriesApp import SeriesApp
|
||||||
from src.infrastructure.logging import setup_logging
|
|
||||||
from src.server.api.analytics import router as analytics_router
|
from src.server.api.analytics import router as analytics_router
|
||||||
from src.server.api.anime import router as anime_router
|
from src.server.api.anime import router as anime_router
|
||||||
from src.server.api.auth import router as auth_router
|
from src.server.api.auth import router as auth_router
|
||||||
@ -53,13 +51,8 @@ from src.server.services.websocket_service import get_websocket_service
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Manage application lifespan (startup and shutdown)."""
|
"""Manage application lifespan (startup and shutdown)."""
|
||||||
# Setup logging first
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
# Startup
|
# Startup
|
||||||
try:
|
try:
|
||||||
logger.info("Starting FastAPI application...")
|
|
||||||
|
|
||||||
# Load configuration from config.json and sync with settings
|
# Load configuration from config.json and sync with settings
|
||||||
try:
|
try:
|
||||||
from src.server.services.config_service import get_config_service
|
from src.server.services.config_service import get_config_service
|
||||||
@ -69,25 +62,25 @@ async def lifespan(app: FastAPI):
|
|||||||
# Sync anime_directory from config.json to settings
|
# Sync anime_directory from config.json to settings
|
||||||
if config.other and config.other.get("anime_directory"):
|
if config.other and config.other.get("anime_directory"):
|
||||||
settings.anime_directory = str(config.other["anime_directory"])
|
settings.anime_directory = str(config.other["anime_directory"])
|
||||||
logger.info(
|
print(
|
||||||
"Loaded anime_directory from config: %s",
|
f"Loaded anime_directory from config: "
|
||||||
settings.anime_directory
|
f"{settings.anime_directory}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to load config from config.json: %s", e)
|
print(f"Warning: Failed to load config from config.json: {e}")
|
||||||
|
|
||||||
# Initialize SeriesApp with configured directory and store it on
|
# Initialize SeriesApp with configured directory and store it on
|
||||||
# application state so it can be injected via dependencies.
|
# application state so it can be injected via dependencies.
|
||||||
if settings.anime_directory:
|
if settings.anime_directory:
|
||||||
app.state.series_app = SeriesApp(settings.anime_directory)
|
app.state.series_app = SeriesApp(settings.anime_directory)
|
||||||
logger.info(
|
print(
|
||||||
"SeriesApp initialized with directory: %s",
|
f"SeriesApp initialized with directory: "
|
||||||
settings.anime_directory
|
f"{settings.anime_directory}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Log warning when anime directory is not configured
|
# Log warning when anime directory is not configured
|
||||||
logger.warning(
|
print(
|
||||||
"ANIME_DIRECTORY not configured. "
|
"WARNING: ANIME_DIRECTORY not configured. "
|
||||||
"Some features may be unavailable."
|
"Some features may be unavailable."
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -107,20 +100,16 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
progress_service.set_broadcast_callback(broadcast_callback)
|
progress_service.set_broadcast_callback(broadcast_callback)
|
||||||
|
|
||||||
logger.info("FastAPI application started successfully")
|
print("FastAPI application started successfully")
|
||||||
logger.info("Server running on http://127.0.0.1:8000")
|
|
||||||
logger.info(
|
|
||||||
"API documentation available at http://127.0.0.1:8000/api/docs"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error during startup: %s", e, exc_info=True)
|
print(f"Error during startup: {e}")
|
||||||
raise # Re-raise to prevent app from starting in broken state
|
raise # Re-raise to prevent app from starting in broken state
|
||||||
|
|
||||||
# Yield control to the application
|
# Yield control to the application
|
||||||
yield
|
yield
|
||||||
|
|
||||||
# Shutdown
|
# Shutdown
|
||||||
logger.info("FastAPI application shutting down")
|
print("FastAPI application shutting down")
|
||||||
|
|
||||||
|
|
||||||
def get_series_app() -> Optional[SeriesApp]:
|
def get_series_app() -> Optional[SeriesApp]:
|
||||||
|
|||||||
@ -567,24 +567,14 @@ class AniWorldApp {
|
|||||||
|
|
||||||
// Check if response has the expected format
|
// Check if response has the expected format
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
// API returns array of AnimeSummary objects with full serie data
|
// API returns array of AnimeSummary objects directly
|
||||||
this.seriesData = data.map(anime => {
|
this.seriesData = data.map(anime => ({
|
||||||
// Count total missing episodes from the episode dictionary
|
id: anime.id,
|
||||||
const episodeDict = anime.missing_episodes || {};
|
name: anime.title,
|
||||||
const totalMissing = Object.values(episodeDict).reduce(
|
title: anime.title,
|
||||||
(sum, episodes) => sum + (Array.isArray(episodes) ? episodes.length : 0),
|
missing_episodes: anime.missing_episodes || 0,
|
||||||
0
|
episodeDict: {} // Will be populated when needed
|
||||||
);
|
}));
|
||||||
|
|
||||||
return {
|
|
||||||
key: anime.key,
|
|
||||||
name: anime.name,
|
|
||||||
site: anime.site,
|
|
||||||
folder: anime.folder,
|
|
||||||
episodeDict: episodeDict,
|
|
||||||
missing_episodes: totalMissing
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else if (data.status === 'success') {
|
} else if (data.status === 'success') {
|
||||||
// Legacy format support
|
// Legacy format support
|
||||||
this.seriesData = data.series;
|
this.seriesData = data.series;
|
||||||
@ -643,7 +633,7 @@ class AniWorldApp {
|
|||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
if (this.sortAlphabetical) {
|
if (this.sortAlphabetical) {
|
||||||
// Pure alphabetical sorting when A-Z is enabled
|
// Pure alphabetical sorting when A-Z is enabled
|
||||||
return this.getDisplayName(a).localeCompare(this.getDisplayName(b));
|
return (a.name || a.folder).localeCompare(b.name || b.folder);
|
||||||
} else {
|
} else {
|
||||||
// Default sorting: missing episodes first (descending), then by name
|
// Default sorting: missing episodes first (descending), then by name
|
||||||
// Always show series with missing episodes first
|
// Always show series with missing episodes first
|
||||||
@ -715,7 +705,7 @@ class AniWorldApp {
|
|||||||
${isSelected ? 'checked' : ''}
|
${isSelected ? 'checked' : ''}
|
||||||
${!canBeSelected ? 'disabled' : ''}>
|
${!canBeSelected ? 'disabled' : ''}>
|
||||||
<div class="series-info">
|
<div class="series-info">
|
||||||
<h3>${this.escapeHtml(this.getDisplayName(serie))}</h3>
|
<h3>${this.escapeHtml(serie.name)}</h3>
|
||||||
<div class="series-folder">${this.escapeHtml(serie.folder)}</div>
|
<div class="series-folder">${this.escapeHtml(serie.folder)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="series-status">
|
<div class="series-status">
|
||||||
@ -859,8 +849,8 @@ class AniWorldApp {
|
|||||||
|
|
||||||
resultsList.innerHTML = results.map(result => `
|
resultsList.innerHTML = results.map(result => `
|
||||||
<div class="search-result-item">
|
<div class="search-result-item">
|
||||||
<span class="search-result-name">${this.escapeHtml(this.getDisplayName(result))}</span>
|
<span class="search-result-name">${this.escapeHtml(result.name)}</span>
|
||||||
<button class="btn btn-small btn-primary" onclick="app.addSeries('${this.escapeHtml(result.link)}', '${this.escapeHtml(this.getDisplayName(result))}')">
|
<button class="btn btn-small btn-primary" onclick="app.addSeries('${this.escapeHtml(result.link)}', '${this.escapeHtml(result.name)}')">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
@ -934,28 +924,15 @@ class AniWorldApp {
|
|||||||
|
|
||||||
async rescanSeries() {
|
async rescanSeries() {
|
||||||
try {
|
try {
|
||||||
this.showToast('Scanning directory...', 'info');
|
|
||||||
|
|
||||||
const response = await this.makeAuthenticatedRequest('/api/anime/rescan', {
|
const response = await this.makeAuthenticatedRequest('/api/anime/rescan', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
console.log('Rescan response:', data);
|
|
||||||
console.log('Success value:', data.success, 'Type:', typeof data.success);
|
|
||||||
|
|
||||||
if (data.success === true) {
|
if (data.status === 'success') {
|
||||||
const seriesCount = data.series_count || 0;
|
this.showToast('Rescan started', 'success');
|
||||||
this.showToast(
|
|
||||||
`Rescan complete! Found ${seriesCount} series with missing episodes.`,
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reload the series list to show the updated data
|
|
||||||
await this.loadSeries();
|
|
||||||
} else {
|
} else {
|
||||||
this.showToast(`Rescan error: ${data.message}`, 'error');
|
this.showToast(`Rescan error: ${data.message}`, 'error');
|
||||||
}
|
}
|
||||||
@ -1036,24 +1013,6 @@ class AniWorldApp {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get display name for anime/series object.
|
|
||||||
* Returns name if available and not empty, otherwise returns key.
|
|
||||||
* @param {Object} anime - Anime/series object with name and key properties
|
|
||||||
* @returns {string} Display name
|
|
||||||
*/
|
|
||||||
getDisplayName(anime) {
|
|
||||||
if (!anime) return '';
|
|
||||||
// Use name if it exists and is not empty (after trimming whitespace)
|
|
||||||
const name = anime.name || '';
|
|
||||||
const trimmedName = name.trim();
|
|
||||||
if (trimmedName) {
|
|
||||||
return trimmedName;
|
|
||||||
}
|
|
||||||
// Fallback to key
|
|
||||||
return anime.key || anime.folder || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConnectionStatus() {
|
updateConnectionStatus() {
|
||||||
const indicator = document.getElementById('connection-status-display');
|
const indicator = document.getElementById('connection-status-display');
|
||||||
if (indicator) {
|
if (indicator) {
|
||||||
@ -2045,7 +2004,7 @@ class AniWorldApp {
|
|||||||
// Update current downloading
|
// Update current downloading
|
||||||
if (data.current_downloading) {
|
if (data.current_downloading) {
|
||||||
currentDownload.classList.remove('hidden');
|
currentDownload.classList.remove('hidden');
|
||||||
document.getElementById('current-serie-name').textContent = this.getDisplayName(data.current_downloading);
|
document.getElementById('current-serie-name').textContent = data.current_downloading.name;
|
||||||
document.getElementById('current-episode').textContent = `${data.current_downloading.missing_episodes} episodes remaining`;
|
document.getElementById('current-episode').textContent = `${data.current_downloading.missing_episodes} episodes remaining`;
|
||||||
} else {
|
} else {
|
||||||
currentDownload.classList.add('hidden');
|
currentDownload.classList.add('hidden');
|
||||||
@ -2056,7 +2015,7 @@ class AniWorldApp {
|
|||||||
queueList.innerHTML = data.queue.map((serie, index) => `
|
queueList.innerHTML = data.queue.map((serie, index) => `
|
||||||
<div class="queue-item">
|
<div class="queue-item">
|
||||||
<div class="queue-item-index">${index + 1}</div>
|
<div class="queue-item-index">${index + 1}</div>
|
||||||
<div class="queue-item-name">${this.escapeHtml(this.getDisplayName(serie))}</div>
|
<div class="queue-item-name">${this.escapeHtml(serie.name)}</div>
|
||||||
<div class="queue-item-status">Waiting</div>
|
<div class="queue-item-status">Waiting</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Startup script for Aniworld FastAPI server with proper logging
|
|
||||||
|
|
||||||
# Activate conda environment and run the server
|
|
||||||
conda run -n AniWorld python run_server.py
|
|
||||||
@ -50,21 +50,11 @@ class TestSeriesAppDependency:
|
|||||||
assert result == mock_series_app_instance
|
assert result == mock_series_app_instance
|
||||||
mock_series_app_class.assert_called_once_with("/path/to/anime")
|
mock_series_app_class.assert_called_once_with("/path/to/anime")
|
||||||
|
|
||||||
@patch('src.server.services.config_service.get_config_service')
|
|
||||||
@patch('src.server.utils.dependencies.settings')
|
@patch('src.server.utils.dependencies.settings')
|
||||||
def test_get_series_app_no_directory_configured(
|
def test_get_series_app_no_directory_configured(self, mock_settings):
|
||||||
self, mock_settings, mock_get_config_service
|
|
||||||
):
|
|
||||||
"""Test SeriesApp dependency when directory is not configured."""
|
"""Test SeriesApp dependency when directory is not configured."""
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_settings.anime_directory = ""
|
mock_settings.anime_directory = ""
|
||||||
|
|
||||||
# Mock config service to return empty config
|
|
||||||
mock_config_service = Mock()
|
|
||||||
mock_config = Mock()
|
|
||||||
mock_config.other = {}
|
|
||||||
mock_config_service.load_config.return_value = mock_config
|
|
||||||
mock_get_config_service.return_value = mock_config_service
|
|
||||||
|
|
||||||
# Act & Assert
|
# Act & Assert
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
@ -188,10 +178,8 @@ class TestAuthenticationDependencies:
|
|||||||
mock_get_current_user.assert_called_once_with(credentials)
|
mock_get_current_user.assert_called_once_with(credentials)
|
||||||
|
|
||||||
@patch('src.server.utils.dependencies.get_current_user')
|
@patch('src.server.utils.dependencies.get_current_user')
|
||||||
def test_optional_auth_with_invalid_credentials(
|
def test_optional_auth_with_invalid_credentials(self,
|
||||||
self,
|
mock_get_current_user):
|
||||||
mock_get_current_user
|
|
||||||
):
|
|
||||||
"""Test optional authentication with invalid credentials."""
|
"""Test optional authentication with invalid credentials."""
|
||||||
# Arrange
|
# Arrange
|
||||||
credentials = HTTPAuthorizationCredentials(
|
credentials = HTTPAuthorizationCredentials(
|
||||||
@ -289,8 +277,6 @@ class TestUtilityDependencies:
|
|||||||
await log_request_dependency(mock_request)
|
await log_request_dependency(mock_request)
|
||||||
|
|
||||||
# Assert - no exception should be raised
|
# Assert - no exception should be raised
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationScenarios:
|
class TestIntegrationScenarios:
|
||||||
"""Integration test scenarios for dependency injection."""
|
"""Integration test scenarios for dependency injection."""
|
||||||
|
|
||||||
@ -298,18 +284,14 @@ class TestIntegrationScenarios:
|
|||||||
"""Test the complete SeriesApp dependency lifecycle."""
|
"""Test the complete SeriesApp dependency lifecycle."""
|
||||||
# Use separate mock instances for each call
|
# Use separate mock instances for each call
|
||||||
with patch('src.server.utils.dependencies.settings') as mock_settings:
|
with patch('src.server.utils.dependencies.settings') as mock_settings:
|
||||||
with patch(
|
with patch('src.server.utils.dependencies.SeriesApp') as mock_series_app_class:
|
||||||
'src.server.utils.dependencies.SeriesApp'
|
|
||||||
) as mock_series_app_class:
|
|
||||||
# Arrange
|
# Arrange
|
||||||
mock_settings.anime_directory = "/path/to/anime"
|
mock_settings.anime_directory = "/path/to/anime"
|
||||||
|
|
||||||
# Create separate mock instances for each instantiation
|
# Create separate mock instances for each instantiation
|
||||||
mock_instance1 = MagicMock()
|
mock_instance1 = MagicMock()
|
||||||
mock_instance2 = MagicMock()
|
mock_instance2 = MagicMock()
|
||||||
mock_series_app_class.side_effect = [
|
mock_series_app_class.side_effect = [mock_instance1, mock_instance2]
|
||||||
mock_instance1, mock_instance2
|
|
||||||
]
|
|
||||||
|
|
||||||
# Act - Get SeriesApp instance
|
# Act - Get SeriesApp instance
|
||||||
app1 = get_series_app()
|
app1 = get_series_app()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user