feat: cron-based scheduler with auto-download after rescan

- Replace asyncio sleep loop with APScheduler AsyncIOScheduler + CronTrigger
- Add schedule_time (HH:MM), schedule_days (days of week), auto_download_after_rescan fields to SchedulerConfig
- Add _auto_download_missing() to queue missing episodes after rescan
- Reload config live via reload_config(SchedulerConfig) without restart
- Update GET/POST /api/scheduler/config to return {success, config, status} envelope
- Add day-of-week pill toggles to Settings -> Scheduler section in UI
- Update JS loadSchedulerConfig / saveSchedulerConfig for new API shape
- Add 29 unit tests for SchedulerConfig model, 18 unit tests for SchedulerService
- Rewrite 23 endpoint tests and 36 integration tests for APScheduler behaviour
- Coverage: 96% api/scheduler, 95% scheduler_service, 90% total (>= 80% threshold)
- Update docs: API.md, CONFIGURATION.md, features.md, CHANGELOG.md
This commit is contained in:
2026-02-21 08:56:17 +01:00
parent ac7e15e1eb
commit 0265ae2a70
15 changed files with 1923 additions and 1628 deletions

View File

@@ -660,7 +660,10 @@ Return current application configuration.
"data_dir": "data",
"scheduler": {
"enabled": true,
"interval_minutes": 60
"interval_minutes": 60,
"schedule_time": "03:00",
"schedule_days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
"auto_download_after_rescan": false
},
"logging": {
"level": "INFO",
@@ -691,7 +694,9 @@ Apply an update to the configuration.
{
"scheduler": {
"enabled": true,
"interval_minutes": 30
"interval_minutes": 60,
"schedule_time": "06:30",
"schedule_days": ["mon", "wed", "fri"]
},
"logging": {
"level": "DEBUG"
@@ -1177,47 +1182,21 @@ Source: [src/server/api/nfo.py](../src/server/api/nfo.py#L637-L684)
Prefix: `/api/scheduler`
Source: [src/server/api/scheduler.py](../src/server/api/scheduler.py#L1-L122)
All GET/POST config responses share the same envelope:
```json
{
"success": true,
"config": { ... },
"status": { ... }
}
```
Source: [src/server/api/scheduler.py](../src/server/api/scheduler.py)
### GET /api/scheduler/config
Get current scheduler configuration.
**Authentication:** Required
**Response (200 OK):**
```json
{
"enabled": true,
"interval_minutes": 60
}
```
Source: [src/server/api/scheduler.py](../src/server/api/scheduler.py#L22-L42)
### POST /api/scheduler/config
Update scheduler configuration.
**Authentication:** Required
**Request Body:**
```json
{
"enabled": true,
"interval_minutes": 30
}
```
**Response (200 OK):** Updated scheduler configuration
Source: [src/server/api/scheduler.py](../src/server/api/scheduler.py#L45-L75)
### POST /api/scheduler/trigger-rescan
Manually trigger a library rescan.
Get current scheduler configuration and runtime status.
**Authentication:** Required
@@ -1226,11 +1205,65 @@ Manually trigger a library rescan.
```json
{
"success": true,
"config": {
"enabled": true,
"interval_minutes": 60,
"schedule_time": "03:00",
"schedule_days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
"auto_download_after_rescan": false
},
"status": {
"is_running": true,
"next_run": "2025-07-15T03:00:00+00:00",
"last_run": null,
"scan_in_progress": false
}
}
```
### POST /api/scheduler/config
Update scheduler configuration and apply changes immediately.
**Authentication:** Required
**Request Body (all fields optional, uses model defaults):**
```json
{
"enabled": true,
"schedule_time": "06:30",
"schedule_days": ["mon", "wed", "fri"],
"auto_download_after_rescan": true
}
```
**Response (200 OK):** Same envelope as GET, reflecting saved values.
**Validation errors (422):**
- `schedule_time` must match `HH:MM` (00:0023:59)
- `schedule_days` entries must be one of `mon tue wed thu fri sat sun`
- `interval_minutes` must be ≥ 1
### POST /api/scheduler/trigger-rescan
Manually trigger a library rescan (and auto-download if configured).
**Authentication:** Required
**Response (200 OK):**
```json
{
"message": "Rescan started successfully"
}
```
Source: [src/server/api/scheduler.py](../src/server/api/scheduler.py#L78-L122)
**Error responses:**
- `503` — SeriesApp not yet initialised
- `500` — Rescan failed unexpectedly
---