[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.

poc multi page

FoxxMD (Jun 12, 2026, 5:57 PM UTC) 4bfcc405 892d035d

+177 -9
+21
next/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta 6 + name="description" 7 + content="Dashboard for Multi Scrobbler" 8 + /> 9 + <meta name="theme-color" content="#000000" /> 10 + <link rel="apple-touch-icon" href="/logo192.png" /> 11 + <link rel="icon" type="image/svg+xml" href="/icon.svg" /> 12 + <link rel="manifest" href="/manifest.json" /> 13 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 14 + <title>Multi Scrobbler</title> 15 + </head> 16 + <body> 17 + <noscript>You need to enable JavaScript to run this app. aaaa</noscript> 18 + <div id="root"></div> 19 + <script type="module" src="/src/client/index-next.tsx"></script> 20 + </body> 21 + </html>
+3 -1
src/backend/server/index.ts
··· 82 82 process.env.USE_HASH_ROUTER = root.get('isSubPath').toString(); 83 83 } 84 84 85 - 85 + app.get(/^\/next$/, (_, res) => { 86 + res.redirect("/next/") 87 + }); 86 88 87 89 const viteExpressOptions: Parameters<typeof ViteExpress.config>[0] = { 88 90 mode: isProd ? 'production' : 'development',
+120
src/client/AppNext.tsx
··· 1 + import React from 'react'; 2 + import { 3 + createBrowserRouter, 4 + createHashRouter, RouteObject, 5 + RouterProvider, useLocation, 6 + } from "react-router-dom"; 7 + import {connect, ConnectedProps, Provider as ReduxProvider} from 'react-redux' 8 + import './App.css'; 9 + import CopyToClipboard from "./components/CopyToClipboard"; 10 + import ExternalLink from "./components/ExternalLink"; 11 + import {store} from './store'; 12 + import Dashboard from "./dashboard/dashboard"; 13 + import RecentPage from "./recent/RecentPage"; 14 + import ScrobbledPage from "./scrobbled/ScrobbledPage"; 15 + import DeadPage from "./deadLetter/DeadPage"; 16 + import {clientUpdate, sourceUpdate} from "./status/ducks"; 17 + import {useEventSource, useEventSourceListener} from "@react-nano/use-event-source"; 18 + import Version from "./Version"; 19 + import { MSComponentList } from './components/msComponent/MSComponentList'; 20 + import { Provider } from './components/Provider'; 21 + 22 + function NoMatch() { 23 + const location = useLocation(); 24 + 25 + return ( 26 + <div> 27 + No page for <code>{location.pathname}</code> exists! 28 + </div> 29 + ); 30 + } 31 + 32 + // https://tailwindflex.com/@sienna/copy-code-block 33 + function MissingDocs() { 34 + return ( 35 + <div> 36 + <div>Oops! You need to build docs first. Run the following commands to build:</div> 37 + <code 38 + className="mt-5 text-sm sm:text-base inline-flex text-left items-center space-x-4 bg-gray-900 text-white rounded-lg p-4 pl-6"> 39 + <span className="flex gap-4"> 40 + <span className="shrink-0 text-gray-500"> 41 + $ 42 + </span> 43 + 44 + <span className="flex-1"> 45 + <span> 46 + npm run <span className="text-yellow-500">docs:install</span> && npm run <span 47 + className="text-yellow-500">docs:build</span> 48 + </span> 49 + </span> 50 + </span> 51 + 52 + <CopyToClipboard text="npm run docs:install && npm run docs:build"/> 53 + </code> 54 + </div> 55 + ); 56 + } 57 + 58 + const routes: RouteObject[] = [ 59 + { 60 + path: "/next", 61 + element: <MSComponentList components={[]}/>, 62 + }, 63 + { 64 + path: "/docs", 65 + element: <MissingDocs/> 66 + }, 67 + { 68 + path: "*", 69 + element: <NoMatch/> 70 + } 71 + ]; 72 + 73 + const genRouter = () => { 74 + const useHashRouter = __USE_HASH_ROUTER__ === 'true'; 75 + return useHashRouter ? createHashRouter(routes) : createBrowserRouter(routes); 76 + } 77 + 78 + const router = genRouter(); 79 + 80 + // const mapDispatchToProps = (dispatch) => { 81 + // return { 82 + // updateSource: (payload) => dispatch(sourceUpdate(payload)), 83 + // updateClient: (payload) => dispatch(clientUpdate(payload)) 84 + // } 85 + // } 86 + 87 + // const connector = connect(null, mapDispatchToProps); 88 + 89 + // type PropsFromRedux = ConnectedProps<typeof connector>; 90 + 91 + // const Global = (props: PropsFromRedux) => { 92 + // const { 93 + // updateSource, 94 + // updateClient 95 + // } = props; 96 + 97 + // const [sourceEventSource, eventSourceStatus] = useEventSource("api/events", false); 98 + // useEventSourceListener(sourceEventSource, ['source', 'client'], evt => { 99 + // const data = JSON.parse(evt.data); 100 + // if(data.from === 'source') { 101 + // updateSource(data); 102 + // } else if(data.from === 'client') { 103 + // updateClient(data); 104 + // } 105 + // }, [updateSource, updateClient]); 106 + 107 + // return <span/>; 108 + // } 109 + 110 + // const ConnectedGlobal = connector(Global); 111 + 112 + function App() { 113 + return ( 114 + <Provider> 115 + <RouterProvider router={router}/> 116 + </Provider> 117 + ); 118 + } 119 + 120 + export default App;
+18
src/client/index-next.tsx
··· 1 + import React from 'react'; 2 + import ReactDOM from 'react-dom/client'; 3 + import './index.css'; 4 + import App from './AppNext.js'; 5 + import dayjs from 'dayjs'; 6 + import utc from 'dayjs/plugin/utc.js'; 7 + import relativeTime from 'dayjs/plugin/relativeTime.js'; 8 + dayjs.extend(utc) 9 + dayjs.extend(relativeTime); 10 + 11 + const root = ReactDOM.createRoot( 12 + document.getElementById('root') as HTMLElement 13 + ); 14 + root.render( 15 + <React.StrictMode> 16 + <App /> 17 + </React.StrictMode> 18 + );
+15 -8
vite.config.ts
··· 2 2 import normalizeUrl from "normalize-url"; 3 3 import { defineConfig } from 'vite'; 4 4 import tailwindcss from '@tailwindcss/vite' 5 + import { dirname, resolve } from 'node:path' 5 6 6 7 const QUOTES_UNWRAP_REGEX: RegExp = new RegExp(/^"(.*)"$/); 7 8 export const generateBaseURL = (userUrl: string | undefined): URL => { 8 9 let cleanUserUrl = userUrl.trim(); 9 - if(QUOTES_UNWRAP_REGEX.test(cleanUserUrl)) { 10 + if (QUOTES_UNWRAP_REGEX.test(cleanUserUrl)) { 10 11 const results = cleanUserUrl.match(QUOTES_UNWRAP_REGEX); 11 12 cleanUserUrl = results[1]; 12 13 } 13 - const base = normalizeUrl(cleanUserUrl, {removeSingleSlash: true}); 14 + const base = normalizeUrl(cleanUserUrl, { removeSingleSlash: true }); 14 15 const u = new URL(base); 15 - if(u.port === '') { 16 - if(u.protocol === 'https:') { 16 + if (u.port === '') { 17 + if (u.protocol === 'https:') { 17 18 u.port = '443'; 18 - } else if(userUrl.includes(`${u.hostname}:80`)) { 19 + } else if (userUrl.includes(`${u.hostname}:80`)) { 19 20 u.port = '80'; 20 21 } 21 22 } ··· 24 25 25 26 export default defineConfig(() => { 26 27 let baseUrlStr = '/'; 27 - if(process.env.BASE_URL !== undefined && process.env.BASE_URL !== '') { 28 + if (process.env.BASE_URL !== undefined && process.env.BASE_URL !== '') { 28 29 const baseUrl = generateBaseURL(process.env.BASE_URL); 29 - if(baseUrl.pathname !== '/') { 30 + if (baseUrl.pathname !== '/') { 30 31 baseUrlStr = baseUrl.toString(); 31 32 } 32 33 } ··· 48 49 ], 49 50 build: { 50 51 sourcemap: true, 51 - cssCodeSplit: true 52 + cssCodeSplit: true, 53 + rolldownOptions: { 54 + input: { 55 + main: resolve(import.meta.dirname, 'index.html'), 56 + next: resolve(import.meta.dirname, 'next/index.html'), 57 + }, 58 + }, 52 59 }, 53 60 define: { 54 61 "__USE_HASH_ROUTER__": JSON.stringify((process.env.USE_HASH_ROUTER ?? false))