[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(client): Make playing now scheduling part of heartbeat

FoxxMD (May 6, 2026, 8:06 PM UTC) 630edd7b 092cf907

+26 -17
+24 -15
src/backend/scrobblers/AbstractScrobbleClient.ts
··· 118 118 deadLetterQueued: number = 0; 119 119 120 120 supportsNowPlaying: boolean = false; 121 + nowPlayingInit: boolean = false; 121 122 nowPlayingEnabled: boolean; 122 123 nowPlayingFilter: (queue: NowPlayingQueue) => SourcePlayerObj | undefined; 123 124 nowPlayingMinThreshold: NowPlayingUpdateThreshold = (_) => 10; ··· 234 235 await this.tryStopScrobbling(); 235 236 } 236 237 237 - public initHeartbeat() { 238 + public initHeartbeat(opts: {deadDelay?: number} = {}) { 238 239 if(this.scheduler.existsById('heartbeat') === false) { 239 240 this.logger.info('Adding Heartbeat Task and running immediately'); 240 241 this.scheduler.addSimpleIntervalJob(new SimpleIntervalJob({ ··· 255 256 this.logger.warn('Heartbeat task is already added to scheduler.'); 256 257 } 257 258 259 + this.initializeNowPlayingSchedule(); 260 + 258 261 if(this.scheduler.existsById('dead') === false && this.initDeadTimeout === undefined) { 259 262 this.logger.verbose('Delaying Dead Scrobbler Processing Task by 2 minutes'); 260 263 this.initDeadTimeout = setTimeout(() => { ··· 278 281 } 279 282 ), {id: 'dead'})); 280 283 // delay for 2 minutes 281 - }, 120 * 1000); 284 + }, (opts.deadDelay ?? 120) * 1000); 282 285 283 286 } else { 284 287 if(this.initDeadTimeout !== undefined) { ··· 424 427 } 425 428 426 429 this.initializeNowPlayingFilter(); 427 - this.initializeNowPlayingSchedule(); 430 + this.nowPlayingInit = true; 428 431 } else { 429 432 this.npLogger.debug('Unsupported feature, disabled.'); 430 433 } ··· 432 435 433 436 protected initializeNowPlayingSchedule() { 434 437 435 - const t = new AsyncTask('Playing Now', (): Promise<any> => { 436 - return this.processingPlayingNow(); 437 - }, (err: Error) => { 438 - this.npLogger.error(new Error('Unexpected error while processing Now Playing queue', {cause: err})); 439 - }); 438 + if(this.scheduler.existsById('pn_task') === false) { 439 + const t = new AsyncTask('Playing Now', (): Promise<any> => { 440 + return this.processingPlayingNow(); 441 + }, (err: Error) => { 442 + this.npLogger.error(new Error('Unexpected error while processing Now Playing queue', {cause: err})); 443 + }); 440 444 441 - this.scheduler.removeById('pn_task'); 442 - 443 - // even though we are processing every 5 seconds the interval that Now Playing is updated at, and that the queue is cleared on, 444 - // is still set by shouldUpdatePlayingNow() 445 - // 5 seconds makes sure our granularity for updates is decently fast *when* we do need to actually update 446 - this.scheduler.addSimpleIntervalJob(new SimpleIntervalJob({milliseconds: this.nowPlayingTaskInterval}, t, {id: 'pn_task'})); 445 + // even though we are processing every 5 seconds the interval that Now Playing is updated at, and that the queue is cleared on, 446 + // is still set by shouldUpdatePlayingNow() 447 + // 5 seconds makes sure our granularity for updates is decently fast *when* we do need to actually update 448 + this.scheduler.addSimpleIntervalJob(new SimpleIntervalJob({milliseconds: this.nowPlayingTaskInterval}, t, {id: 'pn_task'})); 449 + } 447 450 } 448 451 449 452 protected initializeNowPlayingFilter() { ··· 1433 1436 } 1434 1437 1435 1438 processingPlayingNow = async (): Promise<void> => { 1436 - if(this.supportsNowPlaying && this.nowPlayingEnabled) { 1439 + if(!this.supportsNowPlaying || !this.isReady()) { 1440 + return; 1441 + } 1442 + if(this.nowPlayingInit === false) { 1443 + this.initializeNowPlaying(); 1444 + } 1445 + if(this.nowPlayingEnabled) { 1437 1446 const sourcePlayerData = this.nowPlayingFilter(this.nowPlayingQueue); 1438 1447 if(sourcePlayerData === undefined) { 1439 1448 return;
+2 -2
src/backend/tests/scrobbler/scrobblers.test.ts
··· 1040 1040 await using npScrobbler = new NowPlayingScrobbler(); 1041 1041 npScrobbler.nowPlayingTaskInterval = 10; 1042 1042 await npScrobbler.initialize(); 1043 - npScrobbler.scheduler.startById('pn_task'); 1043 + npScrobbler.initHeartbeat(); 1044 1044 1045 1045 await npScrobbler.queuePlayingNow(generateSourcePlayerObj({play:generatePlay({}, {deviceId: genGroupIdStr(generatePlayPlatformId())})}), {type: 'jellyfin', name: 'test'}); 1046 1046 ··· 1054 1054 await using npScrobbler = new NowPlayingScrobbler(); 1055 1055 npScrobbler.nowPlayingTaskInterval = 10; 1056 1056 await npScrobbler.initialize(); 1057 - npScrobbler.scheduler.startById('pn_task'); 1057 + npScrobbler.initHeartbeat(); 1058 1058 1059 1059 const now = dayjs(); 1060 1060