[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): Full destructure recent response and log respon on error

Maybe recenttracks/track does not exist for totally new accounts, though it should.

Guard against this and log response if something unexpected happens so we can troubleshoot

#353

FoxxMD (Sep 11, 2025, 6:27 PM UTC) 506faf39 adf5401f

+59 -49
+17 -12
src/backend/scrobblers/LastfmScrobbler.ts
··· 47 47 } 48 48 49 49 getScrobblesForRefresh = async (limit: number) => { 50 - const resp = await this.api.callApi<UserGetRecentTracksResponse>((client: any) => client.userGetRecentTracks({ 51 - user: this.api.user, 52 - sk: this.api.client.sessionKey, 53 - limit: limit, 54 - extended: true 55 - })); 50 + const resp = await this.api.callApi<UserGetRecentTracksResponse>((client: any) => client.userGetRecentTracks({ 51 + user: this.api.user, 52 + sk: this.api.client.sessionKey, 53 + limit: limit, 54 + extended: true 55 + })); 56 + try { 56 57 const { 57 58 recenttracks: { 58 59 track: list = [], 59 - } 60 + } = {} 60 61 } = resp; 61 62 return list.reduce((acc: any, x: any) => { 62 63 try { ··· 71 72 nowPlaying, 72 73 } 73 74 } = formatted; 74 - if(nowPlaying === true) { 75 + if (nowPlaying === true) { 75 76 // if the track is "now playing" it doesn't get a timestamp so we can't determine when it started playing 76 77 // and don't want to accidentally count the same track at different timestamps by artificially assigning it 'now' as a timestamp 77 78 // so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway 78 - this.logger.debug("Ignoring 'now playing' track returned from Last.fm client", {track, mbid}); 79 + this.logger.debug("Ignoring 'now playing' track returned from Last.fm client", { track, mbid }); 79 80 return acc; 80 - } else if(playDate === undefined) { 81 - this.logger.warn(`Last.fm recently scrobbled track did not contain a timestamp, omitting from time frame check`, {track, mbid}); 81 + } else if (playDate === undefined) { 82 + this.logger.warn(`Last.fm recently scrobbled track did not contain a timestamp, omitting from time frame check`, { track, mbid }); 82 83 return acc; 83 84 } 84 85 return acc.concat(formatted); 85 86 } catch (e) { 86 - this.logger.warn('Failed to format Last.fm recently scrobbled track, omitting from time frame check', {error: e.message}); 87 + this.logger.warn('Failed to format Last.fm recently scrobbled track, omitting from time frame check', { error: e.message }); 87 88 this.logger.debug('Full api response object:'); 88 89 this.logger.debug(x); 89 90 return acc; 90 91 } 91 92 }, []); 93 + } catch (e) { 94 + this.logger.debug(resp); 95 + throw e; 96 + } 92 97 } 93 98 94 99 cleanSourceSearchTitle = (playObj: PlayObject) => {
+42 -37
src/backend/sources/LastfmSource.ts
··· 80 80 limit, 81 81 extended: true 82 82 })); 83 - const { 84 - recenttracks: { 85 - track: list = [], 86 - } 87 - } = resp; 83 + try { 84 + const { 85 + recenttracks: { 86 + track: list = [], 87 + } = {} 88 + } = resp; 88 89 89 - const plays = list.reduce((acc: PlayObject[], x: TrackObject) => { 90 - try { 91 - const formatted = LastfmApiClient.formatPlayObj(x); 92 - const { 93 - data: { 94 - track, 95 - playDate, 96 - }, 97 - meta: { 98 - mbid, 99 - nowPlaying, 90 + const plays = list.reduce((acc: PlayObject[], x: TrackObject) => { 91 + try { 92 + const formatted = LastfmApiClient.formatPlayObj(x); 93 + const { 94 + data: { 95 + track, 96 + playDate, 97 + }, 98 + meta: { 99 + mbid, 100 + nowPlaying, 101 + } 102 + } = formatted; 103 + if (playDate === undefined) { 104 + if (nowPlaying === true) { 105 + formatted.data.playDate = dayjs(); 106 + return acc.concat(formatted); 107 + } 108 + this.logger.warn(`Last.fm recently scrobbled track did not contain a timestamp, omitting from time frame check`, { track, mbid }); 109 + return acc; 100 110 } 101 - } = formatted; 102 - if(playDate === undefined) { 103 - if(nowPlaying === true) { 104 - formatted.data.playDate = dayjs(); 105 - return acc.concat(formatted); 106 - } 107 - this.logger.warn(`Last.fm recently scrobbled track did not contain a timestamp, omitting from time frame check`, {track, mbid}); 111 + return acc.concat(formatted); 112 + } catch (e) { 113 + this.logger.warn('Failed to format Last.fm recently scrobbled track, omitting from time frame check', { error: e.message }); 114 + this.logger.debug('Full api response object:'); 115 + this.logger.debug(x); 108 116 return acc; 109 117 } 110 - return acc.concat(formatted); 111 - } catch (e) { 112 - this.logger.warn('Failed to format Last.fm recently scrobbled track, omitting from time frame check', {error: e.message}); 113 - this.logger.debug('Full api response object:'); 114 - this.logger.debug(x); 115 - return acc; 116 - } 117 - }, []).sort(sortByOldestPlayDate); 118 - // if the track is "now playing" it doesn't get a timestamp so we can't determine when it started playing 119 - // and don't want to accidentally count the same track at different timestamps by artificially assigning it 'now' as a timestamp 120 - // so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway 121 - const history = plays.filter(x => x.meta.nowPlaying !== true); 122 - const now = plays.filter(x => x.meta.nowPlaying === true); 123 - return [history, now]; 118 + }, []).sort(sortByOldestPlayDate); 119 + // if the track is "now playing" it doesn't get a timestamp so we can't determine when it started playing 120 + // and don't want to accidentally count the same track at different timestamps by artificially assigning it 'now' as a timestamp 121 + // so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway 122 + const history = plays.filter(x => x.meta.nowPlaying !== true); 123 + const now = plays.filter(x => x.meta.nowPlaying === true); 124 + return [history, now]; 125 + } catch (e) { 126 + this.logger.debug(resp); 127 + throw e; 128 + } 124 129 } 125 130 126 131 getRecentlyPlayed = async(options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {