diff --git a/src/backend/server/endpointListenbrainzRoutes.ts b/src/backend/server/endpointListenbrainzRoutes.ts index d984bb45..840123a1 100644 --- a/src/backend/server/endpointListenbrainzRoutes.ts +++ b/src/backend/server/endpointListenbrainzRoutes.ts @@ -65,10 +65,10 @@ export const setupLZEndpointRoutes = (app: Express, parentLogger: Logger, scrobb if(isDebugMode()) { logger.debug({body: req.body}, "Recieved request Body"); } - const playerState = playStateFromRequest(req.body); + const playerStates = playStateFromRequest(req.body); for (const source of validSources) { - await source.handle(playerState); + await source.handle(playerStates); } }); diff --git a/src/backend/sources/EndpointListenbrainzSource.ts b/src/backend/sources/EndpointListenbrainzSource.ts index d8cf7678..857592f3 100644 --- a/src/backend/sources/EndpointListenbrainzSource.ts +++ b/src/backend/sources/EndpointListenbrainzSource.ts @@ -89,15 +89,18 @@ export class EndpointListenbrainzSource extends MemorySource { return true; } - handle = async (stateData: PlayerStateData) => { + handle = async (stateData: PlayerStateData[]) => { - await this.processRecentPlays([stateData]); + // if request was an import (multiple plays) then we don't want to process for "now playing" player + // so only process if we only have one payload in the request + if(stateData.length === 1) { + await this.processRecentPlays(stateData); + } - if (stateData.play.meta.nowPlaying === false && this.isValidScrobble(stateData.play)) { - const discovered = await this.discover([stateData.play]); - if (discovered.length > 0) { - await this.scrobble(discovered); - } + const discoverable = stateData.filter(x => x.play.meta.nowPlaying === false && this.isValidScrobble(x.play)); + const discovered = await this.discover(discoverable.map(x => x.play)); + if (discovered.length > 0) { + await this.scrobble(discovered); } this.componentRepo.updateById(this.dbComponent.id, {lastActiveAt: dayjs()}); } @@ -105,20 +108,23 @@ export class EndpointListenbrainzSource extends MemorySource { getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new NowPlayingPlayerState(logger, id, opts); } -export const playStateFromRequest = (obj: SubmitPayload): PlayerStateData => { +export const playStateFromRequest = (obj: SubmitPayload): PlayerStateData[] => { const { listen_type, payload, } = obj; - const play = listenPayloadToPlay(payload[0], listen_type === 'playing_now'); - play.meta.sourceSOT = SOURCE_SOT.HISTORY; - return { - platformId: [play.meta.deviceId, NO_USER], - play, - status: listenTypeAsPlayerStatus(listen_type), - stateUpdatedAt: dayjs() - } + const playStates: PlayerStateData[] = payload.map((x) => { + const play = listenPayloadToPlay(x, listen_type === 'playing_now'); + play.meta.sourceSOT = SOURCE_SOT.HISTORY; + return { + platformId: [play.meta.deviceId, NO_USER], + play, + status: listenTypeAsPlayerStatus(listen_type), + stateUpdatedAt: dayjs() + } + }); + return playStates; } export const listenTypeAsPlayerStatus = (event: string): ReportedPlayerStatus => { -- 2.51.2 From da9edb21162e36f639185628ba7d83b69a7d975b Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 9 Jun 2026 13:42:54 +0000 Subject: [PATCH 2/2] feat(lzendpoint): Improved error handling * Return error response when no sources found by request, or none configured * try-catch and return well-formatted error response if source handling fails --- .../server/endpointListenbrainzRoutes.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/backend/server/endpointListenbrainzRoutes.ts b/src/backend/server/endpointListenbrainzRoutes.ts index 840123a1..061cc49a 100644 --- a/src/backend/server/endpointListenbrainzRoutes.ts +++ b/src/backend/server/endpointListenbrainzRoutes.ts @@ -11,6 +11,7 @@ import { PlayingNowPayload } from '../common/vendor/listenbrainz/interfaces.js'; import ScrobbleClients from '../scrobblers/ScrobbleClients.js'; import { playToListenPayload } from '../common/vendor/listenbrainz/lzUtils.js'; import { stringToDeterministicNumber } from '../utils/StringUtils.js'; +import { messageWithCauses } from '../../core/ErrorUtils.js'; const TEXT_WILDCARD_REGEX = new RegExp(/text\/.+/); @@ -48,28 +49,35 @@ export const setupLZEndpointRoutes = (app: Express, parentLogger: Logger, scrobb lzJsonParser, nonEmptyCheck, async function (req, res) { webhookIngress.trackIngress(req, false); - res.status(200).json({status: "ok"}); + logger.trace({body: req.body}, "Recieved request Body"); + const playerStates = playStateFromRequest(req.body); const sources = scrobbleSources.getByType('endpointlz') as EndpointListenbrainzSource[]; if (sources.length === 0) { logger.warn('Received Listenbrainz endpoint payload but no Listenbrainz endpoint sources are configured'); + return res.status(409).json({error: `Received Listenbrainz endpoint payload but no Listenbrainz endpoint sources are configured`, code: 409}); } const validSources = sources.filter(x => x.matchRequest(req)); if (validSources.length === 0) { const [slug, token] = parseDisplayIdentifiersFromRequest(req); logger.warn(`No Listenbrainz endpoint config matched => Slug: ${slug} | Token: ${token}`); + return res.status(409).json({error: `No Listenbrainz endpoint config matched => Slug: ${slug} | Token: ${token}`, code: 409}); } - if(isDebugMode()) { - logger.debug({body: req.body}, "Recieved request Body"); + try { + for (const source of validSources) { + await source.handle(playerStates); + } + } catch (e) { + const submitListenError = new Error('Unexpected error occurred while processing submit-listens request', {cause: e}); + const errMsg = messageWithCauses(submitListenError); + logger.error(submitListenError); + return res.status(500).json({error: errMsg, code: 500}); } - const playerStates = playStateFromRequest(req.body); - for (const source of validSources) { - await source.handle(playerStates); - } + return res.status(200).json({status: "ok"}); }); app.get('/1/user/:username/playing-now', async function (req, res) {