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]) => )
}
);