feat: Implement sync manager and file watcher for automated playlist synchronization
This commit is contained in:
+78
-2
@@ -125,6 +125,8 @@ const App: React.FC = () => {
|
||||
const [loadingCloud, setLoadingCloud] = useState(false);
|
||||
|
||||
const [syncState, setSyncState] = useState<SyncState>(SyncState.IDLE);
|
||||
const manualSyncInProgress = useRef(false);
|
||||
const lastKnownSyncTimeRef = useRef<string | null | undefined>(undefined);
|
||||
|
||||
// Animation Refs
|
||||
const { leftYellowRef, leftGreenRef, rightYellowRef, rightGreenRef } = useStripeAnimation(syncState);
|
||||
@@ -165,7 +167,7 @@ const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const addToast = (message: string) => {
|
||||
const addToast = useCallback((message: string) => {
|
||||
const id = Date.now();
|
||||
// Start with entering: true to position it above
|
||||
const newToast: Toast = { id, message, exiting: false, entering: true };
|
||||
@@ -182,7 +184,7 @@ const App: React.FC = () => {
|
||||
}, TOAST_AUTO_DISMISS_MS);
|
||||
|
||||
timeoutsRef.current[id] = dismissTimer;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Effect to trigger the "slide down" animation
|
||||
useEffect(() => {
|
||||
@@ -361,9 +363,12 @@ const App: React.FC = () => {
|
||||
if (syncState !== SyncState.IDLE) return;
|
||||
|
||||
setSyncState(SyncState.SYNCING);
|
||||
manualSyncInProgress.current = true;
|
||||
|
||||
const result = await apiService.syncPlaylists(currentStrategy, regexReplacements, localPath || undefined);
|
||||
|
||||
manualSyncInProgress.current = false;
|
||||
|
||||
if (result.status === 'success') {
|
||||
setSyncState(SyncState.SUCCESS);
|
||||
|
||||
@@ -379,6 +384,77 @@ const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// SSE for sync status
|
||||
useEffect(() => {
|
||||
const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL || ''}/api/sync/events`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
const { is_syncing, status, error, last_sync_time } = data;
|
||||
|
||||
// Initialize lastKnownSyncTime if it's the first event
|
||||
if (lastKnownSyncTimeRef.current === undefined) {
|
||||
lastKnownSyncTimeRef.current = last_sync_time;
|
||||
// If we are currently syncing on load, show it
|
||||
if (is_syncing && !manualSyncInProgress.current) {
|
||||
setSyncState(SyncState.SYNCING);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If manual sync is in progress, we ignore background updates to avoid state conflict
|
||||
if (manualSyncInProgress.current) {
|
||||
if (last_sync_time !== lastKnownSyncTimeRef.current) {
|
||||
lastKnownSyncTimeRef.current = last_sync_time;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Syncing State
|
||||
if (is_syncing) {
|
||||
if (syncState !== SyncState.SYNCING) {
|
||||
setSyncState(SyncState.SYNCING);
|
||||
}
|
||||
} else {
|
||||
// Check for completion by comparing timestamps
|
||||
if (last_sync_time !== lastKnownSyncTimeRef.current) {
|
||||
lastKnownSyncTimeRef.current = last_sync_time;
|
||||
|
||||
// A sync has completed since our last check
|
||||
if (status === 'success') {
|
||||
setSyncState(SyncState.SUCCESS);
|
||||
refreshLocal();
|
||||
refreshCloud();
|
||||
addToast("Background sync completed successfully.");
|
||||
setTimeout(() => setSyncState(SyncState.IDLE), SYNC_SUCCESS_TOTAL_MS);
|
||||
} else if (status === 'error') {
|
||||
setSyncState(SyncState.ERROR);
|
||||
addToast(`Background sync failed: ${error}`);
|
||||
setTimeout(() => setSyncState(SyncState.IDLE), SYNC_ERROR_RESET_MS);
|
||||
}
|
||||
} else {
|
||||
// Edge case: We are in SYNCING state but backend says not syncing, and time hasn't changed.
|
||||
if (syncState === SyncState.SYNCING) {
|
||||
setSyncState(SyncState.IDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse SSE event", e);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error("EventSource failed:", err);
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}, [syncState, refreshLocal, refreshCloud, addToast]);
|
||||
|
||||
const handleConnectSuccess = async (serverInfo: PlexServerConnection) => {
|
||||
setCloudServerInfo(serverInfo);
|
||||
if (serverInfo.libraryName) {
|
||||
|
||||
Reference in New Issue
Block a user