fix progress issues

This commit is contained in:
Lukas 2025-11-02 08:33:44 +01:00
parent d5f7b1598f
commit ca4bf72fde
8 changed files with 1217 additions and 322 deletions

215
SERVER_COMMANDS.md Normal file
View File

@ -0,0 +1,215 @@
# Server Management Commands
Quick reference for starting, stopping, and managing the Aniworld server.
## Start Server
### Using the start script (Recommended)
```bash
./start_server.sh
```
### Using conda directly
```bash
conda run -n AniWorld python run_server.py
```
### Using uvicorn directly
```bash
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
```
## Stop Server
### Using the stop script (Recommended)
```bash
./stop_server.sh
```
### Manual commands
**Kill uvicorn processes:**
```bash
pkill -f "uvicorn.*fastapi_app:app"
```
**Kill process on port 8000:**
```bash
lsof -ti:8000 | xargs kill -9
```
**Kill run_server.py processes:**
```bash
pkill -f "run_server.py"
```
## Check Server Status
**Check if port 8000 is in use:**
```bash
lsof -i:8000
```
**Check for running uvicorn processes:**
```bash
ps aux | grep uvicorn
```
**Check server is responding:**
```bash
curl http://127.0.0.1:8000/api/health
```
## Restart Server
```bash
./stop_server.sh && ./start_server.sh
```
## Common Issues
### "Address already in use" Error
**Problem:** Port 8000 is already occupied
**Solution:**
```bash
./stop_server.sh
# or
lsof -ti:8000 | xargs kill -9
```
### Server not responding
**Check logs:**
```bash
tail -f logs/app.log
```
**Check if process is running:**
```bash
ps aux | grep uvicorn
```
### Cannot connect to server
**Verify server is running:**
```bash
curl http://127.0.0.1:8000/api/health
```
**Check firewall:**
```bash
sudo ufw status
```
## Development Mode
**Run with auto-reload:**
```bash
./start_server.sh # Already includes --reload
```
**Run with custom port:**
```bash
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8080 --reload
```
**Run with debug logging:**
```bash
export LOG_LEVEL=DEBUG
./start_server.sh
```
## Production Mode
**Run without auto-reload:**
```bash
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 0.0.0.0 --port 8000 --workers 4
```
**Run with systemd (Linux):**
```bash
sudo systemctl start aniworld
sudo systemctl stop aniworld
sudo systemctl restart aniworld
sudo systemctl status aniworld
```
## URLs
- **Web Interface:** http://127.0.0.1:8000
- **API Documentation:** http://127.0.0.1:8000/api/docs
- **Login Page:** http://127.0.0.1:8000/login
- **Queue Management:** http://127.0.0.1:8000/queue
- **Health Check:** http://127.0.0.1:8000/api/health
## Default Credentials
- **Password:** `Hallo123!`
## Log Files
- **Application logs:** `logs/app.log`
- **Download logs:** `logs/downloads/`
- **Error logs:** Check console output or systemd journal
## Quick Troubleshooting
| Symptom | Solution |
| ------------------------ | ------------------------------------ |
| Port already in use | `./stop_server.sh` |
| Server won't start | Check `logs/app.log` |
| 404 errors | Verify URL and check routing |
| WebSocket not connecting | Check server is running and firewall |
| Slow responses | Check system resources (`htop`) |
| Database errors | Check `data/` directory permissions |
## Environment Variables
```bash
# Set log level
export LOG_LEVEL=DEBUG|INFO|WARNING|ERROR
# Set server port
export PORT=8000
# Set host
export HOST=127.0.0.1
# Set workers (production)
export WORKERS=4
```
## Related Scripts
- `start_server.sh` - Start the server
- `stop_server.sh` - Stop the server
- `run_server.py` - Python server runner
- `scripts/setup.py` - Initial setup
## More Information
- [User Guide](docs/user_guide.md)
- [API Reference](docs/api_reference.md)
- [Deployment Guide](docs/deployment.md)

View File

