[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): Fix interfaces for submit/listen and consolidate

FoxxMD (Nov 13, 2025, 3:36 PM UTC) 83d9af2a 43376152

+70 -35
+30 -6
src/backend/common/vendor/ListenbrainzApiClient.ts
··· 249 249 } 250 250 251 251 static listenPayloadToPlay(payload: ListenPayload, nowPlaying: boolean = false): PlayObject { 252 + 253 + // const listened = payload.listened_at ?? dayjs().unix(); 254 + // const listenedAt = typeof listened === 'number' ? dayjs.unix(listened) : dayjs(listened); 255 + 256 + // const { 257 + // track_metadata: { 258 + // additional_info = {} 259 + // } = {}, 260 + // } = payload; 261 + 262 + // const play = ListenbrainzApiClient.listenResponseToPlay({ 263 + // ...payload, 264 + // track_metadata: { 265 + // ...payload.track_metadata, 266 + // additional_info 267 + // }, 268 + // listened_at: listenedAt.unix() 269 + // }); 270 + 271 + // play.meta.nowPlaying = nowPlaying; 272 + 252 273 const { 253 274 listened_at = dayjs().unix(), 254 275 track_metadata: { ··· 282 303 dur = duration_ms/1000; 283 304 } 284 305 285 - return { 306 + const oldPlay = { 286 307 data: { 287 308 playDate: typeof listened_at === 'number' ? dayjs.unix(listened_at) : dayjs(listened_at), 288 309 track: track_name, ··· 303 324 nowPlaying, 304 325 } 305 326 } 327 + 328 + return oldPlay; 306 329 } 307 330 308 331 static listenResponseToPlay(listen: ListenResponse): PlayObject { ··· 318 341 release_mbid 319 342 } = {}, 320 343 additional_info: { 321 - release_artists_names = [], 344 + release_artist_names = [], 322 345 release_group_mbid 323 346 } = {} 324 347 } = {} ··· 545 568 const primaryArtistMBMapping = artistMappings.find(x => x.artist_credit_name === primaryArtist); 546 569 if(primaryArtistMBMapping !== undefined) { 547 570 // only include as primary if musicbrainz does not disagree with us 548 - if(release_artists_names.length === 0 || (release_artists_names.length > 0 && release_artists_names.includes(primaryArtist))) { 571 + if(release_artist_names.length === 0 || (release_artist_names.length > 0 && release_artist_names.includes(primaryArtist))) { 549 572 brainzMetaRaw.albumArtist = primaryArtistMBMapping.artist_mbid; 550 573 } 551 574 } ··· 581 604 track_name, 582 605 artist_name, 583 606 release_name, 584 - duration, 585 607 additional_info: { 586 608 recording_msid: aRecordingMsid, 587 609 recording_mbid: aRecordingMbid, ··· 601 623 602 624 const playId = recording_msid ?? aRecordingMsid; 603 625 const trackId = aRecordingMbid ?? mRecordingMbid; 604 - let dur = duration ?? aDuration; 626 + let dur = aDuration; 605 627 if (dur === undefined && aDurationMs !== undefined) { 606 628 dur = Math.round(aDurationMs / 1000); 607 629 } ··· 787 809 } 788 810 } 789 811 return undefined; 790 - } 812 + } 813 + 814 + const FEAT_REGEX = new RegExp(/(.+) feat\. (.+)/);
+40 -29
src/backend/common/vendor/listenbrainz/interfaces.ts
··· 89 89 duration_ms?: number; 90 90 track_mbid?: string; 91 91 work_mbids?: WorkMbid[]; 92 + 93 + release_artist_name?: string; 94 + release_artist_names?: string[]; 95 + spotify_album_id?: string; 96 + spotify_album_artist_ids?: string[]; 97 + spotify_artist_ids?: string[]; 98 + artist_names?: string[]; 99 + albumartist?: string; 92 100 } 93 101 export interface Track { 94 102 artist_name: string; ··· 103 111 104 112 duration?: number; 105 113 } 106 - export interface AdditionalTrackInfoResponse extends AdditionalTrackInfo { 107 - recording_msid?: RecordingMsid; 108 - release_artist_name?: string; 109 - release_artists_names?: string; 110 - spotify_album_id?: string; 111 - spotify_album_artist_ids?: string[]; 112 - spotify_artist_ids?: string[]; 114 + 115 + export type ListenType = 'single' | 'playing_now'; 116 + export interface MbidMapping { 117 + recording_name?: string; 118 + artist_mbids?: ArtistMbid[]; 119 + artists?: ArtistMBIDMapping[]; 120 + caa_id?: number; 121 + /** cover album archive mbid, not related to anything else I think */ 122 + caa_release_mbid?: string; 123 + recording_mbid?: RecordingMbid; 124 + release_mbid?: ReleaseMbid; 113 125 } 126 + 114 127 // using submit-listens example from openapi https://rain0r.github.io/listenbrainz-openapi/index.html#/lbCore/submitListens 115 128 // which is documented in official docs https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#openapi-specification 116 129 // and based on this LZ developer comment https://github.com/lyarenei/jellyfin-plugin-listenbrainz/issues/10#issuecomment-1253867941 117 130 131 + // 132 + // data structures for submitting a listen 133 + // 118 134 export interface SubmitListenAdditionalTrackInfo extends AdditionalTrackInfo { 119 - artist_names?: string[]; 120 - release_artist_name?: string; 121 - release_artist_names?: string[]; 122 - spotify_album_id?: string; 123 - spotify_album_artist_ids?: string[]; 124 - spotify_artist_ids?: string[]; 125 - albumartist?: string; 135 + 126 136 } 127 137 export interface TrackPayload extends MinimumTrack { 128 138 additional_info?: SubmitListenAdditionalTrackInfo; 139 + mbid_mapping?: MbidMapping 129 140 } 130 141 export interface ListenPayload { 131 142 listened_at: Date | number; 132 - recording_msid?: RecordingMsid; 133 143 track_metadata: TrackPayload; 134 144 } 135 - export type ListenType = 'single' | 'playing_now'; 145 + 146 + // this is what is sent to submit-listens 136 147 export interface SubmitPayload { 137 148 listen_type: ListenType; 138 149 payload: [ListenPayload]; 139 150 } 140 - export interface TrackResponse extends MinimumTrack { 141 - duration: number; 151 + 152 + // 153 + // data structures returned from listens 154 + // 155 + 156 + export interface AdditionalTrackInfoResponse extends SubmitListenAdditionalTrackInfo { 157 + recording_msid?: RecordingMsid; 158 + } 159 + 160 + export interface TrackResponse extends TrackPayload { 142 161 additional_info: AdditionalTrackInfoResponse; 143 - mbid_mapping: { 144 - recording_name?: string; 145 - artist_mbids?: ArtistMbid[]; 146 - artists?: ArtistMBIDMapping[]; 147 - caa_id?: number; 148 - /** cover album archive mbid, not related to anything else I think */ 149 - caa_release_mbid?: string; 150 - recording_mbid?: RecordingMbid; 151 - release_mbid?: ReleaseMbid; 152 - }; 153 162 } 163 + 164 + // this is what is received from listens endpoint 154 165 export interface ListenResponse { 155 166 156 - inserted_at: number; 167 + inserted_at?: number; 157 168 listened_at: number; 158 169 recording_msid?: RecordingMsid; 159 170 track_metadata: TrackResponse;