fix progress issues
This commit is contained in:
parent
d5f7b1598f
commit
ca4bf72fde
215
SERVER_COMMANDS.md
Normal file
215
SERVER_COMMANDS.md
Normal 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)
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
@ -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
|
|
||||||
@ -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`
|
||||||
|
|||||||
@ -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
23
stop_server.sh
Normal 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"
|
||||||
398
tests/integration/test_download_progress_integration.py
Normal file
398
tests/integration/test_download_progress_integration.py
Normal 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
|
||||||
403
tests/unit/test_download_progress_websocket.py
Normal file
403
tests/unit/test_download_progress_websocket.py
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user