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