feat: Implement scheduling functionality for playlist synchronization

- Added a new scheduler module using APScheduler to manage scheduled sync jobs.
- Introduced cron expression validation and job scheduling based on user-defined settings.
- Enhanced frontend to support schedule settings, including cron, daily, and weekly modes.
- Updated API service to handle fetching and saving schedule settings.
- Modified StrategySelector component to include schedule management UI.
- Added new types for schedule settings and modes in the frontend.
- Updated requirements to include APScheduler for scheduling capabilities.
This commit is contained in:
2025-11-29 10:49:35 +09:00
parent 40f818bd2c
commit 7dae8647e6
9 changed files with 765 additions and 182 deletions
+35
View File
@@ -22,6 +22,12 @@ class ServerConfig:
self.sync_mode = DEFAULT_SYNC_MODE
self.local_path = "playlist"
self.path_rules: list[dict[str, str]] = []
self.schedule_mode = "DISABLED"
self.schedule_cron = ""
self.schedule_daily_time = "02:00"
self.schedule_weekly_days = [0]
self.schedule_weekly_time = "03:00"
self.schedule_auto_watch = False
self.load()
def load(self) -> None:
@@ -49,6 +55,12 @@ class ServerConfig:
self.sync_mode = config.get("sync_mode", DEFAULT_SYNC_MODE)
self.local_path = config.get("local_path", "playlist")
self.path_rules = config.get("path_rules", []) or []
self.schedule_mode = config.get("schedule_mode", "DISABLED")
self.schedule_cron = config.get("schedule_cron", "")
self.schedule_daily_time = config.get("schedule_daily_time", "02:00")
self.schedule_weekly_days = config.get("schedule_weekly_days", [0])
self.schedule_weekly_time = config.get("schedule_weekly_time", "03:00")
self.schedule_auto_watch = config.get("schedule_auto_watch", False)
logger.info(f"Server config loaded: {self.__dict__}")
def save(self):
@@ -63,6 +75,12 @@ class ServerConfig:
"sync_mode": self.sync_mode,
"local_path": self.local_path,
"path_rules": self.path_rules,
"schedule_mode": self.schedule_mode,
"schedule_cron": self.schedule_cron,
"schedule_daily_time": self.schedule_daily_time,
"schedule_weekly_days": self.schedule_weekly_days,
"schedule_weekly_time": self.schedule_weekly_time,
"schedule_auto_watch": self.schedule_auto_watch,
}
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4, ensure_ascii=False)
@@ -102,6 +120,23 @@ class ServerConfig:
def set_path_rules(self, path_rules: list[dict[str, str]]) -> None:
self.path_rules = path_rules or []
def set_schedule(
self,
mode: str,
cron: str,
daily_time: str,
weekly_days: list[int],
weekly_time: str,
auto_watch: bool,
) -> None:
self.schedule_mode = mode
self.schedule_cron = cron
self.schedule_daily_time = daily_time
self.schedule_weekly_days = weekly_days
self.schedule_weekly_time = weekly_time
self.schedule_auto_watch = auto_watch
self.save()
def set_and_save_config(
self,
theme: str = None,
+123
View File
@@ -0,0 +1,123 @@
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from app.utils.config import server_config
from app.utils.logger import logger
from app.utils.playlist_merge import sync_all_playlists, SyncMode
import asyncio
scheduler = BackgroundScheduler()
def validate_cron_expression(expression: str) -> bool:
try:
parts = expression.split()
if len(parts) != 5:
return False
# Try to create a trigger to validate
CronTrigger(
minute=parts[0],
hour=parts[1],
day=parts[2],
month=parts[3],
day_of_week=parts[4]
)
return True
except Exception:
return False
def job_function():
logger.info("Executing scheduled sync job...")
try:
# Re-read config to ensure latest values
server_config.load()
mode_str = server_config.sync_mode
regex_rules = server_config.path_rules
local_path = server_config.local_path
asyncio.run(sync_all_playlists(
sync_mode=SyncMode(mode_str),
path_rules=regex_rules,
local_dir=local_path
))
logger.info("Scheduled sync job completed.")
except Exception as e:
logger.error(f"Scheduled sync job failed: {e}")
def start_scheduler():
if not scheduler.running:
scheduler.start()
logger.info("Scheduler started.")
update_scheduler_job()
def update_scheduler_job():
scheduler.remove_all_jobs()
# Reload config to get latest schedule settings
server_config.load()
mode = server_config.schedule_mode
if mode == "DISABLED":
logger.info("Schedule is disabled.")
return
trigger = None
if mode == "CRON":
cron_exp = server_config.schedule_cron
if cron_exp:
try:
# 5 parts: minute hour day month day_of_week
parts = cron_exp.split()
if len(parts) == 5:
trigger = CronTrigger(
minute=parts[0],
hour=parts[1],
day=parts[2],
month=parts[3],
day_of_week=parts[4]
)
except Exception as e:
logger.error(f"Invalid cron expression: {cron_exp}, error: {e}")
elif mode == "DAILY":
time_str = server_config.schedule_daily_time
try:
hour, minute = map(int, time_str.split(':'))
trigger = CronTrigger(hour=hour, minute=minute)
except ValueError:
logger.error(f"Invalid daily time: {time_str}")
elif mode == "WEEKLY":
days = server_config.schedule_weekly_days # list of ints 0-6 (Sun-Sat)
time_str = server_config.schedule_weekly_time
# Frontend: 0(Sun), 1(Mon)... 6(Sat)
# APScheduler: 0(Mon)... 6(Sun)
aps_days = []
for d in days:
if d == 0: aps_days.append(6)
else: aps_days.append(d - 1)
days_str = ",".join(map(str, aps_days))
try:
hour, minute = map(int, time_str.split(':'))
trigger = CronTrigger(day_of_week=days_str, hour=hour, minute=minute)
except ValueError:
logger.error(f"Invalid weekly time: {time_str}")
if trigger:
scheduler.add_job(job_function, trigger)
logger.info(f"Added scheduled job with mode {mode} and trigger {trigger}")
else:
logger.warning(f"Failed to create trigger for mode {mode}")
def get_next_run_time():
jobs = scheduler.get_jobs()
if not jobs:
return None
# Assuming only one job
job = jobs[0]
return job.next_run_time