From 22d76c459a4d761870017e26028a15ad050748e2 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 16 Mar 2026 19:10:27 +0000 Subject: [PATCH] fix(scrobble): Improve ui/api experience for dead scrobble processing * Return 200 when processing all, don't wait to finish to prevent response timeout * Use event stream to update/remove/add dead scrobbles in ui #489 --- .../scrobblers/AbstractScrobbleClient.ts | 4 ++ src/backend/server/api.ts | 6 +-- src/client/deadLetter/deadLetterDucks.ts | 44 ++++++++++++------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 99a52ccd..d8d5ace0 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -815,6 +815,7 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i deadScrobble.lastRetry = dayjs(); this.deadLetterScrobbles[deadScrobbleIndex] = deadScrobble; this.updateDeadLetterCache(); + this.emitEvent('updateDeadLetter', {dead: deadScrobble}); return [false, deadScrobble]; } finally { await sleep(1000); @@ -857,6 +858,7 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i this.deadLogger.error(new Error(`${id} - Could not scrobble ${buildTrackString(transformedScrobble)} from Source '${deadScrobble.source}' due to error`, {cause: e})); this.deadLetterScrobbles[deadScrobbleIndex] = deadScrobble; this.updateDeadLetterCache(); + this.emitEvent('updateDeadLetter', {dead: deadScrobble}); return [false, deadScrobble]; } finally { await sleep(1000); @@ -873,11 +875,13 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i const index = this.deadLetterScrobbles.findIndex(x => x.id === id); if (index === -1) { this.deadLogger.warn(`No scrobble found with ID ${id}`); + return; } this.deadLogger.info({labels: id}, `Removed scrobble ${buildTrackString(this.deadLetterScrobbles[index].play)} from queue`); this.deadLetterScrobbles.splice(index, 1); this.deadLetterGauge.labels(this.getPrometheusLabels()).set(this.deadLetterScrobbles.length); this.updateDeadLetterCache(); + this.emitEvent('removeDeadLetter', { dead: { id } }); } removeDeadLetterScrobbles = () => { diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index 9e7a1489..b9e244de 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -331,11 +331,9 @@ export const setupApi = (app: ExpressWithAsync, logger: Logger, appLoggerStream: (client as AbstractScrobbleClient).logger.verbose('User requested processing of all dead letter scrobbles via API'); - await ((client as AbstractScrobbleClient).processDeadLetterQueue(1000)); - - const result: DeadLetterScrobble[] = (client as AbstractScrobbleClient).deadLetterScrobbles; + res.status(200).send('OK'); - return res.json(result); + await ((client as AbstractScrobbleClient).processDeadLetterQueue(1000)); }); app.putAsync('/api/dead/:id', clientMiddleFunc(true), async (req, res, next) => { diff --git a/src/client/deadLetter/deadLetterDucks.ts b/src/client/deadLetter/deadLetterDucks.ts index 7a26101c..81d44be1 100644 --- a/src/client/deadLetter/deadLetterDucks.ts +++ b/src/client/deadLetter/deadLetterDucks.ts @@ -92,40 +92,54 @@ export const deadSlice = createSlice({ } ) builder.addMatcher( - (action) => deadApi.endpoints.getDead.matchFulfilled(action) || deadApi.endpoints.processDead.matchFulfilled(action) || deadApi.endpoints.removeDead.matchFulfilled(action), + (action) => deadApi.endpoints.getDead.matchFulfilled(action) || deadApi.endpoints.removeDead.matchFulfilled(action), (state, action) => { deadAdapter.setAll(state, action.payload); } ) + // .addMatcher( + // (action) => deadApi.endpoints.removeDeadSingle.matchFulfilled(action), + // (state, action) => { + // deadAdapter.removeOne(state, action.meta.arg.originalArgs.id) + // } + // ) + // .addMatcher( + // (action) => deadApi.endpoints.processDeadSingle.matchFulfilled(action), + // (state, action) => { + // if (action.payload === undefined) { + // deadAdapter.removeOne(state, action.meta.arg.originalArgs.id); + // } else { + // state.entities[action.meta.arg.originalArgs.id] = action.payload; + // //deadAdapter.updateOne(state, action.meta.arg.originalArgs.id); + // } + // } + // ) .addMatcher( - (action) => deadApi.endpoints.removeDeadSingle.matchFulfilled(action), + (action) => clearDead.match(action), (state, action) => { - deadAdapter.removeOne(state, action.meta.arg.originalArgs.id) + state = deadAdapter.getInitialState(); } ) .addMatcher( - (action) => deadApi.endpoints.processDeadSingle.matchFulfilled(action), + (action) => clientUpdate.match(action) && action.payload.event === 'deadLetter', (state, action) => { - if (action.payload === undefined) { - deadAdapter.removeOne(state, action.meta.arg.originalArgs.id); - } else { - state.entities[action.meta.arg.originalArgs.id] = action.payload; - //deadAdapter.updateOne(state, action.meta.arg.originalArgs.id); - } + state.entities[(action.payload as ApiEventPayload).data.dead.id] = (action.payload as ApiEventPayload).data.dead; + state.ids.push((action.payload as ApiEventPayload).data.dead.id); } ) .addMatcher( - (action) => clearDead.match(action), + (action) => clientUpdate.match(action) && action.payload.event === 'removeDeadLetter', (state, action) => { - state = deadAdapter.getInitialState(); + delete state.entities[(action.payload as ApiEventPayload).data.dead.id]; + state.ids = state.ids.filter(x => x !== (action.payload as ApiEventPayload).data.dead.id); } ) - /*.addMatcher( - (action) => clientUpdate.match(action) && action.payload.event === 'deadLetter', + .addMatcher( + (action) => clientUpdate.match(action) && action.payload.event === 'updateDeadLetter', (state, action) => { state.entities[(action.payload as ApiEventPayload).data.dead.id] = (action.payload as ApiEventPayload).data.dead; } - )*/ + ) } }); -- 2.51.2