[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(lastfm): Remove VA from album artist #340

FoxxMD (Aug 25, 2025, 5:00 PM UTC) c81bdf60 e9e59873

+82 -60
+59 -56
src/backend/common/vendor/LastfmApiClient.ts
··· 204 204 throw e; 205 205 } 206 206 } 207 - 208 - public playToClientPayload(playObj: PlayObject): TrackScrobblePayload { 209 - const { 210 - data: { 211 - artists = [], 212 - album, 213 - albumArtists = [], 214 - track, 215 - duration, 216 - playDate, 217 - meta: { 218 - brainz: { 219 - track: mbid 220 - } = {}, 221 - } = {} 222 - } = {} 223 - } = playObj; 224 - 225 - // LFM does not support multiple artists in scrobble payload 226 - // https://www.last.fm/api/show/track.scrobble 227 - let artist: string; 228 - if (artists.length === 0) { 229 - artist = ""; 230 - } else { 231 - artist = artists[0]; 232 - } 233 - 234 - const additionalRichPayload: Partial<TrackScrobblePayload> = {}; 235 - if(duration !== 0) { 236 - additionalRichPayload.duration = duration; 237 - } 238 - 239 - const rawPayload: TrackScrobblePayload = { 240 - artist: artist, 241 - track, 242 - album, 243 - timestamp: getScrobbleTsSOCDate(playObj).unix(), 244 - mbid, 245 - ...additionalRichPayload 246 - }; 247 - 248 - // LFM does not support multiple artists in scrobble payload 249 - // https://www.last.fm/api/show/track.scrobble 250 - if (albumArtists.length > 0) { 251 - rawPayload.albumArtist = albumArtists[0]; 252 - } 253 - 254 - // I don't know if its lastfm-node-client building the request params incorrectly 255 - // or the last.fm api not handling the params correctly... 256 - // 257 - // ...but in either case if any of the below properties is undefined (possibly also null??) 258 - // then last.fm responds with an IGNORED scrobble and error code 1 (totally unhelpful) 259 - // so remove all undefined keys from the object before passing to the api client 260 - return removeUndefinedKeys(rawPayload); 261 - } 262 207 } 263 208 264 209 export const scrobblePayloadToPlay = (obj: LastfmTrackUpdateRequest): PlayObject => { ··· 316 261 } 317 262 318 263 return play; 319 - } 264 + } 265 + 266 + export const playToClientPayload = (playObj: PlayObject): TrackScrobblePayload => { 267 + const { 268 + data: { 269 + artists = [], 270 + album, 271 + albumArtists = [], 272 + track, 273 + duration, 274 + playDate, 275 + meta: { 276 + brainz: { 277 + track: mbid 278 + } = {}, 279 + } = {} 280 + } = {} 281 + } = playObj; 282 + 283 + // LFM does not support multiple artists in scrobble payload 284 + // https://www.last.fm/api/show/track.scrobble 285 + let artist: string; 286 + if (artists.length === 0) { 287 + artist = ""; 288 + } else { 289 + artist = artists[0]; 290 + } 291 + 292 + const additionalRichPayload: Partial<TrackScrobblePayload> = {}; 293 + if(duration !== 0) { 294 + additionalRichPayload.duration = duration; 295 + } 296 + 297 + const rawPayload: TrackScrobblePayload = { 298 + artist: artist, 299 + track, 300 + album, 301 + timestamp: getScrobbleTsSOCDate(playObj).unix(), 302 + mbid, 303 + ...additionalRichPayload 304 + }; 305 + 306 + // LFM ignores scrobbles where album artist is VA 307 + // https://github.com/FoxxMD/multi-scrobbler/issues/340#issuecomment-3220774257 308 + const nonVaAlbumArtists = albumArtists.filter(x => x.trim().toLocaleLowerCase() !== 'va'); 309 + // LFM does not support multiple artists in scrobble payload 310 + // https://www.last.fm/api/show/track.scrobble 311 + if (nonVaAlbumArtists.length > 0) { 312 + rawPayload.albumArtist = nonVaAlbumArtists[0]; 313 + } 314 + 315 + // I don't know if its lastfm-node-client building the request params incorrectly 316 + // or the last.fm api not handling the params correctly... 317 + // 318 + // ...but in either case if any of the below properties is undefined (possibly also null??) 319 + // then last.fm responds with an IGNORED scrobble and error code 1 (totally unhelpful) 320 + // so remove all undefined keys from the object before passing to the api client 321 + return removeUndefinedKeys(rawPayload); 322 + }
+4 -4
src/backend/scrobblers/LastfmScrobbler.ts
··· 7 7 import { UpstreamError } from "../common/errors/UpstreamError.js"; 8 8 import { FormatPlayObjectOptions } from "../common/infrastructure/Atomic.js"; 9 9 import { LastfmClientConfig } from "../common/infrastructure/config/client/lastfm.js"; 10 - import LastfmApiClient from "../common/vendor/LastfmApiClient.js"; 10 + import LastfmApiClient, { playToClientPayload } from "../common/vendor/LastfmApiClient.js"; 11 11 import { Notifiers } from "../notifier/Notifiers.js"; 12 12 import AbstractScrobbleClient from "./AbstractScrobbleClient.js"; 13 13 ··· 103 103 alreadyScrobbled = async (playObj: PlayObject, log = false) => (await this.existingScrobble(playObj)) !== undefined 104 104 105 105 public playToClientPayload(playObject: PlayObject): object { 106 - return this.api.playToClientPayload(playObject); 106 + return playToClientPayload(playObject); 107 107 } 108 108 109 109 doScrobble = async (playObj: PlayObject) => { ··· 116 116 117 117 const sType = newFromSource ? 'New' : 'Backlog'; 118 118 119 - const scrobblePayload = this.api.playToClientPayload(playObj); 119 + const scrobblePayload = playToClientPayload(playObj); 120 120 121 121 try { 122 122 const response = await this.api.callApi<TrackScrobbleResponse>((client: any) => client.trackScrobble( ··· 173 173 174 174 doPlayingNow = async (data: PlayObject) => { 175 175 try { 176 - const {timestamp, mbid, ...rest} = this.api.playToClientPayload(data); 176 + const {timestamp, mbid, ...rest} = playToClientPayload(data); 177 177 const response = await this.api.callApi<NowPlayingResponse>((client: any) => client.trackUpdateNowPlaying(rest)); 178 178 const { 179 179 nowplaying: {
+19
src/backend/tests/lastfm/lastfm.test.ts
··· 1 + import chai, { expect } from 'chai'; 2 + import asPromised from 'chai-as-promised'; 3 + import { after, before, describe, it } from 'mocha'; 4 + import { generatePlay } from "../utils/PlayTestUtils.js"; 5 + 6 + import { playToClientPayload } from '../../common/vendor/LastfmApiClient.js'; 7 + 8 + chai.use(asPromised); 9 + 10 + describe('#LFM Scrobble Payload Behavior', function () { 11 + 12 + it('Should remove VA from album artist', function() { 13 + const play = generatePlay({albumArtists: ['VA']}); 14 + expect(playToClientPayload(play).albumArtist).to.be.undefined; 15 + 16 + const okPlay = generatePlay({albumArtists: ['My Dude']}); 17 + expect(playToClientPayload(okPlay).albumArtist).eq('My Dude'); 18 + }); 19 + });