[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.

feat: Add dead letter error to data

FoxxMD (Oct 13, 2023, 12:12 PM EDT) 57f2ebf2 884134ca

+71 -38
+9
package-lock.json
··· 23 23 "ajv": "^7.2.4", 24 24 "better-sse": "^0.8.0", 25 25 "body-parser": "^1.19.0", 26 + "clsx": "^2.0.0", 26 27 "common-tags": "^1.8.2", 27 28 "compare-versions": "^4.1.2", 28 29 "concat-stream": "^2.0.0", ··· 7422 7423 "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", 7423 7424 "engines": { 7424 7425 "node": ">=4" 7426 + } 7427 + }, 7428 + "node_modules/clsx": { 7429 + "version": "2.0.0", 7430 + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", 7431 + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", 7432 + "engines": { 7433 + "node": ">=6" 7425 7434 } 7426 7435 }, 7427 7436 "node_modules/co": {
+1
package.json
··· 60 60 "ajv": "^7.2.4", 61 61 "better-sse": "^0.8.0", 62 62 "body-parser": "^1.19.0", 63 + "clsx": "^2.0.0", 63 64 "common-tags": "^1.8.2", 64 65 "compare-versions": "^4.1.2", 65 66 "concat-stream": "^2.0.0",
+13 -7
src/backend/scrobblers/AbstractScrobbleClient.ts
··· 27 27 import {compareScrobbleArtists, compareScrobbleTracks, normalizeStr} from "../utils/StringUtils"; 28 28 import {UpstreamError} from "../common/errors/UpstreamError"; 29 29 import {nanoid} from "nanoid"; 30 - import {ErrorWithCause} from "pony-cause"; 30 + import {ErrorWithCause, messageWithCauses} from "pony-cause"; 31 31 import {de} from "@faker-js/faker"; 32 32 import {del} from "superagent"; 33 33 ··· 537 537 this.addScrobbledTrack(currQueuedPlay.play, scrobbledPlay); 538 538 } catch (e) { 539 539 if (e instanceof UpstreamError && e.showStopper === false) { 540 - this.addDeadLetterScrobble(currQueuedPlay); 541 - this.logger.warn(`Could not scrobble ${buildTrackString(currQueuedPlay.play)} from Source '${currQueuedPlay.source}' but error was not show stopping. Adding scrobble to Dead Letter Queue and will retry on next heartbeat.`); 540 + this.addDeadLetterScrobble(currQueuedPlay, e); 541 + this.logger.warn(new ErrorWithCause(`Could not scrobble ${buildTrackString(currQueuedPlay.play)} from Source '${currQueuedPlay.source}' but error was not show stopping. Adding scrobble to Dead Letter Queue and will retry on next heartbeat.`, {cause: e})); 542 542 } else { 543 543 const processError = new ErrorWithCause('Error occurred while trying to scrobble', {cause: e}); 544 544 //this.logger.error(processError); ··· 621 621 this.addScrobbledTrack(deadScrobble.play, scrobbledPlay); 622 622 } catch (e) { 623 623 deadScrobble.retries++; 624 + deadScrobble.error = messageWithCauses(e); 624 625 deadScrobble.lastRetry = dayjs(); 625 - this.logger.error(`Could not scrobble ${buildTrackString(deadScrobble.play)} from Source '${deadScrobble.source}' due to error`, {leaf: 'Dead Letter'}); 626 - this.logger.error(e); 626 + this.logger.error(new ErrorWithCause(`Could not scrobble ${buildTrackString(deadScrobble.play)} from Source '${deadScrobble.source}' due to error`, {cause: e})); 627 627 this.deadLetterScrobbles[deadScrobbleIndex] = deadScrobble; 628 628 return [false, deadScrobble]; 629 629 } finally { ··· 663 663 this.queuedScrobbles.sort((a, b) => sortByOldestPlayDate(a.play, b.play)); 664 664 } 665 665 666 - protected addDeadLetterScrobble = (data: QueuedScrobble<PlayObject>) => { 667 - this.deadLetterScrobbles.push({id: nanoid(), retries: 0, ...data}); 666 + protected addDeadLetterScrobble = (data: QueuedScrobble<PlayObject>, error: (Error | string) = 'Unspecified error') => { 667 + let eString = ''; 668 + if(typeof error === 'string') { 669 + eString = error; 670 + } else { 671 + eString = messageWithCauses(error); 672 + } 673 + this.deadLetterScrobbles.push({id: nanoid(), retries: 0, error: eString, ...data}); 668 674 this.deadLetterScrobbles.sort((a, b) => sortByOldestPlayDate(a.play, b.play)); 669 675 } 670 676
+8
src/client/components/loading/Loading.css
··· 1 + .connected.loading { 2 + display: inline; 3 + } 4 + .loading { 5 + height: 35px; 6 + fill: white; 7 + display: none; 8 + }
+30
src/client/components/loading/Loading.tsx
··· 1 + import React from 'react'; 2 + import clsx from 'clsx'; 3 + import './Loading.css'; 4 + 5 + /*https://codepen.io/nikhil8krishnan/pen/rVoXJa*/ 6 + export const Loading = (props: {show?: boolean}) => { 7 + const {show = false} = props || {}; 8 + const classes = ['loading']; 9 + if(show) { 10 + classes.push('connected'); 11 + } 12 + return <svg className={clsx(classes)} version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg" 13 + x="0px" y="0px" 14 + xmlnsXlink="http://www.w3.org/1999/xlink" 15 + viewBox="0 0 100 100" xmlSpace="preserve"> 16 + <path 17 + d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"> 18 + <animateTransform 19 + attributeName="transform" 20 + attributeType="XML" 21 + type="rotate" 22 + dur="1s" 23 + from="0 50 50" 24 + to="360 50 50" 25 + repeatCount="indefinite"/> 26 + </path> 27 + </svg>; 28 + } 29 + 30 + export default Loading;
+7 -6
src/client/deadLetter/DeadPage.tsx
··· 1 - import React, {useCallback, useEffect} from 'react'; 1 + import React, {useCallback, useEffect, useState} from 'react'; 2 2 import PlayDisplay from "../components/PlayDisplay"; 3 3 import {recentIncludes} from "../../core/Atomic"; 4 4 import {useSearchParams} from "react-router-dom"; ··· 27 27 isSuccess 28 28 } = useGetDeadQuery({name: searchParams.get('name'), type: searchParams.get('type')}); 29 29 30 - const [removeDeadFetch] = useRemoveDeadSingleMutation(); 31 - const [retryDeadFetch] = useProcessDeadSingleMutation(); 30 + const [removeDeadFetch, removeResult] = useRemoveDeadSingleMutation(); 31 + const [retryDeadFetch, processResult] = useProcessDeadSingleMutation(); 32 32 33 33 const retryDead = useCallback((id: string) => retryDeadFetch({name: searchParams.get('name'), type: searchParams.get('type'), id}), [retryDeadFetch, searchParams]); 34 34 const removeDead = useCallback((id: string) => removeDeadFetch({name: searchParams.get('name'), type: searchParams.get('type'), id}), [removeDeadFetch, searchParams]); ··· 45 45 <ul>{data.map(x => (<li className="my-2.5" key={x.id}> 46 46 <div className="text-lg"><PlayDisplay data={x.play} buildOptions={displayOpts}/></div> 47 47 <div><span className="font-semibold">Source</span>:{x.source.replace('Source -', '')}</div> 48 + <div><span className="font-semibold">Retries</span>: {x.retries}</div> 48 49 <div><span className="font-semibold">Last Retried</span>: {x.lastRetry === undefined ? 'Never' : dayjs.duration(dayjs().diff(dayjs(x.lastRetry))).humanize(true)}</div> 49 - <div><span className="font-semibold">Retries</span>: {x.retries}</div> 50 - <div onClick={() => retryDead(x.id)} className="capitalize underline cursor-pointer">Retry</div> 51 - <div onClick={() => removeDead(x.id)} className="capitalize underline cursor-pointer">Remove</div> 50 + <div><span className="font-semibold">Error</span>: <span className="font-mono text-sm">{x.error}</span></div> 51 + <div onClick={() => retryDead(x.id)} className="capitalize underline cursor-pointer max-w-fit">Retry</div> 52 + <div onClick={() => removeDead(x.id)} className="capitalize underline cursor-pointer max-w-fit">Remove</div> 52 53 </li>))}</ul> 53 54 </div> 54 55 </div>
-8
src/client/logs/LogsSection.css
··· 1 - .connected.loading { 2 - display: inline; 3 - } 4 - .loading { 5 - height: 35px; 6 - fill: white; 7 - display: none; 8 - } 9 1 .line { 10 2 display: block; 11 3 white-space: pre-wrap;
+2 -17
src/client/logs/LogsSection.tsx
··· 6 6 import {useGetLogsQuery, useLazySetLevelQuery, logsApi} from "./logsApi"; 7 7 import {connect, ConnectedProps} from "react-redux"; 8 8 import {RootState} from "../store"; 9 + import Loading from "../components/loading/Loading"; 9 10 10 11 let logBuffer: { message: string, id: string, level: string }[] = []; 11 12 ··· 71 72 <div className="shadow-md rounded my-6 bg-gray-500 text-white"> 72 73 <div className="p-3 font-semibold bg-gray-700 text-white"> 73 74 <h2>Log (Most Recent) 74 - {/*https://codepen.io/nikhil8krishnan/pen/rVoXJa*/} 75 - <svg className="loading connected" version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg" 76 - x="0px" y="0px" 77 - xmlnsXlink="http://www.w3.org/1999/xlink" 78 - viewBox="0 0 100 100" xmlSpace="preserve"> 79 - <path 80 - d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"> 81 - <animateTransform 82 - attributeName="transform" 83 - attributeType="XML" 84 - type="rotate" 85 - dur="1s" 86 - from="0 50 50" 87 - to="360 50 50" 88 - repeatCount="indefinite"/> 89 - </path> 90 - </svg> 75 + <Loading show/> 91 76 </h2> 92 77 </div> 93 78 <div className="p-6">
+1
src/core/Atomic.ts
··· 199 199 id: string 200 200 retries: number 201 201 lastRetry?: RetryType 202 + error: string 202 203 }