feat: Implement sync manager and file watcher for automated playlist synchronization

This commit is contained in:
2025-11-29 12:26:59 +09:00
parent 22697fdc1d
commit fe4061d1a1
8 changed files with 413 additions and 24 deletions
+78 -2
View File
@@ -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) {
+5
View File
@@ -181,4 +181,9 @@ export const apiService = {
});
return handleResponse(response);
},
async getSyncStatus(): Promise<ApiResponse<{ is_syncing: boolean; last_sync_time: string | null; status: string; error: string | null }>> {
const response = await fetch(`${API_BASE}/api/sync/status`);
return handleResponse(response);
},
};