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
+92 -16
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, RegexReplacement, PlexConnectionSettings, SyncState } from './types';
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, RegexReplacement, PlexConnectionSettings, SyncState, ScheduleSettings, ScheduleMode } from './types';
import { apiService } from './services/api';
import {
STRIPE_BASE_SPEED,
@@ -18,7 +18,7 @@ import {
import ServerPanel from './components/ServerPanel';
import StrategySelector from './components/StrategySelector';
import ConnectionModal from './components/ConnectionModal';
import { ArrowLeftRight, ShieldCheck, X, Server, ServerOff } from 'lucide-react';
import { ArrowLeftRight, ShieldCheck, X, Server, ServerOff, Clock } from 'lucide-react';
interface Toast {
id: number;
@@ -142,6 +142,17 @@ const App: React.FC = () => {
// Regex State
const [regexReplacements, setRegexReplacements] = useState<RegexReplacement[]>([]);
// Schedule State
const [scheduleSettings, setScheduleSettings] = useState<ScheduleSettings>({
mode: ScheduleMode.DISABLED,
cronExpression: '',
dailyTime: '02:00',
weeklyDays: [0], // Sunday
weeklyTime: '03:00',
autoWatch: false
});
const [nextRunTime, setNextRunTime] = useState<string | undefined>(undefined);
// Toast Notification System
const [toasts, setToasts] = useState<Toast[]>([]);
const timeoutsRef = useRef<{[key: number]: ReturnType<typeof setTimeout>}>({});
@@ -219,6 +230,35 @@ const App: React.FC = () => {
}
}, []);
const loadSchedule = useCallback(async () => {
const result = await apiService.getScheduleSettings();
if (result.status === 'success') {
setScheduleSettings(result.data);
setNextRunTime(result.data.nextRun);
}
}, []);
// Handle Schedule Save
const handleSaveSchedule = async (settings: ScheduleSettings): Promise<boolean> => {
const result = await apiService.saveScheduleSettings(settings);
if (result.status === 'success') {
setScheduleSettings(settings);
// Refresh schedule info to get next run time
loadSchedule();
if (settings.mode === ScheduleMode.DISABLED) {
addToast("Scheduled tasks disabled.");
} else {
addToast("Scheduled task updated successfully.");
}
return true;
} else {
addToast(result.message || "Failed to update schedule.");
return false;
}
};
// Fetch Local Playlists
const refreshLocal = useCallback(async () => {
if (localAbortRef.current) localAbortRef.current.abort();
@@ -280,7 +320,8 @@ const App: React.FC = () => {
// Load persisted configuration
useEffect(() => {
loadSettings();
}, [loadSettings]);
loadSchedule();
}, [loadSettings, loadSchedule]);
// Initial Load
useEffect(() => {
@@ -367,6 +408,25 @@ const App: React.FC = () => {
const isConnected = cloudServerInfo?.isConnected;
const getScheduleDisplayInfo = () => {
if (scheduleSettings.mode === ScheduleMode.DISABLED) {
return { label: 'Auto-Sync', value: 'Disabled', active: false };
}
let label = 'Schedule';
if (scheduleSettings.mode === ScheduleMode.CRON) label = 'Cron Schedule';
else if (scheduleSettings.mode === ScheduleMode.DAILY) label = 'Daily Schedule';
else if (scheduleSettings.mode === ScheduleMode.WEEKLY) label = 'Weekly Schedule';
return {
label,
value: nextRunTime ? `Next: ${nextRunTime}` : 'Calculating...',
active: true
};
};
const scheduleInfo = getScheduleDisplayInfo();
return (
<div className="min-h-screen flex flex-col bg-gray-900 text-gray-100 font-sans overflow-hidden bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-gray-800 via-gray-900 to-black">
@@ -438,18 +498,32 @@ const App: React.FC = () => {
</h1>
</div>
{/* Connection Status Button */}
<button
onClick={() => setIsConnectionModalOpen(true)}
className={`flex items-center justify-center w-9 h-9 rounded-full border transition-all duration-300 hover:scale-105 active:scale-95 shadow-md ${
isConnected
? "bg-green-500/10 border-green-500/50 text-green-400 hover:bg-green-500/20 hover:shadow-green-500/20"
: "bg-red-500/10 border-red-500/50 text-red-400 hover:bg-red-500/20 hover:shadow-red-500/20"
}`}
title={isConnected ? "Connected to Plex" : "Disconnected"}
>
{isConnected ? <Server size={18} /> : <ServerOff size={18} />}
</button>
{/* Normal Toolbar Right */}
<div className="flex items-center gap-4">
{/* Schedule Info */}
<div className="flex flex-col items-end mr-2 md:mr-0 hidden md:flex">
<span className="text-[10px] uppercase font-bold text-gray-500 tracking-wider">
{scheduleInfo.label}
</span>
<div className={`text-xs font-mono flex items-center gap-1.5 ${scheduleInfo.active ? 'text-plex-orange' : 'text-gray-600'}`}>
{scheduleInfo.active && <Clock size={12} />}
<span>{scheduleInfo.value}</span>
</div>
</div>
{/* Connection Status Button */}
<button
onClick={() => setIsConnectionModalOpen(true)}
className={`flex items-center justify-center w-9 h-9 rounded-full border transition-all duration-300 hover:scale-105 active:scale-95 shadow-md ${
isConnected
? "bg-green-500/10 border-green-500/50 text-green-400 hover:bg-green-500/20 hover:shadow-green-500/20"
: "bg-red-500/10 border-red-500/50 text-red-400 hover:bg-red-500/20 hover:shadow-red-500/20"
}`}
title={isConnected ? "Connected to Plex" : "Disconnected"}
>
{isConnected ? <Server size={18} /> : <ServerOff size={18} />}
</button>
</div>
</>
) : (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
@@ -515,11 +589,13 @@ const App: React.FC = () => {
/* Desktop Positioning: Center Horizontally, Anchored Top */
md:top-[64px] md:right-auto md:left-1/2 md:transform md:-translate-x-1/2 md:-translate-y-1/2"
>
<StrategySelector
<StrategySelector
currentStrategy={currentStrategy}
onSelect={handleStrategyChange}
savedRegexReplacements={regexReplacements}
onSaveRegex={handleSaveRegex}
savedSchedule={scheduleSettings}
onSaveSchedule={handleSaveSchedule}
syncState={syncState}
onSync={handleSyncTrigger}
/>