From 075712e064e29f5a24397d66ee1ca43046a3018e Mon Sep 17 00:00:00 2001 From: Gauvain Date: Sun, 31 May 2026 23:42:51 +0200 Subject: [PATCH] feat(home): refresh Continue Watching on UserDataChanged develop already refreshes the home library sections on LibraryChanged, but nothing reacted to UserDataChanged, so "Continue Watching" / "Next Up" only updated on the 60s interval or screen refocus after finishing an episode. Subscribe to UserDataChanged and invalidate just the progression-based sections (resume / next up / TV hero), debounced to coalesce the burst a single finished item emits. Works on phone and TV (handled in the global provider). Recently-added and suggestions are intentionally left untouched. --- providers/WebSocketProvider.tsx | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/providers/WebSocketProvider.tsx b/providers/WebSocketProvider.tsx index a3d2b6224..6b1140afe 100644 --- a/providers/WebSocketProvider.tsx +++ b/providers/WebSocketProvider.tsx @@ -28,6 +28,20 @@ const LIBRARY_CHANGE_QUERY_KEYS = [ ["episodes"], ] as const; +// Query keys that depend on per-user playback state (resume position, played +// status, favorites) and should be refreshed when the server reports a +// `UserDataChanged`. Scoped to the progression-based sections so finishing an +// episode does not pointlessly refetch "recently added" or suggestions. +const USER_DATA_CHANGE_QUERY_KEYS = [ + ["home", "continueAndNextUp"], + ["home", "resumeItems"], + ["home", "nextUp-all"], + ["home", "heroItems"], + ["resumeItems"], + ["nextUp-all"], + ["nextUp"], +] as const; + interface WebSocketMessage { MessageType: string; Data: any; @@ -83,6 +97,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { const libraryChangeDebounceRef = useRef | null>( null, ); + const userDataChangeDebounceRef = useRef | null>(null); // Pub/sub registry: messageType -> set of handlers. Stored in a ref so // subscribing/dispatching never triggers a re-render. @@ -214,17 +231,49 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { [queryClient], ); + const handleUserDataChanged = useCallback( + (data: any) => { + // Jellyfin sends UserDataChanged when playback position, played status + // or favorites change (e.g. finishing an episode). Only the + // progression-based home sections care about it. + if (!((data?.UserDataList?.length ?? 0) > 0)) { + return; + } + + // Finishing an item can emit several UserDataChanged messages, so + // debounce to invalidate the affected sections only once. + if (userDataChangeDebounceRef.current) { + clearTimeout(userDataChangeDebounceRef.current); + } + userDataChangeDebounceRef.current = setTimeout(() => { + for (const queryKey of USER_DATA_CHANGE_QUERY_KEYS) { + queryClient.invalidateQueries({ queryKey: [...queryKey] }); + } + }, 800); + }, + [queryClient], + ); + // Refresh library-dependent queries when the server reports a change. useEffect( () => subscribe("LibraryChanged", handleLibraryChanged), [subscribe, handleLibraryChanged], ); + // Refresh "Continue Watching" / "Next Up" when playback state changes. + useEffect( + () => subscribe("UserDataChanged", handleUserDataChanged), + [subscribe, handleUserDataChanged], + ); + useEffect(() => { return () => { if (libraryChangeDebounceRef.current) { clearTimeout(libraryChangeDebounceRef.current); } + if (userDataChangeDebounceRef.current) { + clearTimeout(userDataChangeDebounceRef.current); + } }; }, []);