[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(scrobbler): Add migrated play to queue

* Utilize existing methods for added queued/dead scrobbled when migrating from cache
* Using queueScrobble fixes migration not creating queued state
* Using addDeadLetterScrobble reduces duplicated code in migration
* Drop listenranges to avoid bad cache data issue (potentially)

FoxxMD (May 27, 2026, 7:19 PM UTC) 34e0eaef 6d4b6174

+38 -36
+38 -36
src/backend/scrobblers/AbstractScrobbleClient.ts
··· 579 579 lifecycle, 580 580 ...metaRest 581 581 }, 582 + data: { 583 + listenRanges, 584 + artists, 585 + albumArtists, 586 + ...dataRest 587 + } = {}, 582 588 } = play; 583 589 try { 584 - const res = await this.playRepo.createPlays([ 585 - playToRepositoryCreatePlayOpts({ 586 - play: { 587 - ...play, 588 - data: { 589 - ...play.data, 590 - artists: play.data?.artists === undefined ? undefined : artistNamesToCredits(play.data?.artists as unknown as string[]), 591 - albumArtists: play.data?.albumArtists === undefined ? undefined : artistNamesToCredits(play.data?.albumArtists as unknown as string[]) 592 - }, 593 - meta: { 594 - ...metaRest, 595 - lifecycle: { 596 - steps: [] 597 - } 598 - } 599 - }, 600 - componentId: this.dbComponent.id, 601 - state: 'queued', 602 - parentId: play.id 603 - }) 604 - ]); 605 - logger.verbose(`Migrated Play ${res[0].uid} => ${buildTrackString(play)}`); 590 + const updatedPlay: PlayObject = { 591 + ...play, 592 + data: { 593 + artists: artists === undefined ? undefined : artistNamesToCredits(artists as unknown as string[]), 594 + albumArtists: albumArtists === undefined ? undefined : artistNamesToCredits(albumArtists as unknown as string[]), 595 + ...dataRest 596 + }, 597 + meta: { 598 + ...metaRest, 599 + lifecycle: { 600 + steps: [] 601 + } 602 + } 603 + } 604 + // return play object without going through transform since it was (presumably) already transformed before being cached 605 + const res = await this.queueScrobble(updatedPlay, updatedPlay.meta.source, async (x) => x); 606 + if(res.length === 1) { 607 + logger.verbose(`Migrated Play ${res[0].uid} => ${buildTrackString(play)}`); 608 + } 606 609 cachedQueuedScrobble.play.meta.migrated = true; 607 610 migratedQueue.push(cachedQueuedScrobble) 608 611 } catch (e) { ··· 633 636 lifecycle, 634 637 ...metaRest 635 638 }, 639 + data: { 640 + listenRanges, 641 + artists, 642 + albumArtists, 643 + ...dataRest 644 + } = {}, 636 645 } = play; 637 646 try { 638 647 const res = await this.playRepo.createPlays([ ··· 640 649 play: { 641 650 ...play, 642 651 data: { 643 - ...play.data, 644 - artists: play.data?.artists === undefined ? undefined : artistNamesToCredits(play.data?.artists as unknown as string[]), 645 - albumArtists: play.data?.albumArtists === undefined ? undefined : artistNamesToCredits(play.data?.albumArtists as unknown as string[]) 652 + artists: artists === undefined ? undefined : artistNamesToCredits(artists as unknown as string[]), 653 + albumArtists: albumArtists === undefined ? undefined : artistNamesToCredits(albumArtists as unknown as string[]), 654 + ...dataRest 646 655 }, 647 656 meta: { 648 657 ...metaRest, ··· 657 666 }) 658 667 ]); 659 668 logger.verbose(`Added Play ${res[0].uid} to database => ${buildTrackString(play)}`); 660 - await this.queueRepo.create({ 661 - componentId: this.dbComponent.id, 662 - playId: res[0].id, 663 - queueName: CLIENT_DEAD_QUEUE, 664 - queueStatus: 'queued', 665 - retries: cDeadScrobble.retries, 666 - error: cDeadScrobble.error !== undefined ? { message: cDeadScrobble.error } : undefined 667 - }); 669 + await this.addDeadLetterScrobble(res[0], cDeadScrobble.error); 670 + logger.verbose(`Added Play ${res[0].uid} to Failed Queue`); 668 671 cDeadScrobble.play.meta.migrated = true; 669 672 migratedDead.push(cDeadScrobble); 670 - logger.verbose(`Added Play ${res[0].uid} to Failed Queue`); 671 673 } catch (e) { 672 674 migratedDead.push(cDeadScrobble); 673 675 allGood = false; ··· 1394 1396 await this.updateQueueStats([CLIENT_DEAD_QUEUE]); 1395 1397 } 1396 1398 1397 - queueScrobble = async (data: PlayObject | PlayObject[], source: string) => { 1399 + queueScrobble = async (data: PlayObject | PlayObject[], source: string, transformFunc?: (x: PlayObject) => Promise<PlayObject>) => { 1398 1400 const playDatas = (Array.isArray(data) ? data : [data]).map(x => ({...x, meta: {...x.meta, seenAt: dayjs()}})); 1399 1401 1400 1402 const createdQueuedPlays: PlaySelect[] = []; 1401 1403 1402 - for await(const play of pMapIterable(playDatas, this.staggerMappers.preCompare(async x => await this.transformPlay(x, TRANSFORM_HOOK.preCompare)), {concurrency: 3})) { 1404 + for await(const play of pMapIterable(playDatas, this.staggerMappers.preCompare(async x => transformFunc !== undefined ? await transformFunc(x) : await this.transformPlay(x, TRANSFORM_HOOK.preCompare)), {concurrency: 3})) { 1403 1405 try { 1404 1406 // cheap check, looks for play data (non-meta) hash, playdate, and optionally mbid recording 1405 1407 const cheapExisting = await this.playRepo.checkExisting(play, {queueName: CLIENT_INGRESS_QUEUE});