···9999 /**
100100 * Number of times MS should automatically retry scrobbles in dead letter queue
101101 *
102102- * @default 1
103103- * @examples [1]
102102+ * @default 3
103103+ * @examples [3]
104104 * */
105105 deadLetterRetries?: number
106106
+34-33
src/backend/scrobblers/AbstractScrobbleClient.ts
···111111 nowPlayingTaskInterval: number = 5000;
112112 npLogger: Logger;
113113 dupeLogger: Logger;
114114+ deadLogger: Logger;
114115115116 existingScrobble: (playObjPre: PlayObject, existingScrobbles: PlayObject[], log?: boolean) => Promise<PlayMatchResult>
116117···137138 this.logger = childLogger(logger, this.getIdentifier());
138139 this.npLogger = childLogger(this.logger, 'Now Playing');
139140 this.dupeLogger = childLogger(this.logger, 'Dupe');
141141+ this.deadLogger = childLogger(this.logger, 'Dead');
140142 this.notifier = notifier;
141143 this.emitter = emitter;
142144 this.scrobbledPlayObjs = new FixedSizeList<ScrobbledPlayObject>(this.MAX_STORED_SCROBBLES);
···753755754756 const {
755757 options: {
756756- deadLetterRetries = 1
758758+ deadLetterRetries = 3
757759 } = {}
758760 } = this.config;
759761···762764 const processable = this.deadLetterScrobbles.filter(x => x.retries < retries);
763765 const queueStatus = `${processable.length} of ${this.deadLetterScrobbles.length} dead scrobbles have less than ${retries} retries, ${processable.length === 0 ? 'will skip processing.': 'processing now...'}`;
764766 if (processable.length === 0) {
765765- this.logger.verbose({labels: 'Dead Letter'}, queueStatus);
767767+ this.deadLogger.verbose(queueStatus);
766768 return;
767769 }
768768- this.logger.info({labels: 'Dead Letter'}, queueStatus);
770770+ this.logger.info(queueStatus);
769771 if(!this.upstreamRefresh.refreshEnabled) {
770770- this.logger.verbose({labels: 'Dead Letter'}, 'Scrobble refresh is DISABLED. All dead scrobbles will likely always be scrobbled (nothing to check duplicates against).');
772772+ this.deadLogger.verbose('Scrobble refresh is DISABLED. All dead scrobbles will likely always be scrobbled (nothing to check duplicates against).');
771773 }
772774 this.handleQueuedScrobbleRanges();
773775774776 const removedIds = [];
775775- for (const deadScrobble of this.deadLetterScrobbles) {
776776- if (deadScrobble.retries < retries) {
777777- const [scrobbled, dead] = await this.processDeadLetterScrobble(deadScrobble.id);
778778- if (scrobbled) {
779779- removedIds.push(deadScrobble.id);
780780- }
777777+ for (const deadScrobble of processable) {
778778+ const [scrobbled, dead] = await this.processDeadLetterScrobble(deadScrobble.id);
779779+ if (scrobbled) {
780780+ removedIds.push(deadScrobble.id);
781781 }
782782+ await sleep(this.scrobbleSleep);
782783 }
783784 if (removedIds.length > 0) {
784784- this.logger.info({labels: 'Dead Letter'}, `Removed ${removedIds.length} scrobbles from dead letter queue`);
785785+ this.deadLogger.info(`Removed ${removedIds.length} scrobbles from dead letter queue`);
785786 }
786787 }
787788788789 processDeadLetterScrobble = async (id: string): Promise<[boolean, DeadLetterScrobble<PlayObject>?]> => {
789790 const deadScrobbleIndex = this.deadLetterScrobbles.findIndex(x => x.id === id);
791791+ if(deadScrobbleIndex === -1) {
792792+ this.deadLogger.warn(`Could not find a dead scrobble with id ${id}`);
793793+ return [false];
794794+ }
795795+ const deadLabel = {labels: id};
790796 const deadScrobble = this.deadLetterScrobbles[deadScrobbleIndex];
797797+ this.deadLogger.trace(deadLabel, `Processing dead scrobble => ${buildTrackString(deadScrobble.play)}`);
791798792799 if (!(await this.isReady())) {
793793- this.logger.warn({labels: 'Dead Letter'}, 'Cannot process dead letter scrobble because client is not ready.');
800800+ this.deadLogger.warn(deadLabel, 'Cannot process dead letter scrobble because client is not ready.');
794801 return [false, deadScrobble];
795802 }
796803 let historicalPlays: PlayObject[] = [];
···799806 historicalPlays = await this.getSOTScrobblesForPlay(deadScrobble.play);
800807 } catch (e) {
801808 if(e.message === 'Cannot get historical plays due to cached error') {
802802- this.logger.warn(`${buildTrackString(deadScrobble.play)} from Source '${deadScrobble.source}' => Previous error while getting historical scrobbles means this scrobble cannot be compared`);
803803- this.logger.trace(e);
809809+ this.deadLogger.warn(deadLabel, `Previous error while getting historical scrobbles means this scrobble cannot be compared`);
810810+ this.deadLogger.trace(e);
804811 } else {
805805- this.logger.warn(new SimpleError(`${buildTrackString(deadScrobble.play)} from Source '${deadScrobble.source}' => cannot get historical scrobbles`, {cause: e, shortStack: true}));
812812+ this.deadLogger.warn(new SimpleError(`${id} - ${buildTrackString(deadScrobble.play)} from Source '${deadScrobble.source}' => cannot get historical scrobbles`, {cause: e, shortStack: true}));
806813 }
807814 deadScrobble.retries++;
808815 deadScrobble.error = messageWithCauses(e);
809816 deadScrobble.lastRetry = dayjs();
810817 this.deadLetterScrobbles[deadScrobbleIndex] = deadScrobble;
811818 this.updateDeadLetterCache();
819819+ this.emitEvent('updateDeadLetter', {dead: deadScrobble});
812820 return [false, deadScrobble];
813813- } finally {
814814- await sleep(1000);
815821 }
816822 }
817823 const {summary, ...matchResult} = await this.existingScrobble(deadScrobble.play, historicalPlays);
···832838 const scrobbledPlay = await this.scrobble(transformedScrobble);
833839 this.emitEvent('scrobble', {play: transformedScrobble});
834840 this.addScrobbledTrack(transformedScrobble, scrobbledPlay);
841841+ this.removeDeadLetterScrobble(deadScrobble.id)
835842 } catch (e) {
836843837844 const submitError = findCauseByReference(e, ScrobbleSubmitError);
···847854 deadScrobble.retries++;
848855 deadScrobble.error = messageWithCauses(e);
849856 deadScrobble.lastRetry = dayjs();
850850- this.logger.error(new Error(`Could not scrobble ${buildTrackString(transformedScrobble)} from Source '${deadScrobble.source}' due to error`, {cause: e}));
857857+ this.deadLogger.error(new Error(`${id} - Could not scrobble ${buildTrackString(transformedScrobble)} from Source '${deadScrobble.source}' due to error`, {cause: e}));
851858 this.deadLetterScrobbles[deadScrobbleIndex] = deadScrobble;
852859 this.updateDeadLetterCache();
860860+ this.emitEvent('updateDeadLetter', {dead: deadScrobble});
853861 return [false, deadScrobble];
854854- } finally {
855855- await sleep(1000);
856862 }
857857- }
858858- if(deadScrobble !== undefined) {
863863+ } else {
864864+ this.deadLogger.verbose(`Looks like ${buildTrackString(deadScrobble.play)} was already scrobbled!\n${summary}`);
859865 this.removeDeadLetterScrobble(deadScrobble.id)
860866 }
861867862862- return [true];
868868+ return [true, deadScrobble];
863869 }
864870865871 removeDeadLetterScrobble = (id: string) => {
866872 const index = this.deadLetterScrobbles.findIndex(x => x.id === id);
867873 if (index === -1) {
868868- this.logger.warn(`No scrobble found with ID ${id}`, {leaf: 'Dead Letter'});
874874+ this.deadLogger.warn(`No scrobble found with ID ${id}`);
875875+ return;
869876 }
870870- this.logger.info(`Removed scrobble ${buildTrackString(this.deadLetterScrobbles[index].play)} from queue`, {leaf: 'Dead Letter'});
877877+ this.deadLogger.info({labels: id}, `Removed scrobble ${buildTrackString(this.deadLetterScrobbles[index].play)} from queue`);
871878 this.deadLetterScrobbles.splice(index, 1);
872879 this.deadLetterGauge.labels(this.getPrometheusLabels()).set(this.deadLetterScrobbles.length);
873880 this.updateDeadLetterCache();
881881+ this.emitEvent('removeDeadLetter', { dead: { id } });
874882 }
875883876884 removeDeadLetterScrobbles = () => {
···878886 this.updateDeadLetterCache();
879887 this.deadLetterGauge.labels(this.getPrometheusLabels()).set(this.deadLetterScrobbles.length);
880888 this.logger.info('Removed all scrobbles from queue', {leaf: 'Dead Letter'});
881881- }
882882-883883- protected getLatestQueuePlayDate = () => {
884884- if (this.queuedScrobbles.length === 0) {
885885- return undefined;
886886- }
887887- return this.queuedScrobbles[this.queuedScrobbles.length - 1].play.data.playDate;
888889 }
889890890891 queueScrobble = async (data: PlayObject | PlayObject[], source: string) => {
···923924 return (beforeMain + beforeDead) - (this.queuedScrobbles.length + this.deadLetterScrobbles.length);
924925 }
925926926926- protected addDeadLetterScrobble = (data: QueuedScrobble<PlayObject>, error: (Error | string) = 'Unspecified error') => {
927927+ addDeadLetterScrobble = (data: QueuedScrobble<PlayObject>, error: (Error | string) = 'Unspecified error') => {
927928 let eString = '';
928929 if(typeof error === 'string') {
929930 eString = error;
+3-5
src/backend/server/api.ts
···331331332332 (client as AbstractScrobbleClient).logger.verbose('User requested processing of all dead letter scrobbles via API');
333333334334- await (client as AbstractScrobbleClient).processDeadLetterQueue(1000);
335335-336336- const result: DeadLetterScrobble<PlayObject>[] = (client as AbstractScrobbleClient).deadLetterScrobbles;
334334+ res.status(200).send('OK');
337335338338- return res.json(result);
336336+ await ((client as AbstractScrobbleClient).processDeadLetterQueue(1000));
339337 });
340338341339 app.putAsync('/api/dead/:id', clientMiddleFunc(true), async (req, res, next) => {
···358356 return res.status(404).send();
359357 }
360358361361- const [scrobbled, dead] = await (client as AbstractScrobbleClient).processDeadLetterScrobble(deadId);
359359+ const [scrobbled, dead] = await ((client as AbstractScrobbleClient).processDeadLetterScrobble(deadId));
362360363361 if(scrobbled) {
364362 return res.status(200).send();