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:
@@ -2,16 +2,67 @@ from typing import Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError, field_validator
|
||||
|
||||
_VALID_DAYS = frozenset(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])
|
||||
_ALL_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
|
||||
|
||||
class SchedulerConfig(BaseModel):
|
||||
"""Scheduler related configuration."""
|
||||
"""Scheduler related configuration.
|
||||
|
||||
Cron-based scheduling is configured via ``schedule_time`` and
|
||||
``schedule_days``. The legacy ``interval_minutes`` field is kept for
|
||||
backward compatibility but is **deprecated** and ignored when
|
||||
``schedule_time`` is set.
|
||||
"""
|
||||
|
||||
enabled: bool = Field(
|
||||
default=True, description="Whether the scheduler is enabled"
|
||||
)
|
||||
interval_minutes: int = Field(
|
||||
default=60, ge=1, description="Scheduler interval in minutes"
|
||||
default=60,
|
||||
ge=1,
|
||||
description="[Deprecated] Scheduler interval in minutes. "
|
||||
"Use schedule_time + schedule_days instead.",
|
||||
)
|
||||
schedule_time: str = Field(
|
||||
default="03:00",
|
||||
description="Daily run time in 24-hour HH:MM format (e.g. '03:00')",
|
||||
)
|
||||
schedule_days: List[str] = Field(
|
||||
default_factory=lambda: list(_ALL_DAYS),
|
||||
description="Days of week to run the scheduler (3-letter lowercase "
|
||||
"abbreviations: mon, tue, wed, thu, fri, sat, sun). "
|
||||
"Empty list means disabled.",
|
||||
)
|
||||
auto_download_after_rescan: bool = Field(
|
||||
default=False,
|
||||
description="Automatically queue and start downloads for all missing "
|
||||
"episodes after a scheduled rescan completes.",
|
||||
)
|
||||
|
||||
@field_validator("schedule_time")
|
||||
@classmethod
|
||||
def validate_schedule_time(cls, v: str) -> str:
|
||||
"""Validate HH:MM format within 00:00–23:59."""
|
||||
import re
|
||||
if not re.fullmatch(r"([01]\d|2[0-3]):[0-5]\d", v or ""):
|
||||
raise ValueError(
|
||||
f"Invalid schedule_time '{v}'. "
|
||||
"Expected HH:MM in 24-hour format (00:00–23:59)."
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("schedule_days")
|
||||
@classmethod
|
||||
def validate_schedule_days(cls, v: List[str]) -> List[str]:
|
||||
"""Validate each entry is a valid 3-letter lowercase day abbreviation."""
|
||||
invalid = [d for d in v if d not in _VALID_DAYS]
|
||||
if invalid:
|
||||
raise ValueError(
|
||||
f"Invalid day(s) in schedule_days: {invalid}. "
|
||||
f"Allowed values: {sorted(_VALID_DAYS)}"
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class BackupConfig(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user