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:
+92
-16
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user