[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: Implement start/restart/force init from UI regardless of status

FoxxMD (Dec 11, 2024, 6:20 PM UTC) 323004a7 626bca99

+79 -28
+26 -12
src/backend/server/api.ts
··· 20 20 import AbstractScrobbleClient from "../scrobblers/AbstractScrobbleClient.js"; 21 21 import AbstractSource from "../sources/AbstractSource.js"; 22 22 import MemorySource from "../sources/MemorySource.js"; 23 - import { sortByNewestPlayDate } from "../utils.js"; 23 + import { parseBool, sortByNewestPlayDate } from "../utils.js"; 24 24 import { setupAuthRoutes } from "./auth.js"; 25 25 import { setupDeezerRoutes } from "./deezerRoutes.js"; 26 26 import { setupJellyfinRoutes } from "./jellyfinRoutes.js"; ··· 417 417 return res.json(result); 418 418 }); 419 419 420 - app.use('/api/poll', sourceRequiredMiddle); 421 - app.getAsync('/api/poll', async (req, res) => { 420 + app.use('/api/source/init', sourceRequiredMiddle); 421 + app.postAsync('/api/source/init', async (req, res) => { 422 422 // @ts-expect-error TS(2339): Property 'scrobbleSource' does not exist on type '... Remove this comment to see the full error message 423 423 const source = req.scrobbleSource as AbstractSource; 424 - source.logger.verbose('User requested (re)start via API call'); 425 424 426 - if (!source.canPoll) { 427 - source.logger.warn(`Does not support polling (${source.type})`); 428 - return res.status(400).send(`Specified source cannot poll (${source.type})`); 429 - } 425 + const { 426 + query: { 427 + force: forceQ = false 428 + } 429 + } = req; 430 + 431 + const force = parseBool(forceQ, false); 432 + 433 + source.logger.verbose(`User requested${force ? ' a FORCED' :''} (re)init via API call`); 430 434 431 435 res.status(200).send('OK'); 436 + 432 437 if(source.polling) { 433 438 source.logger.info('Source is already polling! Restarting polling...'); 434 439 const stopRes = await source.tryStopPolling(); 435 440 if(stopRes === true) { 436 - source.poll(); 441 + source.poll({force, notify: false}).catch(e => source.logger.error(e)); 437 442 } 438 443 } else { 439 - source.poll(); 444 + source.poll({force, notify: false}).catch(e => source.logger.error(e)); 440 445 } 441 446 }); 442 447 ··· 444 449 app.postAsync('/api/client/init', async (req, res) => { 445 450 // @ts-expect-error TS(2339): Property 'scrobbleSource' does not exist on type '... Remove this comment to see the full error message 446 451 const client = req.scrobbleClient as AbstractScrobbleClient; 447 - client.logger.verbose('User requested (re)start via API call'); 452 + 453 + const { 454 + query: { 455 + force: forceQ = false 456 + } 457 + } = req; 458 + 459 + const force = parseBool(forceQ, false); 460 + 461 + client.logger.verbose(`User requested${force ? ' a FORCED' :''} (re)init via API call`); 448 462 449 463 client.logger.info('Checking (and trying) to stop scrobbler if already running...'); 450 464 if(false === (await client.tryStopScrobbling())) { ··· 452 466 } 453 467 454 468 client.logger.info('Trying to start scrobbler...'); 455 - await client.initScrobbleMonitoring(); 469 + client.initScrobbleMonitoring({force, notify: false}).catch(e => client.logger.error(e)); 456 470 res.status(200).send('OK'); 457 471 }); 458 472
+6 -2
src/client/components/statusCard/ClientStatusCard.tsx
··· 40 40 41 41 const [startClientPut, startResult] = useStartClientMutation(); 42 42 43 - const tryStart = useCallback((name: string) => startClientPut({name}), [startClientPut]); 43 + const tryStart = useCallback((name: string, force?: boolean) => startClientPut({name, force}), [startClientPut]); 44 44 45 45 let header: string | undefined = display; 46 46 let body = <SkeletonParagraph/>; 47 - const startClientElement = <div onClick={() => tryStart(name)} className="capitalize underline cursor-pointer">{status === 'Running' ? 'Restart' : 'Start'}</div> 47 + const startClientElement = ( 48 + <Fragment> 49 + <div onClick={() => tryStart(name)} className="capitalize underline cursor-pointer inline mr-1">{status === 'Running' ? 'Restart' : 'Start'}</div> 50 + (<div onClick={() => tryStart(name, true)} className="capitalize underline cursor-pointer inline">Force</div>) 51 + </Fragment>) 48 52 if(data !== undefined) { 49 53 const { 50 54 hasAuth,
+15 -11
src/client/components/statusCard/SourceStatusCard.tsx
··· 6 6 import {RootState} from "../../store"; 7 7 import {connect, ConnectedProps} from "react-redux"; 8 8 import Player from "../player/Player"; 9 + import {useStartSourceMutation} from "./sourceDucks"; 9 10 import './statusCard.scss'; 10 11 11 12 export interface SourceStatusCardData extends StatusCardSkeletonData, PropsFromRedux { ··· 30 31 data: { 31 32 display, 32 33 name, 34 + type, 33 35 status, 34 36 } = {} 35 37 } = props; 36 38 let header: string | undefined = display; 37 39 let body = <SkeletonParagraph/>; 38 - const poll = useCallback(async () => { 39 - const params = new URLSearchParams({type: data.type, name: data.name}); 40 - await fetch(`/api/poll?${params}`, { 41 - method: 'GET', 42 - }); 43 - },[data]); 44 - let startSourceElement = null; 40 + 41 + const [startPut, startResult] = useStartSourceMutation(); 42 + 43 + const tryStart = useCallback((name: string, type: string, force?: boolean) => startPut({name, type, force}), [startPut]); 44 + 45 + let startSourceElement = (<Fragment> 46 + <div onClick={() => tryStart(name, type)} 47 + className="capitalize underline cursor-pointer inline mr-1">{status === 'Polling' ? 'Restart' : 'Start'} 48 + </div> 49 + (<div onClick={() => tryStart(name, type, true)} 50 + className="capitalize underline cursor-pointer inline">Force 51 + </div>) 52 + </Fragment>); 45 53 if(data !== undefined) 46 54 { 47 55 const { ··· 69 77 let upstreamRecent = null; 70 78 if(supportsUpstreamRecentlyPlayed && (!hasAuth || authed)) { 71 79 upstreamRecent = <div><Link to={`/recent?type=${type}&name=${name}&upstream=1`}>See Recent from Source API</Link></div>; 72 - } 73 - 74 - if((!hasAuth || authed) && canPoll) { 75 - startSourceElement = <div onClick={poll} className="capitalize underline cursor-pointer">{status === 'Polling' ? 'Restart' : 'Start'}</div> 76 80 } 77 81 78 82 // TODO links
+4 -2
src/client/components/statusCard/clientDucks.ts
··· 5 5 baseQuery: fetchBaseQuery({baseUrl: '/api/'}), 6 6 endpoints: (builder) => ({ 7 7 startClient: builder.mutation<undefined, { 8 - name: string 8 + name: string, 9 + force?: boolean 9 10 }>({ 10 11 query: (params) => ({ 11 12 url: '/client/init', 12 13 method: 'POST', 13 14 params: { 14 - name: params.name 15 + name: params.name, 16 + force: params.force 15 17 } 16 18 }) 17 19 })
+25
src/client/components/statusCard/sourceDucks.ts
··· 1 + import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react"; 2 + 3 + export const sourceApi = createApi({ 4 + reducerPath: 'sourceApi', 5 + baseQuery: fetchBaseQuery({baseUrl: '/api/'}), 6 + endpoints: (builder) => ({ 7 + startSource: builder.mutation<undefined, { 8 + name: string, 9 + type: string, 10 + force?: boolean 11 + }>({ 12 + query: (params) => ({ 13 + url: '/source/init', 14 + method: 'POST', 15 + params: { 16 + name: params.name, 17 + type: params.type, 18 + force: params.force 19 + } 20 + }) 21 + }) 22 + }) 23 + }); 24 + 25 + export const {useStartSourceMutation} = sourceApi;
+3 -1
src/client/store.ts
··· 2 2 // Or from '@reduxjs/toolkit/query/react' 3 3 import { setupListeners } from '@reduxjs/toolkit/query' 4 4 import { scrobblerApi } from './components/statusCard/clientDucks'; 5 + import { sourceApi } from './components/statusCard/sourceDucks'; 5 6 import { deadApi, deadSlice } from "./deadLetter/deadLetterDucks"; 6 7 import { logsReducer } from "./logs/logDucks"; 7 8 import { logsApi } from "./logs/logsApi"; ··· 20 21 [deadApi.reducerPath]: deadApi.reducer, 21 22 [scrobbledApi.reducerPath]: scrobbledApi.reducer, 22 23 [scrobblerApi.reducerPath]: scrobblerApi.reducer, 24 + [sourceApi.reducerPath]: sourceApi.reducer, 23 25 [versionApi.reducerPath]: versionApi.reducer, 24 26 //parts: statusReducer 25 27 clients: clientSlice.reducer, ··· 30 32 // Adding the api middleware enables caching, invalidation, polling, 31 33 // and other useful features of `rtk-query`. 32 34 middleware: (getDefaultMiddleware) => 33 - getDefaultMiddleware().concat([statusApi.middleware, logsApi.middleware, recentApi.middleware, scrobbledApi.middleware, deadApi.middleware, scrobblerApi.middleware, versionApi.middleware]), 35 + getDefaultMiddleware().concat([statusApi.middleware, logsApi.middleware, recentApi.middleware, scrobbledApi.middleware, deadApi.middleware, scrobblerApi.middleware, sourceApi.middleware, versionApi.middleware]), 34 36 }) 35 37 36 38 // optional, but required for refetchOnFocus/refetchOnReconnect behaviors