[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(listenbrainz): Move parsing functions out of static

FoxxMD (Nov 13, 2025, 3:43 PM UTC) fa4bf5ab 83d9af2a

+43 -43
+28 -28
src/backend/common/vendor/ListenbrainzApiClient.ts
··· 206 206 207 207 try { 208 208 const resp = await this.getUserListens(maxTracks, user); 209 - return resp.listens.map(x => ListenbrainzApiClient.listenResponseToPlay(x)); 209 + return resp.listens.map(x => listenResponseToPlay(x)); 210 210 } catch (e) { 211 211 this.logger.error(`Error encountered while getting User listens | Error => ${e.message}`); 212 212 return []; ··· 248 248 } 249 249 } 250 250 251 - static listenPayloadToPlay(payload: ListenPayload, nowPlaying: boolean = false): PlayObject { 251 + static submitToPlayObj(submitObj: SubmitPayload, playObj: PlayObject): PlayObject { 252 + if (submitObj.payload.length > 0) { 253 + const respPlay = { 254 + ...playObj, 255 + }; 256 + respPlay.data = { 257 + ...playObj.data, 258 + album: submitObj.payload[0].track_metadata?.release_name ?? playObj.data.album, 259 + track: submitObj.payload[0].track_metadata?.track_name ?? playObj.data.album, 260 + }; 261 + return respPlay; 262 + } 263 + return playObj; 264 + } 265 + 266 + static formatPlayObj(obj: any, options: FormatPlayObjectOptions): PlayObject { 267 + return listenResponseToPlay(obj); 268 + } 269 + } 270 + 271 + export const listenPayloadToPlay = (payload: ListenPayload, nowPlaying: boolean = false): PlayObject => { 252 272 253 273 // const listened = payload.listened_at ?? dayjs().unix(); 254 274 // const listenedAt = typeof listened === 'number' ? dayjs.unix(listened) : dayjs(listened); ··· 328 348 return oldPlay; 329 349 } 330 350 331 - static listenResponseToPlay(listen: ListenResponse): PlayObject { 351 + export const listenResponseToPlay = (listen: ListenResponse): PlayObject => { 332 352 const { 333 353 listened_at, 334 354 track_metadata: { ··· 347 367 } = {} 348 368 } = listen; 349 369 350 - const naivePlay = ListenbrainzApiClient.listenResponseToNaivePlay(listen); 370 + const naivePlay = listenResponseToNaivePlay(listen); 351 371 352 372 if(artistMappings.length === 0) { 353 373 // if there are no artist mappings its likely MB doesn't have info on this track so just use our internally derived attempt ··· 593 613 return play; 594 614 } 595 615 596 - /** 597 - * Try to parse true artists and track name without using MB information 598 - * */ 599 - static listenResponseToNaivePlay(listen: ListenResponse): PlayObject { 616 + /** 617 + * Try to parse true artists and track name without using MB information 618 + * */ 619 + export const listenResponseToNaivePlay = (listen: ListenResponse): PlayObject => { 600 620 const { 601 621 listened_at, 602 622 recording_msid, ··· 682 702 683 703 return play; 684 704 } 685 - 686 - static submitToPlayObj(submitObj: SubmitPayload, playObj: PlayObject): PlayObject { 687 - if (submitObj.payload.length > 0) { 688 - const respPlay = { 689 - ...playObj, 690 - }; 691 - respPlay.data = { 692 - ...playObj.data, 693 - album: submitObj.payload[0].track_metadata?.release_name ?? playObj.data.album, 694 - track: submitObj.payload[0].track_metadata?.track_name ?? playObj.data.album, 695 - }; 696 - return respPlay; 697 - } 698 - return playObj; 699 - } 700 - 701 - static formatPlayObj(obj: any, options: FormatPlayObjectOptions): PlayObject { 702 - return ListenbrainzApiClient.listenResponseToPlay(obj); 703 - } 704 - } 705 705 706 706 707 707 export const playToListenPayload = (play: PlayObject): ListenPayload => {
+3 -3
src/backend/sources/EndpointListenbrainzSource.ts
··· 12 12 ReportedPlayerStatus 13 13 } from "../common/infrastructure/Atomic.js"; 14 14 import { ListenbrainzEndpointSourceConfig } from "../common/infrastructure/config/source/endpointlz.js"; 15 - import { ListenbrainzApiClient } from "../common/vendor/ListenbrainzApiClient.js"; 15 + import { ListenbrainzApiClient, listenPayloadToPlay } from "../common/vendor/ListenbrainzApiClient.js"; 16 16 import { SubmitPayload } from '../common/vendor/listenbrainz/interfaces.js'; 17 17 import { ListenPayload } from '../common/vendor/listenbrainz/interfaces.js'; 18 18 import { parseRegexSingleOrFail } from "../utils.js"; ··· 76 76 static formatPlayObj(obj: ListenPayload, options: FormatPlayObjectOptions & { 77 77 nowPlaying?: boolean 78 78 } = {}): PlayObject { 79 - return ListenbrainzApiClient.listenPayloadToPlay(obj, options.nowPlaying); 79 + return listenPayloadToPlay(obj, options.nowPlaying); 80 80 } 81 81 82 82 getRecentlyPlayed = async (options = {}) => { ··· 108 108 payload, 109 109 } = obj; 110 110 111 - const play = ListenbrainzApiClient.listenPayloadToPlay(payload[0], listen_type === 'playing_now'); 111 + const play = listenPayloadToPlay(payload[0], listen_type === 'playing_now'); 112 112 return { 113 113 platformId: [play.meta.deviceId, NO_USER], 114 114 play,
+12 -12
src/backend/tests/listenbrainz/listenbrainz.test.ts
··· 6 6 import { PlayObject } from "../../../core/Atomic.js"; 7 7 import { UpstreamError } from "../../common/errors/UpstreamError.js"; 8 8 9 - import { ListenbrainzApiClient, playToListenPayload } from "../../common/vendor/ListenbrainzApiClient.js"; 9 + import { ListenbrainzApiClient, playToListenPayload, listenResponseToPlay, listenPayloadToPlay } from "../../common/vendor/ListenbrainzApiClient.js"; 10 10 import { ListenResponse } from '../../common/vendor/listenbrainz/interfaces.js'; 11 11 import { ExpectedResults } from "../utils/interfaces.js"; 12 12 import { withRequestInterception } from "../utils/networking.js"; ··· 33 33 describe('When user-submitted artist/track do NOT match MB mappings', function() { 34 34 it('Uses user submitted values when no artist mappings', async function () { 35 35 for(const test of noArtistMapping as unknown as LZTestFixture[]) { 36 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 36 + const play = listenResponseToPlay(test.data); 37 37 assert.equal(play.data.track, test.expected.track); 38 38 assert.sameDeepMembers(play.data.artists, test.expected.artists); 39 39 } ··· 41 41 42 42 it('Uses user-submitted values when when either mapped track/artist do not match', async function () { 43 43 for(const test of veryWrong as unknown as LZTestFixture[]) { 44 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 44 + const play = listenResponseToPlay(test.data); 45 45 assert.equal(play.data.track, test.expected.track); 46 46 assert.sameDeepMembers( play.data.artists, test.expected.artists); 47 47 } ··· 49 49 50 50 it('Should extract additional artists from track name', async function () { 51 51 for(const test of incorrectMultiArtistsTrackName as unknown as LZTestFixture[]) { 52 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 52 + const play = listenResponseToPlay(test.data); 53 53 assert.equal(play.data.track, test.expected.track); 54 54 assert.sameDeepMembers(play.data.artists, test.expected.artists); 55 55 } ··· 61 61 62 62 it('Detects slightly different track names as equal', async function () { 63 63 for(const test of slightlyDifferentNames as unknown as LZTestFixture[]) { 64 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 64 + const play = listenResponseToPlay(test.data); 65 65 assert.equal(play.data.track, test.expected.track); 66 66 assert.sameDeepMembers(play.data.artists, test.expected.artists); 67 67 } ··· 69 69 70 70 it('Uses all mapped artists', async function () { 71 71 for(const test of multiMappedArtistsWithSingleUserArtist as unknown as LZTestFixture[]) { 72 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 72 + const play = listenResponseToPlay(test.data); 73 73 assert.equal(play.data.track, test.expected.track); 74 74 assert.sameDeepMembers(play.data.artists, test.expected.artists); 75 75 } ··· 77 77 78 78 it('Respects artists with joiner symbols in proper names', async function () { 79 79 for(const test of artistWithProperJoiner as unknown as LZTestFixture[]) { 80 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 80 + const play = listenResponseToPlay(test.data); 81 81 assert.equal(play.data.track, test.expected.track); 82 82 assert.sameDeepMembers( play.data.artists, test.expected.artists); 83 83 } ··· 85 85 86 86 it('Detects user-submitted artists have joiners', async function () { 87 87 for(const test of multiArtistInArtistName as unknown as LZTestFixture[]) { 88 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 88 + const play = listenResponseToPlay(test.data); 89 89 assert.equal(play.data.track, test.expected.track); 90 90 assert.sameDeepMembers(play.data.artists, test.expected.artists); 91 91 } ··· 93 93 94 94 it('Detects artists in user-submitted track', async function () { 95 95 for(const test of multiArtistsInTrackName as unknown as LZTestFixture[]) { 96 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 96 + const play = listenResponseToPlay(test.data); 97 97 assert.equal(play.data.track, test.expected.track); 98 98 assert.sameDeepMembers( play.data.artists, test.expected.artists); 99 99 } ··· 101 101 102 102 it('Detects and uses normalized artist/track names', async function () { 103 103 for(const test of normalizedValues as unknown as LZTestFixture[]) { 104 - const play = ListenbrainzApiClient.listenResponseToPlay(test.data); 104 + const play = listenResponseToPlay(test.data); 105 105 assert.equal(play.data.track, test.expected.track); 106 106 assert.sameDeepMembers( play.data.artists, test.expected.artists); 107 107 } ··· 164 164 165 165 submitPayload.track_metadata.additional_info.artist_names = additionalArtists; 166 166 167 - const playFromPayload = ListenbrainzApiClient.listenPayloadToPlay(submitPayload); 167 + const playFromPayload = listenPayloadToPlay(submitPayload); 168 168 169 169 expect(playFromPayload.data.artists).to.be.eql(additionalArtists) 170 170 ··· 179 179 180 180 submitPayload.track_metadata.additional_info.artist_names = additionalArtists; 181 181 182 - const playFromPayload = ListenbrainzApiClient.listenPayloadToPlay(submitPayload); 182 + const playFromPayload = listenPayloadToPlay(submitPayload); 183 183 184 184 expect(playFromPayload.data.artists).to.be.eql(['Artist A', 'Artist B']) 185 185