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

refactor: Move scrobble to top level of play object

FoxxMD (Jun 10, 2026, 1:54 AM UTC) f7577c43 f3531f41

+33 -43
+26 -38
src/backend/scrobblers/AbstractScrobbleClient.ts
··· 821 821 // if no existing transform then we can run cheap db match 822 822 const cheapExisting = await this.playRepo.checkExisting(playObj, {states: ['scrobbled']}); 823 823 if(cheapExisting !== undefined) { 824 - const s: ScrobbledPlayObject = {play: cheapExisting.play, scrobble: cheapExisting.play.meta.lifecycle?.scrobble?.mergedScrobble}; 824 + const s: ScrobbledPlayObject = {play: cheapExisting.play, scrobble: cheapExisting.play.scrobble?.mergedScrobble}; 825 825 return [s, [s]]; 826 826 } 827 827 } ··· 840 840 return hasAcceptableTemporalAccuracy(temporalComparison.match) 841 841 }); 842 842 843 - const s: ScrobbledPlayObject = {play: matchPlayDate, scrobble: matchPlayDate.meta.lifecycle?.scrobble?.mergedScrobble}; 843 + const s: ScrobbledPlayObject = {play: matchPlayDate, scrobble: matchPlayDate.scrobble?.mergedScrobble}; 844 844 845 845 return [s, [s]]; 846 846 } ··· 864 864 try { 865 865 const result = await this.doScrobble(playObj); 866 866 const { 867 - scrobble = {}, 868 - } = playObj.meta.lifecycle; 869 - playObj.meta.lifecycle.scrobble = { 867 + scrobble = {} 868 + } = playObj; 869 + playObj.scrobble = { 870 870 ...scrobble, 871 871 payload: result.payload, 872 872 warnings: result.warnings, ··· 1073 1073 } 1074 1074 if (historicalError === undefined) { 1075 1075 const { summary, ...matchResult } = await this.existingScrobble(currQueuedPlay.play, historicalPlays); 1076 - signal.throwIfAborted(); 1077 - const { 1078 - scrobble = {}, 1079 - ...lifeRest 1080 - } = currQueuedPlay.play.meta.lifecycle ?? { steps: [], original: currQueuedPlay.play }; 1081 - currQueuedPlay.play.meta.lifecycle = { 1082 - ...lifeRest, 1083 - scrobble: { 1084 - ...scrobble, 1085 - match: matchResult 1086 - } 1076 + currQueuedPlay.play.scrobble = { 1077 + ...(currQueuedPlay.play.scrobble ?? {}), 1078 + match: matchResult 1087 1079 } 1080 + signal.throwIfAborted(); 1088 1081 if (!matchResult.match) { 1089 1082 const transformedScrobble = await this.transformPlay(currQueuedPlay.play, TRANSFORM_HOOK.postCompare); 1090 1083 signal.throwIfAborted(); ··· 1099 1092 await this.addScrobbledTrack(scrobbledPlay); 1100 1093 //handledShiftedPlay = true; 1101 1094 } catch (e) { 1102 - currQueuedPlay.play.meta.lifecycle.scrobble = { 1095 + currQueuedPlay.play.scrobble = { 1103 1096 }; 1104 1097 1105 1098 const submitError = findCauseByReference(e, ScrobbleSubmitError); 1106 1099 if (submitError !== undefined) { 1107 - currQueuedPlay.play.meta.lifecycle.scrobble.payload = submitError.payload; 1108 - currQueuedPlay.play.meta.lifecycle.scrobble.response = submitError.responseBody; 1109 - currQueuedPlay.play.meta.lifecycle.scrobble.error = serializeError(submitError); 1100 + currQueuedPlay.play.scrobble.payload = submitError.payload; 1101 + currQueuedPlay.play.scrobble.response = submitError.responseBody; 1102 + currQueuedPlay.play.scrobble.error = serializeError(submitError); 1110 1103 } else { 1111 - currQueuedPlay.play.meta.lifecycle.scrobble.payload = this.playToClientPayload(transformedScrobble); 1112 - currQueuedPlay.play.meta.lifecycle.scrobble.error = serializeError(e); 1104 + currQueuedPlay.play.scrobble.payload = this.playToClientPayload(transformedScrobble); 1105 + currQueuedPlay.play.scrobble.error = serializeError(e); 1113 1106 } 1114 1107 1115 1108 queueError = e; ··· 1295 1288 } 1296 1289 signal?.throwIfAborted(); 1297 1290 const {summary, ...matchResult} = await this.existingScrobble(deadScrobble.play, historicalPlays); 1298 - const { 1299 - scrobble = {}, 1300 - ...lifeRest 1301 - } = deadScrobble.play.meta.lifecycle ?? {steps: [], original: deadScrobble.play}; 1302 - deadScrobble.play.meta.lifecycle = { 1303 - ...lifeRest, 1304 - scrobble: { 1305 - ...scrobble, 1306 - match: matchResult 1307 - } 1291 + deadScrobble.play.scrobble = { 1292 + ...(deadScrobble.play.scrobble ?? {}), 1293 + match: matchResult 1308 1294 } 1309 1295 if(!matchResult.match) { 1310 1296 const transformedScrobble = await this.transformPlay(deadScrobble.play, TRANSFORM_HOOK.postCompare); ··· 1314 1300 await this.addScrobbledTrack(scrobbledPlay); 1315 1301 this.removeDeadLetterScrobble(deadScrobble, 'scrobbled', true); 1316 1302 } catch (e) { 1317 - 1303 + deadScrobble.play.scrobble = { 1304 + ...(deadScrobble.play.scrobble ?? {}) 1305 + } 1318 1306 const submitError = findCauseByReference(e, ScrobbleSubmitError); 1319 1307 if(submitError !== undefined) { 1320 - deadScrobble.play.meta.lifecycle.scrobble.payload = submitError.payload; 1321 - deadScrobble.play.meta.lifecycle.scrobble.response = submitError.responseBody; 1322 - deadScrobble.play.meta.lifecycle.scrobble.error = serializeError(submitError); 1308 + deadScrobble.play.scrobble.payload = submitError.payload; 1309 + deadScrobble.play.scrobble.response = submitError.responseBody; 1310 + deadScrobble.play.scrobble.error = serializeError(submitError); 1323 1311 } else { 1324 - deadScrobble.play.meta.lifecycle.scrobble.payload = this.playToClientPayload(transformedScrobble); 1325 - deadScrobble.play.meta.lifecycle.scrobble.error = serializeError(e); 1312 + deadScrobble.play.scrobble.payload = this.playToClientPayload(transformedScrobble); 1313 + deadScrobble.play.scrobble.error = serializeError(e); 1326 1314 } 1327 1315 1328 1316 this.queueRepo.updateById(deadQueueState.id, {retries: deadQueueState.retries + 1, error: e, updatedAt: dayjs(), queueStatus: 'failed'});
+4 -2
src/core/Atomic.ts
··· 301 301 302 302 export type ErrorLike = Error | ErrorObject; 303 303 304 + /** scrobble action plus the match result before scrobbling */ 304 305 export interface ScrobbleResult<D extends DateLike = Dayjs> { 305 306 match?: PlayMatchResult<D> 306 307 payload?: ScrobblePayload ··· 314 315 //input?: object 315 316 //original?: PlayObjectLifecycleless<D> 316 317 steps: LifecycleStep[] 317 - scrobble?: ScrobbleResult<D> 318 + //scrobble?: ScrobbleResult<D> 318 319 } 319 320 320 321 export interface LifecycleStep { ··· 349 350 reason?: string 350 351 closestMatchedPlay?: PlayObjectLifecycleless<D> 351 352 transformedPlay?: PlayObjectLifecycleless<D> 352 - summary?: String 353 + summary?: string 353 354 createdAt: string 354 355 } 355 356 ··· 371 372 data: PlayData<D>, 372 373 meta: PlayMetaLifecycleless<D> 373 374 original?: PlayOriginal<D> 375 + scrobble?: ScrobbleResult<D> 374 376 } 375 377 376 378 export const isPlayObject = (obj: object): obj is PlayObject => {
+3 -3
src/core/tests/utils/fixtures.ts
··· 124 124 const res = await existingScrobble(play, existingPlays); 125 125 scrobbleRes.match = res; 126 126 if(res.match) { 127 - play.meta.lifecycle.scrobble = scrobbleRes; 127 + play.scrobble = scrobbleRes; 128 128 return play; 129 129 } 130 130 131 131 scrobbleRes.payload = playToListenPayload(play); 132 132 if(error) { 133 133 scrobbleRes.error = new Error('Failed to scrobble to client', {cause: new UpstreamError('Client returned a 400 or something')}); 134 - play.meta.lifecycle.scrobble = scrobbleRes; 134 + play.scrobble = scrobbleRes; 135 135 return play; 136 136 } 137 137 ··· 141 141 142 142 scrobbleRes.response = generateRandomObj(2); 143 143 scrobbleRes.mergedScrobble = lifecyclelessInvariantTransform(play); 144 - play.meta.lifecycle.scrobble = scrobbleRes; 144 + play.scrobble = scrobbleRes; 145 145 146 146 return play; 147 147 }