mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-01 11:38:26 +01:00
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.
This commit is contained in:
@@ -28,6 +28,20 @@ const LIBRARY_CHANGE_QUERY_KEYS = [
|
|||||||
["episodes"],
|
["episodes"],
|
||||||
] as const;
|
] 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 {
|
interface WebSocketMessage {
|
||||||
MessageType: string;
|
MessageType: string;
|
||||||
Data: any;
|
Data: any;
|
||||||
@@ -83,6 +97,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
const libraryChangeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
|
const libraryChangeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const userDataChangeDebounceRef = useRef<ReturnType<
|
||||||
|
typeof setTimeout
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
// Pub/sub registry: messageType -> set of handlers. Stored in a ref so
|
// Pub/sub registry: messageType -> set of handlers. Stored in a ref so
|
||||||
// subscribing/dispatching never triggers a re-render.
|
// subscribing/dispatching never triggers a re-render.
|
||||||
@@ -214,17 +231,49 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
|||||||
[queryClient],
|
[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.
|
// Refresh library-dependent queries when the server reports a change.
|
||||||
useEffect(
|
useEffect(
|
||||||
() => subscribe("LibraryChanged", handleLibraryChanged),
|
() => subscribe("LibraryChanged", handleLibraryChanged),
|
||||||
[subscribe, handleLibraryChanged],
|
[subscribe, handleLibraryChanged],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Refresh "Continue Watching" / "Next Up" when playback state changes.
|
||||||
|
useEffect(
|
||||||
|
() => subscribe("UserDataChanged", handleUserDataChanged),
|
||||||
|
[subscribe, handleUserDataChanged],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (libraryChangeDebounceRef.current) {
|
if (libraryChangeDebounceRef.current) {
|
||||||
clearTimeout(libraryChangeDebounceRef.current);
|
clearTimeout(libraryChangeDebounceRef.current);
|
||||||
}
|
}
|
||||||
|
if (userDataChangeDebounceRef.current) {
|
||||||
|
clearTimeout(userDataChangeDebounceRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user