From 9bb48fbf13f8c62ffc13b3c2124e60b17759e0a2 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 15 Jun 2026 17:17:08 +0000 Subject: [PATCH] POC player live update --- package-lock.json | 30 +++++++++++ package.json | 1 + .../scrobblers/AbstractScrobbleClient.ts | 1 + src/backend/server/api.ts | 26 +++++++++- src/backend/server/middleware.ts | 27 ++++++++++ src/backend/sources/AbstractSource.ts | 1 + src/client/AppNext.tsx | 43 ++++++++++++++++ src/client/components/Provider.tsx | 2 + src/client/components/chakraPlayer/Player.tsx | 50 ++++++++++++++++++- .../msComponent/MSComponentList.tsx | 1 + .../msComponent/MSComponentSummary.tsx | 4 +- 11 files changed, 182 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index dcc7d5f1..915d59e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,6 +125,7 @@ "@storybook/react-vite": "^10.4.3", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.101.0", + "@tanstack/react-query-devtools": "^5.101.0", "@tsconfig/node18": "^1.0.1", "@types/chai": "^4.3.0", "@types/chai-as-promised": "^7.1.5", @@ -5184,6 +5185,17 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-devtools": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.101.0.tgz", + "integrity": "sha512-MVqw17k08RQtGGLEL654+dX/btbX9p/8WjkznO//zusLTMaObxi3Q+MoFwGVkC9K3tqjn8qrrNhJevXx4fJTeQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.101.0", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", @@ -5201,6 +5213,24 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.101.0.tgz", + "integrity": "sha512-cpZA0+WqKXwrwMfiWZEGGF6QrIWVQFbhBtxqDF5sQsAfrFf47HIE6fiPbQU3wyAUEN2+7UNqLCQe7oG6m3f93w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.101.0", + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", diff --git a/package.json b/package.json index 984a9754..5c3a60c4 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "@storybook/react-vite": "^10.4.3", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-query": "^5.101.0", + "@tanstack/react-query-devtools": "^5.101.0", "@tsconfig/node18": "^1.0.1", "@types/chai": "^4.3.0", "@types/chai-as-promised": "^7.1.5", diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 6e7bbfad..293c4a4c 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -1731,6 +1731,7 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i data: payload, type: this.type, name: this.name, + componentId: this.dbComponent.id, from: 'client' }); } diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index cdfd9f91..fb6ac4cd 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -31,7 +31,7 @@ import { setupAuthRoutes } from "./auth.js"; import { setupDeezerRoutes } from "./deezerRoutes.js"; import {setupLZEndpointRoutes} from "./endpointListenbrainzRoutes.js"; import {setupLastfmEndpointRoutes} from "./endpointLastfmRoutes.js"; -import { makeClientCheckMiddle, makeComponentMiddle, makeSourceCheckMiddle } from "./middleware.js"; +import { makeClientCheckMiddle, makeComponentMiddle, makeSourceCheckMiddle, makeSourceNextMiddle, SourceAwareRequest } from "./middleware.js"; import { setupWebscrobblerRoutes } from "./webscrobblerRoutes.js"; import ScrobbleSources from "../sources/ScrobbleSources.js"; import ScrobbleClients from "../scrobblers/ScrobbleClients.js"; @@ -109,6 +109,7 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro const sourceRequiredMiddle = sourceMiddleFunc(true); const componentMiddle = makeComponentMiddle(scrobbleSources, scrobbleClients); + const sourceAwareMiddle = makeSourceNextMiddle(scrobbleSources); const setLogWebSettings: ExpressHandler = async (req, res, next) => { // @ts-expect-error logLevel not part of session @@ -235,6 +236,29 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro return res.json([...sourceData, ...clientData]); }); + app.get('/api/sources/:componentVal/players', sourceAwareMiddle, async (req: SourceAwareRequest, res, next) => { + if(req.component instanceof MemorySource) { + return res.json(req.component.playersToObject()); + } + return res.json({}); + }); + + app.get('/api/sources/:componentVal/players/:platformId', sourceAwareMiddle, async (req: SourceAwareRequest, res, next) => { + if(req.component instanceof MemorySource) { + const { + params: { + platformId + } + } = req; + const player = req.component.players.get(platformId as string); + if(player === undefined) { + return res.status(400).json({error: `No player with platform id ${platformId} exists`}); + } + return res.json(player); + } + return res.json({}); + }); + app.get('/api/status', async (req, res, next) => { const sourceData = scrobbleSources.sources.map((x) => { diff --git a/src/backend/server/middleware.ts b/src/backend/server/middleware.ts index 1de19a57..23e29d0d 100644 --- a/src/backend/server/middleware.ts +++ b/src/backend/server/middleware.ts @@ -99,5 +99,32 @@ export const makeComponentMiddle = (sources: ScrobbleSources, clients: ScrobbleC (req as ComponentAwareRequest).component = component; + next(); +} + +export interface SourceAwareRequest extends Request { + component: AbstractSource +} + +export const makeSourceNextMiddle = (sources: ScrobbleSources): ExpressHandler => async (req: Request, res: Response, next: NextFunction) => { + const { + params: { + componentVal + } + } = req; + + const componentId = Number.parseInt(componentVal as string); + if (isNaN(componentId)) { + return res.status(400).json({ error: 'Source Id must be a number' }); + } + + let component: AbstractSource; + component = sources.sources.find(x => x.componentId === componentId); + if(component === undefined) { + return res.status(404).json({error: `No Source with the Id ${componentId} exists`}); + } + + (req as SourceAwareRequest).component = component; + next(); } \ No newline at end of file diff --git a/src/backend/sources/AbstractSource.ts b/src/backend/sources/AbstractSource.ts index 0ffbb82b..423b52d1 100644 --- a/src/backend/sources/AbstractSource.ts +++ b/src/backend/sources/AbstractSource.ts @@ -805,6 +805,7 @@ export default abstract class AbstractSource extends AbstractComponent implement this.emitter.emit(eventName, { type: this.type, name: this.name, + componentId: this.dbComponent.id, from: 'source', data: payload, }); diff --git a/src/client/AppNext.tsx b/src/client/AppNext.tsx index 13b9881a..eb3e0448 100644 --- a/src/client/AppNext.tsx +++ b/src/client/AppNext.tsx @@ -19,6 +19,8 @@ import Version from "./Version"; import { MSComponentList, MSComponentListFetchable } from './components/msComponent/MSComponentList'; import { Provider } from './components/Provider'; import { Container } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query' +import { ComponentsApiJson, ComponentSourceApiJson } from '../core/Api'; function NoMatch() { const location = useLocation(); @@ -110,10 +112,51 @@ const router = genRouter(); // const ConnectedGlobal = connector(Global); +const Global = (props) => { + + const queryClient = useQueryClient(); + const [sourceEventSource, eventSourceStatus] = useEventSource("/api/events", false); + useEventSourceListener(sourceEventSource, ['source', 'client'], evt => { + const data = JSON.parse(evt.data); + if(data.event === 'playerUpdate') { + + console.log(`Updated => Component ${data.componentId} Player ${data.data.platformId}`); + queryClient.setQueryData(['components', data.componentId, 'players', data.data.platformId], data.data); + + // queryClient.setQueryData(['components'], (old: ComponentsApiJson[]) => { + // const componentIndex = old.findIndex(x => x.id === data.componentId); + // if(componentIndex !== -1) { + // console.log(`Updated => Component ${data.componentId} Player ${data.data.platformId}`); + // //(old[componentIndex] as ComponentSourceApiJson).players[data.data.platformId] = data.data; + // const newData = [...old]; + // newData[componentIndex] = { + // ...old[componentIndex], + // players: { + // // @ts-ignore + // ...old[componentIndex].players, + // [data.data.platformId]: data.data + // } + // } + // return newData; + // } + // //console.log(old); + // }); + } + if(data.from === 'source') { + console.log(data); + } else if(data.from === 'client') { + console.log(data); + } + }, []); + + return ; +} + function App() { return ( + diff --git a/src/client/components/Provider.tsx b/src/client/components/Provider.tsx index e3a5fc02..cbb1af58 100644 --- a/src/client/components/Provider.tsx +++ b/src/client/components/Provider.tsx @@ -12,6 +12,7 @@ import { QueryClient, QueryClientProvider, } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient() @@ -21,6 +22,7 @@ export function Provider(props: ColorModeProviderProps) { + ) } \ No newline at end of file diff --git a/src/client/components/chakraPlayer/Player.tsx b/src/client/components/chakraPlayer/Player.tsx index b9103c90..5b8039f2 100644 --- a/src/client/components/chakraPlayer/Player.tsx +++ b/src/client/components/chakraPlayer/Player.tsx @@ -1,9 +1,13 @@ -import React, { ComponentProps, useMemo, forwardRef, Fragment } from "react" +import React, { ComponentProps, useMemo, forwardRef, Fragment, useEffect } from "react" import { Accordion, Progress, For, Span, Stack, Spacer, Text, Image, Box, Heading, AbsoluteCenter, Button, Separator, HStack, Flex, Center, Badge, IconButton, Container, Collapsible, Card, LinkOverlay, LinkBox } from '@chakra-ui/react'; import { TextMuted } from "../TextMuted"; import { SOURCE_SOT, SOURCE_SOT_TYPES, SourcePlayerJson } from "../../../core/Atomic"; import { timeToHumanTimestamp } from "../../../core/TimeUtils"; import { capitalize } from "../../../core/StringUtils"; +import { QueryFunctionContext, queryOptions, useQuery, useQueryClient } from '@tanstack/react-query'; +import { ErrorAlert } from "../ErrorAlert"; +import ky from 'ky'; +import { baseUrl } from "../../utils"; export interface PlayerProps { data: SourcePlayerJson @@ -81,4 +85,48 @@ export const ChakraPlayer = (props: PlayerProps) => { +} + +export interface ChakraPlayerFetchableProps { + componentId: number + platformId: string + data: SourcePlayerJson + sot?: SOURCE_SOT_TYPES +} + +export const ChakraPlayerFetchable = (props: ChakraPlayerFetchableProps) => { + const { + componentId, + platformId, + data: initData, + sot + } = props; + const queryClient = useQueryClient(); + useEffect(() => { + if(initData !== undefined) { + queryClient.setQueryData(['components', componentId, 'players', platformId], initData); + } + },[initData]); + + const { isPending, isError, data, error } = useQuery({ + queryKey: ['components', componentId, 'players', platformId], + queryFn: queryFn, + structuralSharing: false, + staleTime: Infinity, + }); + + if(isError) { + return + } + + console.log('rendering player'); + + if(!isPending) { + return + } +} + +type PlayerQueryKey = ['components', number, 'players', string]; +const queryFn = async (context: QueryFunctionContext) => { + return await ky.get(`sources/${context.queryKey[1]}/players/${context.queryKey[3]}`, { baseUrl: baseUrl }).json() as SourcePlayerJson; } \ No newline at end of file diff --git a/src/client/components/msComponent/MSComponentList.tsx b/src/client/components/msComponent/MSComponentList.tsx index ffb6f374..dc9cdd6d 100644 --- a/src/client/components/msComponent/MSComponentList.tsx +++ b/src/client/components/msComponent/MSComponentList.tsx @@ -12,6 +12,7 @@ export interface ComponentListProps { } export const MSComponentList = (props: ComponentListProps) => { + console.log('rendering component list'); const [shownType, setShownType] = useState("All"); return ( diff --git a/src/client/components/msComponent/MSComponentSummary.tsx b/src/client/components/msComponent/MSComponentSummary.tsx index a9d25168..31701d31 100644 --- a/src/client/components/msComponent/MSComponentSummary.tsx +++ b/src/client/components/msComponent/MSComponentSummary.tsx @@ -6,7 +6,7 @@ import { isClientType } from "../../../backend/common/infrastructure/Atomic.js"; import { capitalize } from "../../../core/StringUtils.js"; import { ShortDateDisplay } from "../DateDisplay.js"; import { ChevronRightButton } from "../icons/ChakraIcons.js"; -import { ChakraPlayer } from "../chakraPlayer/Player.js"; +import { ChakraPlayer, ChakraPlayerFetchable } from "../chakraPlayer/Player.js"; import { InfoTip } from "../ToggleTip.js"; export const MSComponentSummary = (props: { data: ComponentCommonApiJson }) => { @@ -27,7 +27,7 @@ export const MSComponentSummary = (props: { data: ComponentCommonApiJson }) => { body = ( { - Object.values(players).map((x) => ) + Object.entries(players).map(([key, x]) => ) } ); -- 2.51.2