@ -1,67 +1,7 @@
{ {
"pending": [ "pending": [
{ {
"id": "1ee2224c-24bf-46ea-b577-30d04ce8ecb8", "id": "0c1563e1-db41-4163-95b5-b0a6a9531bf1",
"serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD",
"episode": {
"season": 1,
"episode": 7,
"title": null
},
"status": "pending",
"priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472425Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "40f88e7f-912d-431f-b840-e960851e9fdf",
"serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD",
"episode": {
"season": 1,
"episode": 8,
"title": null
},
"status": "pending",
"priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472460Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "3e9a2239-5a5f-4c3a-9d8f-b4d3172e17aa",
"serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD",
"episode": {
"season": 1,
"episode": 9,
"title": null
},
"status": "pending",
"priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472495Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "6a7f16e1-8621-402f-8c32-bed2db9e9749",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -72,7 +12,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472529Z", "added_at": "2025-11-02T06:44:29.298728Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -81,7 +21,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "c91d509d-66ed-4846-a61a-0d0e38034213", "id": "5813d573-6834-47e6-a2c0-545aa9c28125",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -92,7 +32,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472562Z", "added_at": "2025-11-02T06:44:29.298756Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -101,7 +41,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "eaf73ac6-d8fd-4714-bb4b-e6c7875a5bee", "id": "f89d2f3a-b8f9-47f3-9cc1-15419489c883",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -112,7 +52,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472595Z", "added_at": "2025-11-02T06:44:29.298786Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -121,7 +61,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "7b57ccf5-4d07-48ea-9306-41335a79eb8e", "id": "30684faf-7d6e-4544-81c8-6a592d47cdfd",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -132,7 +72,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472629Z", "added_at": "2025-11-02T06:44:29.298813Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -141,7 +81,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "67322d2f-b376-4111-9e44-4a5367f1e5c6", "id": "94794c92-0546-485d-8829-0594d04536ee",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -152,7 +92,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472662Z", "added_at": "2025-11-02T06:44:29.298840Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -161,7 +101,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "827e02b4-6599-4fa2-8260-8493d098858f", "id": "23ba40ea-e7bb-4def-9bea-825466859d4c",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -172,7 +112,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472695Z", "added_at": "2025-11-02T06:44:29.298867Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -181,7 +121,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "bf7a53ac-c45a-4d05-aa13-9da48f83093d", "id": "33462781-c0e8-4e0c-b4d2-172f4856d796",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -192,7 +132,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472727Z", "added_at": "2025-11-02T06:44:29.298895Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -201,7 +141,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "956fd623-91c5-408f-9173-73af645dfac9", "id": "0314395b-9540-4f73-8a16-7007797a89b9",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -212,7 +152,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472759Z", "added_at": "2025-11-02T06:44:29.298924Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -221,7 +161,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "0cf66d7f-0158-43f6-af13-a06d1302569b", "id": "1074d85c-a66e-42f2-b299-70a2584944ff",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -232,7 +172,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472793Z", "added_at": "2025-11-02T06:44:29.298952Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -241,7 +181,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "9f36b72c-d169-44ff-9c76-b9c326486546", "id": "384ebe70-3f39-44e7-9162-372246fbff69",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -252,7 +192,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472825Z", "added_at": "2025-11-02T06:44:29.298982Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -261,7 +201,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "f84e96e2-c41c-49f6-937a-dc6ef543b194", "id": "63e51a26-c3c8-4ce0-950a-39d0d131ee0a",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -272,7 +212,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472874Z", "added_at": "2025-11-02T06:44:29.299010Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -281,7 +221,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "b5718a39-a4a8-4ae5-b033-f7ac21444519", "id": "8b38c4cb-9c1f-4f74-8dc3-5c5e8e292bf1",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -292,7 +232,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472909Z", "added_at": "2025-11-02T06:44:29.299038Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -301,7 +241,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "2c9a0f29-f46a-4a50-9c60-38f98d5042d0", "id": "3a19d684-5710-4c50-920e-12a5e0f933c9",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -312,7 +252,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472942Z", "added_at": "2025-11-02T06:44:29.299066Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -321,7 +261,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "bb566a05-4308-4dd2-a4a8-a330facc7861", "id": "62cd2622-ed1d-43fb-831b-3512718cabea",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -332,7 +272,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.472975Z", "added_at": "2025-11-02T06:44:29.299094Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -341,7 +281,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "e00f909b-2614-4552-8774-984b230c962b", "id": "160901aa-1a0f-4ae8-bc27-1753fec582cd",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -352,7 +292,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473014Z", "added_at": "2025-11-02T06:44:29.299123Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -361,7 +301,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "ad91d8eb-c655-463c-88ef-5f0cc501b937", "id": "efd0717e-708f-455a-942c-461195bc75ed",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -372,7 +312,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473050Z", "added_at": "2025-11-02T06:44:29.299153Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -381,7 +321,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "af7429fa-2bd5-43b5-9ff1-a98ee6a80b0b", "id": "c232eb7e-4745-457e-aad3-a103dd808664",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -392,7 +332,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473084Z", "added_at": "2025-11-02T06:44:29.299182Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -401,7 +341,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "2082cb55-85f4-4fb9-8a22-398c566df455", "id": "916b49f9-4af5-4de5-931b-082a68621592",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -412,7 +352,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473117Z", "added_at": "2025-11-02T06:44:29.299213Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -421,7 +361,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "d7110004-a6a9-4f00-be8c-96b7771a585f", "id": "ebe578cb-58eb-4380-b379-65f46f99b792",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -432,7 +372,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473150Z", "added_at": "2025-11-02T06:44:29.299242Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -441,7 +381,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "21499a37-0ac0-497a-aebb-f61cb7b873eb", "id": "523a2eab-371b-4a92-a9ba-179a3a0eca84",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -452,7 +392,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473184Z", "added_at": "2025-11-02T06:44:29.299274Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -461,7 +401,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "1b523ae3-14e2-4147-ac9c-0fddebb35827", "id": "846a365c-0300-4384-9548-3f4db3f612d0",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -472,7 +412,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473217Z", "added_at": "2025-11-02T06:44:29.299305Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -481,7 +421,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "b8bf978b-7f8d-4aeb-930b-fb55fd052632", "id": "af2100e8-2457-41ba-aec6-dbdc5f58a5b0",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -492,7 +432,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473250Z", "added_at": "2025-11-02T06:44:29.299335Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -501,7 +441,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "3541e47d-4cc8-4f81-ad72-3022a978b308", "id": "49c9dc78-5cc1-4036-ba07-b8d5d64197b3",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -512,7 +452,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473284Z", "added_at": "2025-11-02T06:44:29.299367Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -521,7 +461,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "2ee4a535-31d2-4318-be84-2cdb0b853384", "id": "b08b3060-7e4d-4220-8141-4006623a1855",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -532,7 +472,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473317Z", "added_at": "2025-11-02T06:44:29.299396Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -541,7 +481,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "15db1133-a9c6-4869-9618-915bbf09c40c", "id": "d0503f9b-6598-4a6e-afaa-f866a1fefaf0",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -552,7 +492,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473350Z", "added_at": "2025-11-02T06:44:29.299430Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -561,7 +501,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "d0acdf2f-9563-4c09-8995-6da7ba76eeb5", "id": "d87a4bb7-8d66-42c1-963a-7f7de6a57fc9",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -572,7 +512,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473384Z", "added_at": "2025-11-02T06:44:29.299475Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -581,7 +521,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "9a4da2ad-46e8-4df2-aa5d-7cdea6a173fa", "id": "08605fcb-408c-4a4e-a529-cbbd52305508",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -592,7 +532,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473416Z", "added_at": "2025-11-02T06:44:29.299506Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -601,7 +541,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "8b5b8a10-4fa0-4a0b-8edc-cc9be0a742d0", "id": "a0157d01-8631-4701-ab51-9c82787930be",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -612,7 +552,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473451Z", "added_at": "2025-11-02T06:44:29.299542Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -621,7 +561,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "25e29c4b-f407-4d8b-b2ac-e4f39533e183", "id": "d3dff6a3-c853-4270-b357-106659fc80bc",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -632,7 +572,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473483Z", "added_at": "2025-11-02T06:44:29.299596Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -641,7 +581,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "a65e100c-1060-48e2-b065-979a9f0ae647", "id": "e459527d-f518-4e04-947f-efaba1a17f47",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -652,7 +592,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473517Z", "added_at": "2025-11-02T06:44:29.299649Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -661,7 +601,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "a731f0f0-9ac0-4ec5-ac51-bad3f285592d", "id": "166f2724-a632-4925-80a9-466b47d27516",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -672,7 +612,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473553Z", "added_at": "2025-11-02T06:44:29.299683Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -681,7 +621,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "db7c028c-a105-4592-aa2b-df76c11075b7", "id": "c65e9f24-ab9d-4865-b23b-3e76e501c418",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -692,7 +632,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473587Z", "added_at": "2025-11-02T06:44:29.299713Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -701,7 +641,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "e49a8143-c0af-480b-915c-1bd77e99572c", "id": "e3b65bc9-0185-4b57-8223-5524f6eab5f2",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -712,7 +652,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473621Z", "added_at": "2025-11-02T06:44:29.299743Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -721,7 +661,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "c68b02a7-1bca-4403-8ee4-81fd935c7d74", "id": "8599b63a-ca44-4def-b0d7-d38377b84184",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -732,7 +672,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473655Z", "added_at": "2025-11-02T06:44:29.299773Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -741,7 +681,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "dfaef101-ab10-46cc-b4cf-d1cde0b1382a", "id": "8efe8645-2ff6-478c-a07d-b4c433645698",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -752,7 +692,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473690Z", "added_at": "2025-11-02T06:44:29.299803Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -761,7 +701,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "e6ffbe50-00e6-4710-8fc3-41858baab084", "id": "4fabf138-d193-462f-bec9-df907ebf46e4",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -772,7 +712,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473725Z", "added_at": "2025-11-02T06:44:29.299833Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -781,7 +721,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "6dd00e02-269d-41a4-8318-037c451d1282", "id": "4133c81b-df1c-4b82-be1a-358ed2d84f1a",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -792,7 +732,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473766Z", "added_at": "2025-11-02T06:44:29.299860Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -801,7 +741,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "a72414f0-a1db-46fb-9292-f776cd732c3f", "id": "84f6cff7-15d6-4ad5-b94e-e712626dfa19",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -812,7 +752,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473802Z", "added_at": "2025-11-02T06:44:29.299888Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -821,7 +761,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "1a3541b5-63db-417b-9683-24ed2ee9325f", "id": "adff47ad-57af-4dc6-a9bd-ea7284f52b87",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -832,7 +772,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473837Z", "added_at": "2025-11-02T06:44:29.299916Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -841,7 +781,7 @@
"source_url": null "source_url": null
}, },
{ {
"id": "7a697f8b-f48a-4f8e-8c1d-457b1a802db5", "id": "9df059cb-c3aa-41f9-92da-b9d11b618d42",
"serie_id": "highschool-dxd", "serie_id": "highschool-dxd",
"serie_folder": "Highschool DxD", "serie_folder": "Highschool DxD",
"serie_name": "Highschool DxD", "serie_name": "Highschool DxD",
@ -852,7 +792,7 @@
}, },
"status": "pending", "status": "pending",
"priority": "NORMAL", "priority": "NORMAL",
"added_at": "2025-11-01T17:50:20.473878Z", "added_at": "2025-11-02T06:44:29.299945Z",
"started_at": null, "started_at": null,
"completed_at": null, "completed_at": null,
"progress": null, "progress": null,
@ -863,5 +803,5 @@
], ],
"active": [], "active": [],
"failed": [], "failed": [],
"timestamp": "2025-11-01T18:23:07.297144+00:00" "timestamp": "2025-11-02T07:30:11.550330+00:00"
} }

