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

Fix counts for scrobbles/dead

FoxxMD (May 6, 2026, 2:56 AM UTC) 22c09165 cbe996db

+45 -25
+1 -1
src/backend/scrobblers/AbstractScrobbleClient.ts
··· 244 244 protected async postDatabase(): Promise<void> { 245 245 this.playRepo.componentId = this.dbComponent.id; 246 246 this.queueRepo.componentId = this.dbComponent.id; 247 - this.tracksScrobbled = this.dbComponent.countLive; 247 + this.tracksScrobbled = this.dbComponent.countLive + this.dbComponent.countNonLive; 248 248 this.queuedLength = await this.queueRepo.getQueueCount(this.dbComponent.id, [CLIENT_INGRESS_QUEUE]); 249 249 this.queuedGauge.labels(this.getPrometheusLabels()).set(this.queuedLength); 250 250 this.deadLetterLength = await this.queueRepo.getQueueCount(this.dbComponent.id, [CLIENT_DEAD_QUEUE], ['queued', 'failed']);
+36 -24
src/backend/server/api.ts
··· 239 239 hasAuthInteraction: requiresAuthInteraction, 240 240 authed, 241 241 initialized: x.isReady(), 242 - deadLetterScrobbles: x.deadLetterLength, // x.deadLetterScrobbles.length, 242 + deadLetterScrobbles: x.deadLetterQueued, // x.deadLetterScrobbles.length, 243 243 queued: x.queuedLength // x.queuedScrobbles.length 244 244 }; 245 245 if (!base.initialized) { ··· 326 326 const { 327 327 // @ts-expect-error TS(2339): Property 'scrobbleSource' does not exist on type '... Remove this comment to see the full error message 328 328 scrobbleClient: client, 329 - query 329 + query = {} 330 330 } = req; 331 331 332 + const deadQuery: QueryPlaysOpts = { 333 + ...query as Partial<QueryPlaysOpts>, 334 + queues: [ 335 + { 336 + queueName: CLIENT_DEAD_QUEUE, 337 + queueStatus: 'queued' 338 + } 339 + ] 340 + } 341 + 332 342 // @ts-ignore 333 - const result: DeadLetterScrobble<PlayObject>[] = (await (client as AbstractScrobbleClient).getPlaysPaginated(query as Partial<QueryPlaysOpts>)).data.map(x => playSelectToDeadScrobble); 343 + const result: DeadLetterScrobble<PlayObject>[] = (await (client as AbstractScrobbleClient).getPlaysPaginated(deadQuery)).data.map(playSelectToDeadScrobble); 334 344 335 345 return res.json(result); 336 346 }); ··· 361 371 362 372 (client as AbstractScrobbleClient).logger.verbose(`User requested processing of dead letter scrobble ${deadId} via API call`) 363 373 364 - const deadScrobble = (client as AbstractScrobbleClient).deadLetterScrobbles.find(x => x.id === deadId); 365 - 366 - if(deadScrobble === undefined) { 367 - (client as AbstractScrobbleClient).logger.debug(`No dead letter scrobble with ID ${deadId}`) 368 - return res.status(404).send(); 369 - } 370 - 371 - const [scrobbled, dead] = await ((client as AbstractScrobbleClient).processDeadLetterScrobble(undefined, deadId)); 372 - 373 - if(scrobbled) { 374 - return res.status(200).send(); 374 + try { 375 + const [scrobbled, dead] = await (client as AbstractScrobbleClient).processDeadLetterScrobble(undefined, deadId); 376 + if(scrobbled) { 377 + return res.status(200).send(); 378 + } 379 + return res.json(playSelectToDeadScrobble(dead)); 380 + } catch (e) { 381 + if(e.message.includes(`Play ${deadId} does not exist`)) { 382 + logger.warn(e); 383 + return res.status(404).json({error: e}); 384 + } 385 + logger.error(e); 386 + return res.status(500).json({error: e}); 375 387 } 376 - 377 - return res.json(playSelectToDeadScrobble(dead)); 378 388 }); 379 389 380 390 app.delete('/api/dead', clientMiddleFunc(true), async (req, res, next) => { ··· 403 413 404 414 (client as AbstractScrobbleClient).logger.verbose(`User requested removal of dead letter scrobble ${deadId} via API call`) 405 415 406 - const deadScrobble = (client as AbstractScrobbleClient).deadLetterScrobbles.find(x => x.id === deadId); 407 - 408 - if(deadScrobble === undefined) { 409 - (client as AbstractScrobbleClient).logger.verbose(`No dead letter scrobble with ID ${deadId}`) 410 - return res.status(404).send(); 416 + try { 417 + await (client as AbstractScrobbleClient).removeDeadLetterScrobble(deadId,'failed', false); 418 + return res.status(200).send(); 419 + } catch (e) { 420 + if(e.message.includes(`Play ${deadId} does not exist`)) { 421 + logger.warn(e); 422 + return res.status(404).json({error: e}); 423 + } 424 + logger.error(e); 425 + return res.status(500).json({error: e}); 411 426 } 412 - 413 - (client as AbstractScrobbleClient).removeDeadLetterScrobble(deadId,'failed', false); 414 - return res.status(200).send(); 415 427 }); 416 428 417 429 app.get('/api/scrobbled', clientMiddleFunc(false), async (req, res, next) => {
+8
src/client/status/ducks.ts
··· 107 107 } 108 108 ) 109 109 .addMatcher( 110 + (action) => clientUpdate.match(action) && action.payload.event === 'removeDeadLetter', 111 + (state, action) => { 112 + if(state.entities[action.payload.id] !== undefined) { 113 + state.entities[action.payload.id].deadLetterScrobbles--; 114 + } 115 + } 116 + ) 117 + .addMatcher( 110 118 (action) => clientUpdate.match(action) && action.payload.event === 'statusChange', 111 119 (state, action) => { 112 120 if(state.entities[action.payload.id] !== undefined) {