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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user