View File

@ -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

View File

@ -105,7 +105,6 @@ For each task completed:
--- ---
### Prerequisites ### Prerequisites
1. Server is running: `conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload` 1. Server is running: `conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload`

View File

@ -6,6 +6,7 @@ class QueueManager {
constructor() { constructor() {
this.socket = null; this.socket = null;
this.refreshInterval = null; this.refreshInterval = null;
this.pendingProgressUpdates = new Map(); // Store progress updates waiting for cards
this.init(); this.init();
} }
@ -78,6 +79,13 @@ class QueueManager {
const serieName = data.serie_name || data.serie || 'Unknown'; const serieName = data.serie_name || data.serie || 'Unknown';
const episode = data.episode || ''; const episode = data.episode || '';
this.showToast(`Completed: ${serieName}${episode ? ' - Episode ' + episode : ''}`, 'success'); this.showToast(`Completed: ${serieName}${episode ? ' - Episode ' + episode : ''}`, 'success');
// Clear any pending progress updates for this download
const downloadId = data.item_id || data.download_id || data.id;
if (downloadId) {
this.pendingProgressUpdates.delete(downloadId);
}
// Full reload needed - item moved from active to completed // Full reload needed - item moved from active to completed
this.loadQueueData(); this.loadQueueData();
}; };
@ -88,6 +96,13 @@ class QueueManager {
const handleDownloadError = (data) => { const handleDownloadError = (data) => {
const message = data.error || data.message || 'Unknown error'; const message = data.error || data.message || 'Unknown error';
this.showToast(`Download failed: ${message}`, 'error'); this.showToast(`Download failed: ${message}`, 'error');
// Clear any pending progress updates for this download
const downloadId = data.item_id || data.download_id || data.id;
if (downloadId) {
this.pendingProgressUpdates.delete(downloadId);
}
// Full reload needed - item moved from active to failed // Full reload needed - item moved from active to failed
this.loadQueueData(); this.loadQueueData();
}; };
@ -218,6 +233,9 @@ class QueueManager {
const data = await response.json(); const data = await response.json();
this.updateQueueDisplay(data); this.updateQueueDisplay(data);
// Process any pending progress updates after queue is loaded
this.processPendingProgressUpdates();
} catch (error) { } catch (error) {
console.error('Error loading queue data:', error); console.error('Error loading queue data:', error);
} }
@ -286,20 +304,43 @@ class QueueManager {
updateDownloadProgress(data) { updateDownloadProgress(data) {
console.log('updateDownloadProgress called with:', JSON.stringify(data, null, 2)); console.log('updateDownloadProgress called with:', JSON.stringify(data, null, 2));
// Extract download ID - handle different data structures // Extract download ID - prioritize metadata.item_id (actual item ID)
let downloadId = data.id || data.download_id || data.item_id; // Progress service sends id with "download_" prefix, but we need the actual item ID
let downloadId = null;
// First try metadata.item_id (this is the actual download item ID)
if (data.metadata && data.metadata.item_id) {
downloadId = data.metadata.item_id;
}
// Fallback to other ID fields
if (!downloadId) {
downloadId = data.item_id || data.download_id;
}
// If ID starts with "download_", extract the actual ID
if (!downloadId && data.id) {
if (data.id.startsWith('download_')) {
downloadId = data.id.substring(9); // Remove "download_" prefix
} else {
downloadId = data.id;
}
}
// Check if data is wrapped in another 'data' property // Check if data is wrapped in another 'data' property
if (!downloadId && data.data) { if (!downloadId && data.data) {
downloadId = data.data.id || data.data.download_id || data.data.item_id; if (data.data.metadata && data.data.metadata.item_id) {
downloadId = data.data.metadata.item_id;
} else if (data.data.item_id) {
downloadId = data.data.item_id;
} else if (data.data.id && data.data.id.startsWith('download_')) {
downloadId = data.data.id.substring(9);
} else {
downloadId = data.data.id || data.data.download_id;
}
data = data.data; // Use nested data data = data.data; // Use nested data
} }
// Also try metadata.item_id as fallback
if (!downloadId && data.metadata && data.metadata.item_id) {
downloadId = data.metadata.item_id;
}
if (!downloadId) { if (!downloadId) {
console.warn('No download ID in progress data'); console.warn('No download ID in progress data');
console.warn('Data structure:', data); console.warn('Data structure:', data);
@ -307,15 +348,31 @@ class QueueManager {
return; return;
} }
console.log(`Looking for download card with ID: ${downloadId}`);
// Find the download card in active downloads // Find the download card in active downloads
const card = document.querySelector(`[data-download-id="${downloadId}"]`); const card = document.querySelector(`[data-download-id="${downloadId}"]`);
if (!card) { if (!card) {
// Card not found - might need to reload queue to get new active download // Card not found - store update and reload queue
console.log(`Download card not found for ID: ${downloadId}, reloading queue`); console.warn(`Download card not found for ID: ${downloadId}`);
// Debug: Log all existing download cards
const allCards = document.querySelectorAll('[data-download-id]');
console.log(`Found ${allCards.length} download cards:`);
allCards.forEach(c => console.log(` - ${c.getAttribute('data-download-id')}`));
// Store this progress update to retry after queue loads
console.log(`Storing progress update for ${downloadId} to retry after reload`);
this.pendingProgressUpdates.set(downloadId, data);
// Reload queue to sync state
console.log('Reloading queue to sync state...');
this.loadQueueData(); this.loadQueueData();
return; return;
} }
console.log(`Found download card for ID: ${downloadId}, updating progress`);
// Extract progress information - handle both ProgressService and yt-dlp formats // Extract progress information - handle both ProgressService and yt-dlp formats
const progress = data.progress || data; const progress = data.progress || data;
const percent = progress.percent || 0; const percent = progress.percent || 0;
@ -364,6 +421,35 @@ class QueueManager {
console.log(`Updated progress for ${downloadId}: ${percent.toFixed(1)}%`); console.log(`Updated progress for ${downloadId}: ${percent.toFixed(1)}%`);
} }
processPendingProgressUpdates() {
if (this.pendingProgressUpdates.size === 0) {
return;
}
console.log(`Processing ${this.pendingProgressUpdates.size} pending progress updates...`);
// Process each pending update
const processed = [];
for (const [downloadId, data] of this.pendingProgressUpdates.entries()) {
// Check if card now exists
const card = document.querySelector(`[data-download-id="${downloadId}"]`);
if (card) {
console.log(`Retrying progress update for ${downloadId}`);
this.updateDownloadProgress(data);
processed.push(downloadId);
} else {
console.log(`Card still not found for ${downloadId}, will retry on next reload`);
}
}
// Remove processed updates
processed.forEach(id => this.pendingProgressUpdates.delete(id));
if (processed.length > 0) {
console.log(`Successfully processed ${processed.length} pending updates`);
}
}
renderActiveDownloads(downloads) { renderActiveDownloads(downloads) {
const container = document.getElementById('active-downloads'); const container = document.getElementById('active-downloads');

23
stop_server.sh Normal file
View File

@ -0,0 +1,23 @@
#!/bin/bash
# Stop Aniworld FastAPI Server
echo "Stopping Aniworld server..."
# Method 1: Kill uvicorn processes
pkill -f "uvicorn.*fastapi_app:app" && echo "✓ Stopped uvicorn processes"
# Method 2: Kill any process using port 8000
PORT_PID=$(lsof -ti:8000)
if [ -n "$PORT_PID" ]; then
kill -9 $PORT_PID
echo "✓ Killed process on port 8000 (PID: $PORT_PID)"
else
echo "✓ Port 8000 is already free"
fi
# Method 3: Kill any python processes running the server
pkill -f "run_server.py" && echo "✓ Stopped run_server.py processes"
echo ""
echo "Server stopped successfully!"
echo "You can restart it with: ./start_server.sh"

View File

@ -0,0 +1,398 @@
"""Integration tests for download progress WebSocket real-time updates.
This module tests the end-to-end flow of download progress from the
download service through the WebSocket service to connected clients.
"""
import asyncio
from typing import Any, Dict, List
from unittest.mock import Mock, patch
import pytest
from src.server.models.download import EpisodeIdentifier
from src.server.services.anime_service import AnimeService
from src.server.services.download_service import DownloadService
from src.server.services.progress_service import ProgressService
from src.server.services.websocket_service import WebSocketService
@pytest.fixture
def mock_series_app():
"""Mock SeriesApp for testing."""
app = Mock()
app.series_list = []
app.search = Mock(return_value=[])
app.ReScan = Mock()
def mock_download(
serie_folder, season, episode, key, callback=None, **kwargs
):
"""Simulate download with realistic progress updates."""
if callback:
# Simulate yt-dlp progress updates
for percent in [10, 25, 50, 75, 90, 100]:
callback({
'percent': float(percent),
'downloaded_mb': percent,
'total_mb': 100.0,
'speed_mbps': 2.5,
'eta_seconds': int((100 - percent) / 2.5),
})
result = Mock()
result.success = True
result.message = "Download completed"
return result
app.download = Mock(side_effect=mock_download)
return app
@pytest.fixture
def progress_service():
"""Create a ProgressService instance."""
return ProgressService()
@pytest.fixture
def websocket_service():
"""Create a WebSocketService instance."""
return WebSocketService()
@pytest.fixture
async def anime_service(mock_series_app, progress_service):
"""Create an AnimeService."""
with patch(
"src.server.services.anime_service.SeriesApp",
return_value=mock_series_app
):
service = AnimeService(
directory="/test/anime",
progress_service=progress_service,
)
yield service
@pytest.fixture
async def download_service(anime_service, progress_service):
"""Create a DownloadService."""
service = DownloadService(
anime_service=anime_service,
progress_service=progress_service,
persistence_path="/tmp/test_integration_progress_queue.json",
)
yield service
await service.stop()
class TestDownloadProgressIntegration:
"""Integration tests for download progress WebSocket flow."""
@pytest.mark.asyncio
async def test_full_progress_flow_with_websocket(
self, download_service, websocket_service
):
"""Test complete flow from download to WebSocket broadcast."""
# Track all messages sent via WebSocket
sent_messages: List[Dict[str, Any]] = []
# Mock WebSocket broadcast methods
original_broadcast_progress = (
websocket_service.broadcast_download_progress
)
async def mock_broadcast_progress(download_id: str, data: dict):
"""Capture broadcast calls."""
sent_messages.append({
'type': 'download_progress',
'download_id': download_id,
'data': data,
})
# Call original to maintain functionality
await original_broadcast_progress(download_id, data)
websocket_service.broadcast_download_progress = (
mock_broadcast_progress
)
# Connect download service to WebSocket service
async def broadcast_callback(update_type: str, data: dict):
"""Bridge download service to WebSocket service."""
if update_type == "download_progress":
await websocket_service.broadcast_download_progress(
data.get("download_id", ""),
data,
)
download_service.set_broadcast_callback(broadcast_callback)
# Add download to queue
await download_service.add_to_queue(
serie_id="integration_test",
serie_folder="test_folder",
serie_name="Integration Test Anime",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
# Start processing
await download_service.start_queue_processing()
# Wait for download to complete
await asyncio.sleep(1.0)
# Verify progress messages were sent
progress_messages = [
m for m in sent_messages if m['type'] == 'download_progress'
]
assert len(progress_messages) >= 3 # Multiple progress updates
# Verify progress increases
percentages = [
m['data'].get('progress', {}).get('percent', 0)
for m in progress_messages
]
# Should have increasing percentages
for i in range(1, len(percentages)):
assert percentages[i] >= percentages[i - 1]
# Last update should be close to 100%
assert percentages[-1] >= 90
@pytest.mark.asyncio
async def test_websocket_client_receives_progress(
self, download_service, websocket_service
):
"""Test that WebSocket clients receive progress messages."""
# Track messages received by clients
client_messages: List[Dict[str, Any]] = []
# Mock WebSocket client
class MockWebSocket:
"""Mock WebSocket for testing."""
async def accept(self):
pass
async def send_json(self, data):
"""Capture sent messages."""
client_messages.append(data)
async def receive_json(self):
# Keep connection open
await asyncio.sleep(10)
mock_ws = MockWebSocket()
# Connect mock client
connection_id = "test_client_1"
await websocket_service.connect(mock_ws, connection_id)
# Connect download service to WebSocket service
async def broadcast_callback(update_type: str, data: dict):
if update_type == "download_progress":
await websocket_service.broadcast_download_progress(
data.get("download_id", ""),
data,
)
download_service.set_broadcast_callback(broadcast_callback)
# Add and start download
await download_service.add_to_queue(
serie_id="client_test",
serie_folder="test_folder",
serie_name="Client Test Anime",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
await download_service.start_queue_processing()
await asyncio.sleep(1.0)
# Verify client received messages
progress_messages = [
m for m in client_messages
if m.get('type') == 'download_progress'
]
assert len(progress_messages) >= 2
# Cleanup
await websocket_service.disconnect(connection_id)
@pytest.mark.asyncio
async def test_multiple_clients_receive_same_progress(
self, download_service, websocket_service
):
"""Test that all connected clients receive progress updates."""
# Track messages for each client
client1_messages: List[Dict] = []
client2_messages: List[Dict] = []
class MockWebSocket:
"""Mock WebSocket for testing."""
def __init__(self, message_list):
self.messages = message_list
async def accept(self):
pass
async def send_json(self, data):
self.messages.append(data)
async def receive_json(self):
await asyncio.sleep(10)
# Connect two clients
client1 = MockWebSocket(client1_messages)
client2 = MockWebSocket(client2_messages)
await websocket_service.connect(client1, "client1")
await websocket_service.connect(client2, "client2")
# Connect download service
async def broadcast_callback(update_type: str, data: dict):
if update_type == "download_progress":
await websocket_service.broadcast_download_progress(
data.get("download_id", ""),
data,
)
download_service.set_broadcast_callback(broadcast_callback)
# Start download
await download_service.add_to_queue(
serie_id="multi_client_test",
serie_folder="test_folder",
serie_name="Multi Client Test",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
await download_service.start_queue_processing()
await asyncio.sleep(1.0)
# Both clients should receive progress
client1_progress = [
m for m in client1_messages
if m.get('type') == 'download_progress'
]
client2_progress = [
m for m in client2_messages
if m.get('type') == 'download_progress'
]
assert len(client1_progress) >= 2
assert len(client2_progress) >= 2
# Both should have similar number of updates
assert abs(len(client1_progress) - len(client2_progress)) <= 2
# Cleanup
await websocket_service.disconnect("client1")
await websocket_service.disconnect("client2")
@pytest.mark.asyncio
async def test_progress_data_structure_matches_frontend_expectations(
self, download_service, websocket_service
):
"""Test that progress data structure matches frontend requirements."""
captured_data: List[Dict] = []
async def capture_broadcast(update_type: str, data: dict):
if update_type == "download_progress":
captured_data.append(data)
await websocket_service.broadcast_download_progress(
data.get("download_id", ""),
data,
)
download_service.set_broadcast_callback(capture_broadcast)
await download_service.add_to_queue(
serie_id="structure_test",
serie_folder="test_folder",
serie_name="Structure Test",
episodes=[EpisodeIdentifier(season=2, episode=3)],
)
await download_service.start_queue_processing()
await asyncio.sleep(1.0)
assert len(captured_data) > 0
# Verify data structure matches frontend expectations
for data in captured_data:
# Required fields for frontend (queue.js)
assert 'download_id' in data or 'item_id' in data
assert 'serie_name' in data
assert 'season' in data
assert 'episode' in data
assert 'progress' in data
# Progress object structure
progress = data['progress']
assert 'percent' in progress
assert 'downloaded_mb' in progress
assert 'total_mb' in progress
# Verify episode info
assert data['season'] == 2
assert data['episode'] == 3
assert data['serie_name'] == "Structure Test"
@pytest.mark.asyncio
async def test_disconnected_client_doesnt_receive_progress(
self, download_service, websocket_service
):
"""Test that disconnected clients don't receive updates."""
client_messages: List[Dict] = []
class MockWebSocket:
async def accept(self):
pass
async def send_json(self, data):
client_messages.append(data)
async def receive_json(self):
await asyncio.sleep(10)
mock_ws = MockWebSocket()
# Connect and then disconnect
connection_id = "temp_client"
await websocket_service.connect(mock_ws, connection_id)
await websocket_service.disconnect(connection_id)
# Connect download service
async def broadcast_callback(update_type: str, data: dict):
if update_type == "download_progress":
await websocket_service.broadcast_download_progress(
data.get("download_id", ""),
data,
)
download_service.set_broadcast_callback(broadcast_callback)
# Start download after disconnect
await download_service.add_to_queue(
serie_id="disconnect_test",
serie_folder="test_folder",
serie_name="Disconnect Test",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
initial_message_count = len(client_messages)
await download_service.start_queue_processing()
await asyncio.sleep(1.0)
# Should not receive progress updates after disconnect
progress_messages = [
m for m in client_messages[initial_message_count:]
if m.get('type') == 'download_progress'
]
assert len(progress_messages) == 0

View File

@ -0,0 +1,403 @@
"""Unit tests for download progress WebSocket updates.
This module tests the integration between download service progress tracking
and WebSocket broadcasting to ensure real-time updates are properly sent
to connected clients.
"""
import asyncio
from typing import Any, Dict, List
from unittest.mock import Mock, patch
import pytest
from src.server.models.download import (
DownloadPriority,
DownloadProgress,
EpisodeIdentifier,
)
from src.server.services.anime_service import AnimeService
from src.server.services.download_service import DownloadService
from src.server.services.progress_service import ProgressService
@pytest.fixture
def mock_series_app():
"""Mock SeriesApp for testing."""
app = Mock()
app.series_list = []
app.search = Mock(return_value=[])
app.ReScan = Mock()
# Mock download with progress callback
def mock_download(
serie_folder, season, episode, key, callback=None, **kwargs
):
"""Simulate download with progress updates."""
if callback:
# Simulate progress updates
callback({
'percent': 25.0,
'downloaded_mb': 25.0,
'total_mb': 100.0,
'speed_mbps': 2.5,
'eta_seconds': 30,
})
callback({
'percent': 50.0,
'downloaded_mb': 50.0,
'total_mb': 100.0,
'speed_mbps': 2.5,
'eta_seconds': 20,
})
callback({
'percent': 100.0,
'downloaded_mb': 100.0,
'total_mb': 100.0,
'speed_mbps': 2.5,
'eta_seconds': 0,
})
# Return success result
result = Mock()
result.success = True
result.message = "Download completed"
return result
app.download = Mock(side_effect=mock_download)
return app
@pytest.fixture
def progress_service():
"""Create a ProgressService instance for testing."""
return ProgressService()
@pytest.fixture
async def anime_service(mock_series_app, progress_service):
"""Create an AnimeService with mocked dependencies."""
with patch(
"src.server.services.anime_service.SeriesApp",
return_value=mock_series_app
):
service = AnimeService(
directory="/test/anime",
progress_service=progress_service,
)
yield service
@pytest.fixture
async def download_service(anime_service, progress_service):
"""Create a DownloadService with dependencies."""
service = DownloadService(
anime_service=anime_service,
progress_service=progress_service,
persistence_path="/tmp/test_download_progress_queue.json",
)
yield service
await service.stop()
class TestDownloadProgressWebSocket:
"""Test download progress WebSocket broadcasting."""
@pytest.mark.asyncio
async def test_progress_callback_broadcasts_updates(
self, download_service
):
"""Test that progress callback broadcasts updates via WebSocket."""
broadcasts: List[Dict[str, Any]] = []
async def mock_broadcast(update_type: str, data: dict):
"""Capture broadcast calls."""
broadcasts.append({"type": update_type, "data": data})
download_service.set_broadcast_callback(mock_broadcast)
# Add item to queue
item_ids = await download_service.add_to_queue(
serie_id="test_serie_1",
serie_folder="test_folder",
serie_name="Test Anime",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.NORMAL,
)
assert len(item_ids) == 1
# Start processing - this should trigger download with progress
result = await download_service.start_queue_processing()
assert result is not None
# Wait for download to process
await asyncio.sleep(0.5)
# Filter progress broadcasts
progress_broadcasts = [
b for b in broadcasts if b["type"] == "download_progress"
]
# Should have received multiple progress updates
assert len(progress_broadcasts) >= 2
# Verify progress data structure
for broadcast in progress_broadcasts:
data = broadcast["data"]
assert "download_id" in data or "item_id" in data
assert "progress" in data
progress = data["progress"]
assert "percent" in progress
assert "downloaded_mb" in progress
assert "total_mb" in progress
assert 0 <= progress["percent"] <= 100
@pytest.mark.asyncio
async def test_progress_updates_include_episode_info(
self, download_service
):
"""Test that progress updates include episode information."""
broadcasts: List[Dict[str, Any]] = []
async def mock_broadcast(update_type: str, data: dict):
broadcasts.append({"type": update_type, "data": data})
download_service.set_broadcast_callback(mock_broadcast)
# Add item with specific episode info
await download_service.add_to_queue(
serie_id="test_serie_2",
serie_folder="test_folder",
serie_name="My Test Anime",
episodes=[EpisodeIdentifier(season=2, episode=5)],
priority=DownloadPriority.HIGH,
)
# Start processing
await download_service.start_queue_processing()
await asyncio.sleep(0.5)
# Find progress broadcasts
progress_broadcasts = [
b for b in broadcasts if b["type"] == "download_progress"
]
assert len(progress_broadcasts) > 0
# Verify episode info is included
data = progress_broadcasts[0]["data"]
assert data["serie_name"] == "My Test Anime"
assert data["season"] == 2
assert data["episode"] == 5
@pytest.mark.asyncio
async def test_progress_percent_increases(self, download_service):
"""Test that progress percentage increases over time."""
broadcasts: List[Dict[str, Any]] = []
async def mock_broadcast(update_type: str, data: dict):
broadcasts.append({"type": update_type, "data": data})
download_service.set_broadcast_callback(mock_broadcast)
await download_service.add_to_queue(
serie_id="test_serie_3",
serie_folder="test_folder",
serie_name="Progress Test",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
await download_service.start_queue_processing()
await asyncio.sleep(0.5)
# Get progress broadcasts in order
progress_broadcasts = [
b for b in broadcasts if b["type"] == "download_progress"
]
# Verify we have multiple updates
assert len(progress_broadcasts) >= 2
# Verify progress increases
percentages = [
b["data"]["progress"]["percent"] for b in progress_broadcasts
]
# Each percentage should be >= the previous one
for i in range(1, len(percentages)):
assert percentages[i] >= percentages[i - 1]
@pytest.mark.asyncio
async def test_progress_includes_speed_and_eta(self, download_service):
"""Test that progress updates include speed and ETA."""
broadcasts: List[Dict[str, Any]] = []
async def mock_broadcast(update_type: str, data: dict):
broadcasts.append({"type": update_type, "data": data})
download_service.set_broadcast_callback(mock_broadcast)
await download_service.add_to_queue(
serie_id="test_serie_4",
serie_folder="test_folder",
serie_name="Speed Test",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
await download_service.start_queue_processing()
await asyncio.sleep(0.5)
progress_broadcasts = [
b for b in broadcasts if b["type"] == "download_progress"
]
assert len(progress_broadcasts) > 0
# Check that speed and ETA are present
progress = progress_broadcasts[0]["data"]["progress"]
assert "speed_mbps" in progress
assert "eta_seconds" in progress
# Speed and ETA should be numeric (or None)
if progress["speed_mbps"] is not None:
assert isinstance(progress["speed_mbps"], (int, float))
if progress["eta_seconds"] is not None:
assert isinstance(progress["eta_seconds"], (int, float))
@pytest.mark.asyncio
async def test_no_broadcast_without_callback(self, download_service):
"""Test that no errors occur when broadcast callback is not set."""
# Don't set broadcast callback
await download_service.add_to_queue(
serie_id="test_serie_5",
serie_folder="test_folder",
serie_name="No Broadcast Test",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
# Should complete without errors
await download_service.start_queue_processing()
await asyncio.sleep(0.5)
# Verify download completed successfully
status = await download_service.get_queue_status()
assert len(status.completed_downloads) == 1
@pytest.mark.asyncio
async def test_broadcast_error_handling(self, download_service):
"""Test that broadcast errors don't break download process."""
error_count = 0
async def failing_broadcast(update_type: str, data: dict):
"""Broadcast that always fails."""
nonlocal error_count
error_count += 1
raise RuntimeError("Broadcast failed")
download_service.set_broadcast_callback(failing_broadcast)
await download_service.add_to_queue(
serie_id="test_serie_6",
serie_folder="test_folder",
serie_name="Error Handling Test",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
# Should complete despite broadcast errors
await download_service.start_queue_processing()
await asyncio.sleep(0.5)
# Verify download still completed
status = await download_service.get_queue_status()
assert len(status.completed_downloads) == 1
# Verify broadcast was attempted
assert error_count > 0
@pytest.mark.asyncio
async def test_multiple_downloads_broadcast_separately(
self, download_service
):
"""Test that multiple downloads broadcast their progress separately."""
broadcasts: List[Dict[str, Any]] = []
async def mock_broadcast(update_type: str, data: dict):
broadcasts.append({"type": update_type, "data": data})
download_service.set_broadcast_callback(mock_broadcast)
# Add multiple episodes
item_ids = await download_service.add_to_queue(
serie_id="test_serie_7",
serie_folder="test_folder",
serie_name="Multi Episode Test",
episodes=[
EpisodeIdentifier(season=1, episode=1),
EpisodeIdentifier(season=1, episode=2),
],
)
assert len(item_ids) == 2
# Start processing
await download_service.start_queue_processing()
await asyncio.sleep(1.0) # Give time for both downloads
# Get progress broadcasts
progress_broadcasts = [
b for b in broadcasts if b["type"] == "download_progress"
]
# Should have progress for both episodes
assert len(progress_broadcasts) >= 4 # At least 2 updates per episode
# Verify different download IDs
download_ids = set()
for broadcast in progress_broadcasts:
download_id = (
broadcast["data"].get("download_id")
or broadcast["data"].get("item_id")
)
if download_id:
download_ids.add(download_id)
# Should have at least 2 unique download IDs
assert len(download_ids) >= 2
@pytest.mark.asyncio
async def test_progress_data_format_matches_model(self, download_service):
"""Test that broadcast data matches DownloadProgress model."""
broadcasts: List[Dict[str, Any]] = []
async def mock_broadcast(update_type: str, data: dict):
broadcasts.append({"type": update_type, "data": data})
download_service.set_broadcast_callback(mock_broadcast)
await download_service.add_to_queue(
serie_id="test_serie_8",
serie_folder="test_folder",
serie_name="Model Test",
episodes=[EpisodeIdentifier(season=1, episode=1)],
)
await download_service.start_queue_processing()
await asyncio.sleep(0.5)
progress_broadcasts = [
b for b in broadcasts if b["type"] == "download_progress"
]
assert len(progress_broadcasts) > 0
# Verify progress can be parsed as DownloadProgress
progress_data = progress_broadcasts[0]["data"]["progress"]
progress = DownloadProgress(**progress_data)
# Verify required fields
assert isinstance(progress.percent, float)
assert isinstance(progress.downloaded_mb, float)
assert 0 <= progress.percent <= 100
assert progress.downloaded_mb >= 0