[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): Use custom hook for player queries and fix now-playing forced re-render

FoxxMD (Jul 6, 2026, 5:07 PM UTC) b2b20187 655b97ce

+74 -54
+74 -54
src/client/components/chakraPlayer/Player.tsx
··· 65 65 expiration 66 66 } = data; 67 67 68 + const [positionBuffer, setProgressBuffer] = useState<undefined | number>(undefined); 69 + const [intervalId, setIntervalId] = useState<undefined | number>(undefined); 70 + const [lastUpdated, setLastUpdated] = useState<undefined | number>(undefined); 71 + 68 72 if(expiration !== undefined && dayjs().isAfter(dayjs(expiration))) { 69 73 return null; 70 74 } ··· 83 87 } 84 88 } 85 89 } 86 - 87 - const [positionBuffer, setProgressBuffer] = useState<undefined | number>(undefined); 88 - const [intervalId, setIntervalId] = useState<undefined | number>(undefined); 89 - 90 90 91 91 // this effect block is used to set the buffer progress using a realtime counter (setinterval) 92 92 // we only set this as "active" if the player has reported it is calculated playing and we have position ··· 117 117 }); 118 118 } else if(isNowPlaying) { 119 119 interval = setInterval(() => { 120 + // force now playing-only player to re-render 121 + // so we can stop rendering it if it passes expiration date 122 + setLastUpdated(dayjs().unix()); 120 123 }, 1000); 121 124 setIntervalId((old) => { 122 125 if(old !== undefined) { ··· 136 139 } 137 140 } 138 141 return () => clearInterval(interval); 139 - },[setProgressBuffer, data, setIntervalId, isNowPlaying]); 142 + },[setProgressBuffer, data, setIntervalId, setLastUpdated, isNowPlaying]); 140 143 141 144 const indeterminate = isNowPlaying || (calculated === 'playing' && data.position === undefined); 142 145 const positionProgress = indeterminate || data.position === undefined || duration === undefined ? undefined : Math.trunc((data.position/duration) * 100); ··· 204 207 data: initData, 205 208 sot 206 209 } = props; 210 + const { isPending, isError, data, error } = usePlayerQuery(componentId, platformId, {initData}); 211 + 212 + if (isError) { 213 + return <ErrorAlert error={error} /> 214 + } 215 + 216 + if (!isPending) { 217 + return <ChakraPlayer nowPlaying={nowPlaying} data={data} sot={sot} /> 218 + } 219 + } 220 + 221 + type UsePlayerQueryOpts = { 222 + initData?: SourcePlayerJson 223 + } 224 + 225 + export function usePlayerQuery(componentId: number, platformId: string, opts: UsePlayerQueryOpts = {}) { 226 + const { initData } = opts; 207 227 const queryClient = useQueryClient(); 208 228 useEffect(() => { 209 229 if (initData !== undefined && queryClient.getQueryData(tanQueries.players.single(componentId, platformId).queryKey) === undefined) { ··· 223 243 staleTime: Infinity, 224 244 }); 225 245 226 - if (isError) { 227 - return <ErrorAlert error={error} /> 228 - } 229 - 230 - if (!isPending) { 231 - return <ChakraPlayer nowPlaying={nowPlaying} data={data} sot={sot} /> 232 - } 246 + return { data, isPending, isError, error }; 233 247 } 234 248 235 249 export const PlayersContainer = (props: { data: ComponentCommonApiJson, live?: boolean, nowPlaying?: boolean, stack?: ComponentProps<typeof Stack>, container?: ComponentProps<typeof Container> }) => { ··· 273 287 </Stack>; 274 288 } 275 289 290 + function usePlayersQuery(componentId: number, players?: ComponentCommonApiJson['players']) { 291 + const queryClient = useQueryClient(); 292 + 293 + useEffect(() => { 294 + if(queryClient.getQueryData(tanQueries.players.list(componentId).queryKey) === undefined) { 295 + queryClient.setQueryData(tanQueries.players.list(componentId).queryKey, players ?? {}); 296 + } 297 + }, [players]); 298 + 299 + const client = useSSEContext<MsSseEvent>(); 300 + useSSEAnyEvent(client, (payload) => { 301 + if('componentId' in (payload.data as object) && (payload.data as Record<string, any>).componentId === componentId) { 302 + switch(payload.type) { 303 + case 'playerUpdate': { 304 + const playerPayload = payload.data as MsSseEventPayload<SourcePlayerJson>; 305 + queryClient.setQueryData(tanQueries.players.list(componentId).queryKey, (old: Record<string, SourcePlayerJson>) => { 306 + if(old[playerPayload.data.platformId] === undefined || 'expiration' in playerPayload.data) { 307 + let newData: Record<string, SourcePlayerJson> = {...old}; 308 + newData[playerPayload.data.platformId] = playerPayload.data; 309 + return newData; 310 + } 311 + }); 312 + } 313 + break; 314 + case 'playerDelete':{ 315 + const playerPayload = payload.data as MsSseEventPayload<{platformId: string}>; 316 + queryClient.setQueryData(tanQueries.players.list(componentId).queryKey, (old: Record<string, SourcePlayerJson>) => { 317 + if(old[playerPayload.data.platformId] !== undefined) { 318 + let newData: Record<string, SourcePlayerJson> = {...old}; 319 + delete newData[playerPayload.data.platformId]; 320 + return newData; 321 + } 322 + }); 323 + } 324 + break; 325 + } 326 + } 327 + }); 328 + 329 + const { isPending, isError, data = {}, error } = useQuery({ 330 + ...tanQueries.players.list(componentId), 331 + staleTime: Infinity, 332 + }); 333 + 334 + return {isPending, isError, data, error}; 335 + } 336 + 276 337 export const PlayersContainerFetchable = (props: { data: ComponentCommonApiJson, live?: boolean, nowPlaying?: boolean, stack?: ComponentProps<typeof Stack>, container?: ComponentProps<typeof Container> }) => { 277 338 const { 278 339 data: initData, ··· 282 343 stack = {} 283 344 } = props; 284 345 285 - const queryClient = useQueryClient(); 286 - 287 - useEffect(() => { 288 - if (initData !== undefined && queryClient.getQueryData(tanQueries.players.list(initData.id).queryKey) === undefined) { 289 - queryClient.setQueryData(tanQueries.players.list(initData.id).queryKey, initData.players); 290 - } 291 - }, [initData]); 292 - 293 - const client = useSSEContext<MsSseEvent>(); 294 - useSSEAnyEvent(client, (payload) => { 295 - if('componentId' in (payload.data as object) && (payload.data as Record<string, any>).componentId === initData.id) { 296 - switch(payload.type) { 297 - case 'playerUpdate': { 298 - const playerPayload = payload.data as MsSseEventPayload<SourcePlayerJson>; 299 - queryClient.setQueryData(tanQueries.players.list(initData.id).queryKey, (old: Record<string, SourcePlayerJson>) => { 300 - if(old[playerPayload.data.platformId] === undefined || 'expiration' in playerPayload.data) { 301 - let newData: Record<string, SourcePlayerJson> = {...old}; 302 - newData[playerPayload.data.platformId] = playerPayload.data; 303 - return newData; 304 - } 305 - }); 306 - } 307 - break; 308 - case 'playerDelete':{ 309 - const playerPayload = payload.data as MsSseEventPayload<{platformId: string}>; 310 - queryClient.setQueryData(tanQueries.players.list(initData.id).queryKey, (old: Record<string, SourcePlayerJson>) => { 311 - if(old[playerPayload.data.platformId] !== undefined) { 312 - let newData: Record<string, SourcePlayerJson> = {...old}; 313 - delete newData[playerPayload.data.platformId]; 314 - return newData; 315 - } 316 - }); 317 - } 318 - break; 319 - } 320 - } 321 - }); 322 - 323 - const { isPending, isError, data = {}, error } = useQuery({ 324 - ...tanQueries.players.list(initData.id), 325 - staleTime: Infinity, 326 - }); 346 + const { isPending, isError, data = {}, error } = usePlayersQuery(initData.id, initData.players); 327 347 328 348 const mergedData = useMemo(() => ({...initData, players: data}),[initData,data]); 329 349