[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(listenbrainz): Parse MB metadata from LZ Source listens and set in Play

Had not updated LZ source to parse metadata into Play since adding brainz metadata to Play object

#333

FoxxMD (Jul 31, 2025, 3:53 PM UTC) bf6bd4be 53f78b02

+159 -22
+141 -22
src/backend/common/vendor/ListenbrainzApiClient.ts
··· 1 1 import { stringSameness } from '@foxxmd/string-sameness'; 2 2 import dayjs from "dayjs"; 3 3 import request, { Request, Response } from 'superagent'; 4 - import { PlayObject, URLData } from "../../../core/Atomic.js"; 4 + import { BrainzMeta, PlayObject, URLData } from "../../../core/Atomic.js"; 5 5 import { combinePartsToString, slice } from "../../../core/StringUtils.js"; 6 6 import { 7 7 findDelimiters, ··· 24 24 import { log } from 'console'; 25 25 import { version } from '../../ioc.js'; 26 26 27 + /* 28 + * https://musicbrainz.org/doc/MusicBrainz_Database/Schema#Overview 29 + */ 30 + 31 + /** A unique product a Recording is issued on. 32 + * 33 + * This is like an album (release group) but is specific to the type, year, catalog, etc... for this release 34 + * 35 + * EX: 1984 US release of "The Wall" by "Pink Floyd", release on label "Columbia Records" with catalog number "C2K 36183" 36 + * 37 + * @see https://musicbrainz.org/doc/Release 38 + * 39 + * Referred to in MB api response as release_mbid 40 + * 41 + */ 42 + type ReleaseMbid = string; 43 + 44 + /** The "abstract", non-unique album/single/EP the Recording belongs to 45 + * 46 + * This is what people normally think of as an album (release group) 47 + * 48 + * EX: "The Wall" by "Pink Floyd" 49 + * 50 + * @see https://musicbrainz.org/doc/Release 51 + * 52 + * Referred to in MB api response as release_group -> mbid 53 + * 54 + */ 55 + type ReleaseGroupMbid = string; 56 + 57 + /** A unique mix/edit/master of a Work 58 + * 59 + * This is like a song but is unique to the master/edit of the song 60 + * 61 + * 62 + * * Album version of the track "Into the Blue" by "Moby" 63 + * * Remix "Into the Blue (Buzz Boys Main Room Mayhem mix)" by "Moby" 64 + * 65 + * @see https://musicbrainz.org/doc/Recording 66 + * 67 + * Referred to in MB api response as recording_mbid 68 + */ 69 + type RecordingMbid = string; 70 + 71 + /** The "abstract", non-unique Song produced by an Artist 72 + * 73 + * All Recordings "belong" to a single Work 74 + * 75 + * EX: Song "Into the Blue" by "Moby" 76 + * 77 + * @see Song "Into the Blue" by "Moby" 78 + */ 79 + type WorkMbid = string; 80 + 81 + /** A musician or group or musicians that release music 82 + * 83 + * @see https://musicbrainz.org/doc/Artist 84 + * 85 + * MB does not distinguish between Artist and Album Artists in API responses except for by release_artist_name in additional_info 86 + * All artists/album artists are included in mbid_mappings artists 87 + * 88 + */ 89 + type ArtistMbid = string; 90 + 91 + /** A unique, random identifier used for each scrobble. Not the same as recording_mbid */ 92 + type RecordingMsid = string; 27 93 28 94 export interface ArtistMBIDMapping { 29 95 artist_credit_name: string 30 - artist_mbid: string 96 + artist_mbid: ArtistMbid 31 97 join_phrase: string 32 98 } 33 99 ··· 38 104 } 39 105 40 106 export interface AdditionalTrackInfo { 41 - artist_mbids?: string[] 42 - release_mbid?: string 43 - release_group_mbid?: string 44 - recording_mbid?: string 107 + artist_mbids?: ArtistMbid[] 108 + release_mbid?: ReleaseMbid 109 + release_group_mbid?: ReleaseGroupMbid 110 + recording_mbid?: RecordingMbid 45 111 submission_client?: string 46 112 submission_client_version?: string 47 113 spotify_id?: string ··· 56 122 57 123 duration_ms?: number 58 124 track_mbid?: string 59 - work_mbids?: string[] 125 + work_mbids?: WorkMbid[] 60 126 } 61 127 62 128 export interface Track { 63 129 artist_name: string; 64 130 track_name: string; 65 131 release_name?: string; 66 - artist_mbids?: string[]; 67 - artist_msid?: string; 132 + artist_mbids?: ArtistMbid[]; 133 + artist_msid?: ArtistMbid; 68 134 recording_mbid?: string; 69 - release_mbid?: string; 135 + release_mbid?: ReleaseMbid; 70 136 release_msid?: string; 71 137 tags?: string[]; 72 138 ··· 74 140 } 75 141 76 142 export interface AdditionalTrackInfoResponse extends AdditionalTrackInfo { 77 - recording_msid?: string 143 + recording_msid?: RecordingMsid 144 + release_artist_name?: string 145 + release_artists_names?: string 78 146 } 79 147 80 148 // using submit-listens example from openapi https://rain0r.github.io/listenbrainz-openapi/index.html#/lbCore/submitListens ··· 96 164 97 165 export interface ListenPayload { 98 166 listened_at: Date | number; 99 - recording_msid?: string; 167 + recording_msid?: RecordingMsid; 100 168 track_metadata: TrackPayload; 101 169 } 102 170 ··· 117 185 additional_info: AdditionalTrackInfoResponse 118 186 mbid_mapping: { 119 187 recording_name?: string 120 - artist_mbids?: string[] 188 + artist_mbids?: ArtistMbid[] 121 189 artists?: ArtistMBIDMapping[] 122 190 caa_id?: number 191 + /** cover album archive mbid, not related to anything else I think */ 123 192 caa_release_mbid?: string 124 - recording_mbid?: string 125 - release_mbid?: string 193 + recording_mbid?: RecordingMbid 194 + release_mbid?: ReleaseMbid 126 195 } 127 196 } 128 197 ··· 135 204 136 205 inserted_at: number 137 206 listened_at: number; 138 - recording_msid?: string; 207 + recording_msid?: RecordingMsid; 139 208 track_metadata: TrackResponse; 140 209 } 141 210 export class ListenbrainzApiClient extends AbstractApiClient { ··· 398 467 mbid_mapping: { 399 468 recording_name, 400 469 artists: artistMappings = [], 401 - recording_mbid: mRecordingMbid 470 + recording_mbid: mRecordingMbid, 471 + release_mbid 472 + } = {}, 473 + additional_info: { 474 + release_artists_names = [], 475 + release_group_mbid 402 476 } = {} 403 477 } = {} 404 478 } = listen; ··· 419 493 420 494 let filteredSubmittedArtistName = artist_name; 421 495 let filteredSubmittedTrackName = track_name; 496 + 497 + let primaryArtist; 422 498 423 499 if(artistMappings.length > 0) { 424 500 ··· 516 592 // now we check that primary artist exists in mapped artists 517 593 const candidatedPrimaryArtist = artistsFromUserValues[0]; 518 594 const normalizedCandidatePrimary = normalizeStr(candidatedPrimaryArtist); 519 - const primaryArtist = mappedArtists.find(x => { 595 + primaryArtist = mappedArtists.find(x => { 520 596 if(normalizeStr(x) === normalizedCandidatePrimary) 521 597 { 522 598 return true; ··· 536 612 // now we have the primary artist matched! 537 613 // at this point we assume any differences between user and MB artists is a data discrepancy rather than being outright wrong 538 614 615 + /* 616 + * 617 + * Can safely use musicbrainz metadata beyond this point! 618 + * 619 + */ 620 + 539 621 // next we match up user artists with mapped artists in order to use "more correct" spelling/punctuation/etc. from MB mapping 540 622 const mappedUserArtists: string[] = []; 541 623 for(const userArtist of artistsFromUserValues) { ··· 597 679 normalTrackName = recording_name; 598 680 } 599 681 600 - return { 682 + const brainzMetaRaw: BrainzMeta = { 683 + ...(naivePlay.data.meta.brainz ?? {}), 684 + artist: artistMappings.map(x => x.artist_mbid), 685 + album: release_mbid, 686 + releaseGroup: release_group_mbid 687 + } 688 + 689 + // this should always find an artist but to be safe... 690 + const primaryArtistMBMapping = artistMappings.find(x => x.artist_credit_name === primaryArtist); 691 + if(primaryArtistMBMapping !== undefined) { 692 + // only include as primary if musicbrainz does not disagree with us 693 + if(release_artists_names.length === 0 || (release_artists_names.length > 0 && release_artists_names.includes(primaryArtist))) { 694 + brainzMetaRaw.albumArtist = primaryArtistMBMapping.artist_mbid; 695 + } 696 + } 697 + 698 + const brainzMeta = removeUndefinedKeys(brainzMetaRaw); 699 + 700 + const play: PlayObject = { 601 701 data: { 602 702 ...naivePlay.data, 603 703 track: normalTrackName, 604 - artists: derivedArtists, 704 + artists: derivedArtists 605 705 }, 606 706 meta: naivePlay.meta 607 707 } 708 + 709 + if(brainzMeta !== undefined) { 710 + play.data.meta = { 711 + brainz: brainzMeta 712 + } 713 + } 714 + 715 + return play; 608 716 } 609 717 610 718 /** ··· 662 770 } 663 771 artists = uniqueNormalizedStrArr(artists); 664 772 665 - return { 773 + const play: PlayObject = { 666 774 data: { 667 775 playDate: dayjs.unix(listened_at), 668 776 track: normalTrackName, ··· 672 780 }, 673 781 meta: { 674 782 source: 'listenbrainz', 675 - trackId, 676 783 playId, 677 784 deviceId: combinePartsToString([music_service_name ?? music_service, submission_client, submission_client_version]) 678 785 } 679 786 } 787 + 788 + // we shouldn't include more metdata here because we don't know if the MB mapped data is actually correct 789 + if(trackId !== undefined) { 790 + play.data.meta = { 791 + brainz: { 792 + track: trackId 793 + } 794 + } 795 + play.meta.trackid = trackId; 796 + } 797 + 798 + return play; 680 799 } 681 800 682 801 static submitToPlayObj(submitObj: SubmitPayload, playObj: PlayObject): PlayObject {
+18
src/core/Atomic.ts
··· 66 66 end: ListenProgress 67 67 } 68 68 69 + /** https://musicbrainz.org/doc/MusicBrainz_Database/Schema#Overview */ 69 70 export interface BrainzMeta { 71 + /** 72 + * artist_mbids 73 + * 74 + * All artists, including ft guests etc... go here */ 70 75 artist?: string[] 76 + /** 77 + * artists_mbid 78 + * 79 + * If multiple artists for track this is the "original" artist who is releasing the single/album */ 71 80 albumArtist?: string 81 + /** 82 + * release_mbid 83 + * 84 + * The unique release like --> 1984 US release of "The Wall" by "Pink Floyd", release on label "Columbia Records" with catalog number "C2K 36183" 85 + * */ 72 86 album?: string 87 + /** Unique track id, recording_mbid */ 73 88 track?: string 89 + /** 90 + * 91 + * The "consolidated" album like --> "The Wall" by "Pink Floyd" */ 74 92 releaseGroup?: string 75 93 } 76 94