diff --git a/src/client/components/ActivityDetail.tsx b/src/client/components/ActivityDetail.tsx
index f3f7d405..920b016b 100644
--- a/src/client/components/ActivityDetail.tsx
+++ b/src/client/components/ActivityDetail.tsx
@@ -19,8 +19,10 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
play: {
meta: {
lifecycle: {
+ original,
scrobble
} = {},
+ lifecycle
} = {},
} = {}
}
@@ -29,6 +31,7 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
const [collapsibleOpen, setCollapsibleOpen] = useState(undefined);
return (
+
@@ -37,7 +40,7 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
-
+
@@ -58,11 +61,12 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
- {error !== undefined ? : null}
{scrobble !== undefined ? : null}
+ {error !== undefined ? : null}
+
)
}
\ No newline at end of file
diff --git a/src/client/components/ActivityTimeline.tsx b/src/client/components/ActivityTimeline.tsx
index 258118bc..e5f3257e 100644
--- a/src/client/components/ActivityTimeline.tsx
+++ b/src/client/components/ActivityTimeline.tsx
@@ -6,6 +6,7 @@ import { ErrorAlert } from "./ErrorAlert";
import { IoMdCodeDownload } from "react-icons/io";
import { BiWrench } from "react-icons/bi";
import { IoMusicalNoteOutline } from "react-icons/io5";
+import { PiMagnifyingGlassDuotone } from "react-icons/pi";
import { TbDatabaseEdit } from "react-icons/tb";
import { MdFiberNew } from "react-icons/md";
import { capitalize } from "../../core/StringUtils";
@@ -17,6 +18,7 @@ import { ScrobbleMatchResult } from "./ScrobbleMatchResult";
import { ScrobbleActionResult } from "./ScrobbleActionResult";
import { ExpandCollapse } from "./ExpandCollapse";
import { MSCollapsible } from "./MSCollapsible";
+import { TimelineErrorIcon } from "./timeline/TimelineIcon";
export interface ActivityDetailProps {
@@ -41,7 +43,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
steps = [],
scrobble: {
match,
- payload
+ payload,
+ error
} = {},
scrobble
},
@@ -51,12 +54,12 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
const [scrobbleCollapsibleOpen, setScrobbleCollapsibleOpen] = useState(false);
return (
-
+
-
+
@@ -90,7 +93,7 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
-
+
@@ -112,8 +115,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
-
-
+
+
@@ -136,9 +139,11 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
-
-
-
+ {error !== undefined ? : (
+
+
+
+ )}
diff --git a/src/client/components/ScrobbleActionResult.tsx b/src/client/components/ScrobbleActionResult.tsx
index 51889312..d1dac7ed 100644
--- a/src/client/components/ScrobbleActionResult.tsx
+++ b/src/client/components/ScrobbleActionResult.tsx
@@ -12,6 +12,7 @@ import { JsonDiffPatch } from "./JsonDiff";
import { formatNumber, jdiff } from "../../core/DataUtils";
import { capitalize } from "../../core/StringUtils";
import { MSCollapsible, MSCollapsibleExternalProps } from "./MSCollapsible";
+import { TimelineErrorIcon } from "./timeline/TimelineIcon";
export interface ScrobbleActionResultProps extends MSCollapsibleExternalProps {
result: ScrobbleResult,
@@ -32,13 +33,30 @@ export const ScrobbleActionResult = (props: ScrobbleActionResultProps) => {
collapsibleOpen
} = props;
+ let responseSuffix: JSX.Element,
+ warningsElm: JSX.Element,
+ errorElm: JSX.Element | null;
+
+ if (warnings.length > 0) {
+ warningsElm = warnings
+ }
+ if (error !== undefined) {
+ errorElm = an error
+ }
+
+ if (warningsElm !== undefined && errorElm !== undefined) {
+ responseSuffix = with {warningsElm} and {errorElm};
+ } else if (warningsElm !== undefined || errorElm !== undefined) {
+ responseSuffix = with {warningsElm ?? errorElm};
+ }
+
return (
-
+
-
+
@@ -52,35 +70,40 @@ export const ScrobbleActionResult = (props: ScrobbleActionResultProps) => {
- {response !== undefined ? (
+ {response !== undefined || error !== undefined ? (
-
-
-
+ {error !== undefined ? : (
+
+
+
+ )}
- Received Response{scrobbler !== undefined ? from {capitalize(scrobbler)} : null}{warnings.length > 0 ? with warnings : null}
+ Received Response{scrobbler !== undefined ? from {capitalize(scrobbler)} : null}{responseSuffix !== undefined ? {responseSuffix} : null}
-
- {warnings.length > 0 ? (
-
-
-
- Warnings in Response
-
-
- {warnings.map((x) => {x})}
-
-
-
-
- ) : null}
+
+ {error !== undefined ? : null}
+ {response !== undefined ? : null}
+ {warnings.length > 0 ? (
+
+
+
+ Warnings in Response
+
+
+ {warnings.map((x) => {x})}
+
+
+
+
+ ) : null}
+
diff --git a/src/client/components/TransformSteps.tsx b/src/client/components/TransformSteps.tsx
index c913e2aa..c34d4402 100644
--- a/src/client/components/TransformSteps.tsx
+++ b/src/client/components/TransformSteps.tsx
@@ -1,10 +1,10 @@
-import { ComponentProps } from "react"
-import { Timeline, Icon, Span, Stack, Heading } from '@chakra-ui/react';
+import { ComponentProps, Fragment } from "react"
+import { Timeline, Icon, Span, Stack, Heading, Box } from '@chakra-ui/react';
import { JsonPlayObject, LifecycleStep } from "../../core/Atomic";
import { PlayData } from "./PlayData";
import { ErrorAlert } from "./ErrorAlert";
import { BiWrench } from "react-icons/bi";
-import { IoMusicalNoteOutline } from "react-icons/io5";
+import { MdMusicNote } from "react-icons/md";
import { ChakraCodeBlockShort, ChakraPlainBlockShort } from "./CodeBlock";
import { JsonDiffPatch } from "./JsonDiff";
import { jdiff } from "../../core/DataUtils";
@@ -25,47 +25,63 @@ export const TransformSteps = (props: LifeycleStepsTimelineProps) => {
let currentPlay: JsonPlayObject | false = JSON.parse(JSON.stringify(original));
return (
-
+
{steps.map((x, index) => {
+ const {
+ patch,
+ inputs,
+ source,
+ name
+ } = x;
let err: Error;
- const left = JSON.parse(JSON.stringify(currentPlay));
- if (currentPlay !== false) {
+ const left = currentPlay !== false ? JSON.parse(JSON.stringify(currentPlay)) : false;
+ if (currentPlay !== false && patch !== undefined) {
try {
- currentPlay = jdiff.patch(currentPlay, x.patch) as JsonPlayObject;
+ currentPlay = jdiff.patch(currentPlay, patch) as JsonPlayObject;
} catch (e) {
err = new Error('Could not patch Play object', { cause: e });
currentPlay = false;
}
+ } else {
+ currentPlay = false;
}
+ let diffElm: JSX.Element;
+ if(left !== false && currentPlay !== false) {
+ diffElm =
+
+ ;
+ } else if(patch !== undefined) {
+ diffElm = ;
+ }
+ const showAnyDetails = inputs !== undefined || diffElm !== undefined || err !== undefined;
return
-
+
- {x.name} with {x.source}
+ {name} with {source}
-
- Diff
- {err !== undefined ? : null}
- {left !== false && currentPlay !== false ? (
-
-
-
- ) : }
- Inputs
-
- {x.inputs.map((y) => {
- return
- })}
+ {showAnyDetails ?
+
+ {err !== undefined ? : null}
+ {diffElm !== undefined ? Diff{diffElm} : null }
+ {inputs !== undefined ? (
+
+ Inputs
+
+ {x.inputs.map((y) => {
+ return
+ })}
+ ) : null}
-
+ : null }
})}
@@ -74,8 +90,8 @@ export const TransformSteps = (props: LifeycleStepsTimelineProps) => {
-
-
+
+
diff --git a/src/client/components/timeline/TimelineIcon.tsx b/src/client/components/timeline/TimelineIcon.tsx
new file mode 100644
index 00000000..0a7470dc
--- /dev/null
+++ b/src/client/components/timeline/TimelineIcon.tsx
@@ -0,0 +1,8 @@
+import { AiOutlineExclamationCircle } from "react-icons/ai";
+import { Icon } from '@chakra-ui/react';
+
+export const TimelineErrorIcon = () => (
+
+
+
+ )
\ No newline at end of file
diff --git a/src/stories/ActivityTimeline.stories.tsx b/src/stories/ActivityTimeline.stories.tsx
index b2b0c2ab..b58468a6 100644
--- a/src/stories/ActivityTimeline.stories.tsx
+++ b/src/stories/ActivityTimeline.stories.tsx
@@ -7,7 +7,7 @@ import { ActivityTimeline } from "../client/components/ActivityTimeline";
import {Provider} from "../client/components/Provider";
import { generateJsonPlays } from "../backend/tests/utils/PlayTestUtils.js";
import { ErrorLike, JsonPlayObject, PlayLifecycle } from "../core/Atomic.js";
-import { examplePlay } from "./storyUtils.js";
+import { examplePlay, lastfmErrorExample } from "./storyUtils.js";
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = preview.meta({
@@ -32,4 +32,11 @@ decorators: [
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const ActivityTimelineStory = meta.story({
//render: function Render(args) { return () }
-});
\ No newline at end of file
+});
+
+export const ScrobbleError = meta.story({
+ args: {
+ play: lastfmErrorExample()
+ }
+});
+
diff --git a/src/stories/List.stories.tsx b/src/stories/List.stories.tsx
index f3a64d4a..f77bfcf7 100644
--- a/src/stories/List.stories.tsx
+++ b/src/stories/List.stories.tsx
@@ -7,7 +7,7 @@ import { CList } from "../client/components/List";
import {Provider} from "../client/components/Provider";
import { generateJsonPlays } from "../backend/tests/utils/PlayTestUtils.js";
import { ErrorLike, JsonPlayObject } from "../core/Atomic.js";
-import {examplePlay} from './storyUtils.js';
+import {examplePlay, lastfmErrorExample} from './storyUtils.js';
const stack = "Scrobble Submit Error: Failed to submit to Listenbrainz (listen_type single)\n at ListenbrainzApiClient.submitListen (/app/src/backend/common/vendor/ListenbrainzApiClient.ts:246:19)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async ListenbrainzScrobbler.doScrobble (/app/src/backend/scrobblers/ListenbrainzScrobbler.ts:87:28)\n at async ListenbrainzScrobbler.scrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:679:28)\n at async ListenbrainzScrobbler.processDeadLetterScrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:920:39)\n at async ListenbrainzScrobbler.processDeadLetterQueue (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:894:43)\n at async PromisePoolExecutor.handler (/app/src/backend/tasks/heartbeatClients.ts:35:21)\n at async PromisePoolExecutor.waitForActiveTaskToFinish (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:375:9)\n at async PromisePoolExecutor.waitForProcessingSlot (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:368:13)\n at async PromisePoolExecutor.process (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:354:13)";
@@ -28,7 +28,7 @@ const errorExample: ErrorLike = {
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = preview.meta({
- title: 'List',
+ title: 'Examples/ActivityLog',
component: CList,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
@@ -38,17 +38,11 @@ const meta = preview.meta({
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
args: {
- data: (generateJsonPlays(3, undefined, {source: 'Spotify'})).map((x, index) => {
- const mod = (index + 1) % 3;
- switch(mod) {
- case 1:
- return {play: x, status: 'queued'}
- case 2:
- return {play: examplePlay(), status: 'scrobbled'}
- case 0:
- return {play: x, status: 'error', error: errorExample};
- }
- }),
+ data:[
+ ...generateJsonPlays(2).map((x) => ({play: x, status: 'queued'})),
+ {play: examplePlay(), status: 'scrobbled'},
+ {play: lastfmErrorExample(), status: 'error'}
+ ] ,
},
decorators: [
(Story) => (),
diff --git a/src/stories/storyUtils.ts b/src/stories/storyUtils.ts
index 6b4d1080..a6c780ae 100644
--- a/src/stories/storyUtils.ts
+++ b/src/stories/storyUtils.ts
@@ -1099,4 +1099,324 @@ export const examplePlay = (): JsonPlayObject => ({
},
lifecycle: exampleLifecycle()
}
-});
\ No newline at end of file
+});
+
+const lastfmErrorLifcycle: PlayLifecycle = {
+ "input": {
+ "track": {
+ "album": {
+ "album_type": "single",
+ "artists": [
+ {
+ "external_urls": {
+ "spotify": "https://open.spotify.com/artist/4l8MDydHy2RGwcGscG0uCB"
+ },
+ "href": "https://api.spotify.com/v1/artists/4l8MDydHy2RGwcGscG0uCB",
+ "id": "4l8MDydHy2RGwcGscG0uCB",
+ "name": "Mpho.Wav",
+ "type": "artist",
+ "uri": "spotify:artist:4l8MDydHy2RGwcGscG0uCB"
+ }
+ ],
+ "external_urls": {
+ "spotify": "https://open.spotify.com/album/1TCD8jrQdFclnpYVQxjon2"
+ },
+ "href": "https://api.spotify.com/v1/albums/1TCD8jrQdFclnpYVQxjon2",
+ "id": "1TCD8jrQdFclnpYVQxjon2",
+ "images": [
+ {
+ "height": 640,
+ "url": "https://i.scdn.co/image/ab67616d0000b273b0e9752208d1c426fcda8820",
+ "width": 640
+ },
+ {
+ "height": 300,
+ "url": "https://i.scdn.co/image/ab67616d00001e02b0e9752208d1c426fcda8820",
+ "width": 300
+ },
+ {
+ "height": 64,
+ "url": "https://i.scdn.co/image/ab67616d00004851b0e9752208d1c426fcda8820",
+ "width": 64
+ }
+ ],
+ "name": "Into The Deep",
+ "release_date": "2026-02-27",
+ "release_date_precision": "day",
+ "total_tracks": 3,
+ "type": "album",
+ "uri": "spotify:album:1TCD8jrQdFclnpYVQxjon2"
+ },
+ "artists": [
+ {
+ "external_urls": {
+ "spotify": "https://open.spotify.com/artist/4l8MDydHy2RGwcGscG0uCB"
+ },
+ "href": "https://api.spotify.com/v1/artists/4l8MDydHy2RGwcGscG0uCB",
+ "id": "4l8MDydHy2RGwcGscG0uCB",
+ "name": "Mpho.Wav",
+ "type": "artist",
+ "uri": "spotify:artist:4l8MDydHy2RGwcGscG0uCB"
+ }
+ ],
+ "disc_number": 1,
+ "duration_ms": 318875,
+ "explicit": false,
+ "external_ids": {
+ "isrc": "US23A9495906"
+ },
+ "external_urls": {
+ "spotify": "https://open.spotify.com/track/3Uar9flTUlakmFt9nkJdNZ"
+ },
+ "href": "https://api.spotify.com/v1/tracks/3Uar9flTUlakmFt9nkJdNZ",
+ "id": "3Uar9flTUlakmFt9nkJdNZ",
+ "is_local": false,
+ "name": "Bab'omdala",
+ "popularity": 26,
+ "preview_url": "https://p.scdn.co/mp3-preview/ec6e58ff47870192adfa02cd02b963e269ff330e?cid=f85f5aa4211f4ea78aa364547798b340",
+ "track_number": 1,
+ "type": "track",
+ "uri": "spotify:track:3Uar9flTUlakmFt9nkJdNZ"
+ },
+ "played_at": "2026-03-10T18:20:34.606Z",
+ "context": {
+ "type": "album",
+ "external_urls": {
+ "spotify": "https://open.spotify.com/album/4JgbPlF06nOTO03mSBSgtq"
+ },
+ "href": "https://api.spotify.com/v1/albums/4JgbPlF06nOTO03mSBSgtq",
+ "uri": "spotify:album:4JgbPlF06nOTO03mSBSgtq"
+ }
+ },
+ "original": {
+ "data": {
+ "album": "Into The Deep",
+ "track": "Bab'omdala",
+ "duration": 318.875,
+ "isrc": "US23A9495906",
+ "artists": [
+ "Mpho.Wav"
+ ],
+ "albumArtists": [],
+ // @ts-ignore
+ "playDate": "2026-03-10T18:20:34.606Z",
+ // @ts-ignore
+ "playDateCompleted": "2026-03-10T18:20:34.606Z",
+ "meta": {
+ "spotify": {
+ "track": "3Uar9flTUlakmFt9nkJdNZ",
+ "artist": [
+ "4l8MDydHy2RGwcGscG0uCB"
+ ],
+ "albumArtist": [],
+ "album": "1TCD8jrQdFclnpYVQxjon2"
+ },
+ "brainz": {
+ "trackNumber": 1
+ }
+ }
+ },
+ "meta": {
+ "deviceId": "NoDevice-SingleUser",
+ "source": "Spotify",
+ "musicService": "Spotify",
+ "trackId": "3Uar9flTUlakmFt9nkJdNZ",
+ "scrobbleTsSOC": 2,
+ "newFromSource": false,
+ "url": {
+ "web": "https://open.spotify.com/track/3Uar9flTUlakmFt9nkJdNZ"
+ },
+ "art": {
+ "album": "https://i.scdn.co/image/ab67616d00001e02b0e9752208d1c426fcda8820"
+ }
+ }
+ },
+ "steps": [
+ {
+ "name": "preCompare",
+ "source": "Spotify - default"
+ }
+ ],
+ "scrobble": {
+ "payload": {
+ "artist": "Mpho.Wav",
+ "track": "Bab'omdala",
+ "album": "Into The Deep",
+ "timestamp": 1773166834,
+ "duration": 318.875
+ },
+ "error": {
+ "showStopper": false,
+ "name": "Scrobble Submit Error",
+ "payload": {
+ "artist": "Mpho.Wav",
+ "track": "Bab'omdala",
+ "album": "Into The Deep",
+ "timestamp": 1773166834,
+ "duration": 318.875
+ },
+ "message": "Failed to submit scrobble to Last.fm",
+ "stack": "Scrobble Submit Error: Failed to submit scrobble to Last.fm\n at LastfmApiClient.scrobble (/app/src/backend/common/vendor/LastfmApiClient.ts:447:19)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async LastfmScrobbler.doScrobble (/app/src/backend/scrobblers/LastfmScrobbler.ts:88:28)\n at async LastfmScrobbler.scrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:623:28)\n at async LastfmScrobbler.processDeadLetterScrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:937:39)\n at async LastfmScrobbler.processDeadLetterQueue (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:882:43)\n at async PromisePoolExecutor.handler (/app/src/backend/tasks/heartbeatClients.ts:35:21)\n at async PromisePoolExecutor.waitForActiveTaskToFinish (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:375:9)\n at async PromisePoolExecutor.waitForProcessingSlot (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:368:13)\n at async PromisePoolExecutor.process (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:354:13)",
+ "cause": {
+ "showStopper": false,
+ "name": "Error",
+ "message": "Last.fm ignored scrobble => (Code 1) (No error message returned) -- See https://www.last.fm/api/errorcodes for more information",
+ "stack": "Error: Last.fm ignored scrobble => (Code 1) (No error message returned) -- See https://www.last.fm/api/errorcodes for more information\n at LastfmApiClient.scrobble (/app/src/backend/common/vendor/LastfmApiClient.ts:424:23)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async LastfmScrobbler.doScrobble (/app/src/backend/scrobblers/LastfmScrobbler.ts:88:28)\n at async LastfmScrobbler.scrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:623:28)\n at async LastfmScrobbler.processDeadLetterScrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:937:39)\n at async LastfmScrobbler.processDeadLetterQueue (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:882:43)\n at async PromisePoolExecutor.handler (/app/src/backend/tasks/heartbeatClients.ts:35:21)\n at async PromisePoolExecutor.waitForActiveTaskToFinish (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:375:9)\n at async PromisePoolExecutor.waitForProcessingSlot (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:368:13)\n at async PromisePoolExecutor.process (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:354:13)"
+ }
+ },
+ "match": {
+ "match": false,
+ "score": 0.32285714285714284,
+ "breakdowns": [
+ "Artist: 0.00 * 0.3 = 0.00",
+ "Title: 0.06 * 0.4 = 0.02",
+ "Time: (Fuzzy) 0.6 * 0.5 = 0.30",
+ "Time Detail => Existing: 14:15:15-04:00 - Candidate: 14:20:34-04:00 | Temporal Sameness: Fuzzy | Play Diff: 319s (Needed <10s) | Fuzzy Duration Diff: 0s (Needed <= 10s) | Range Comparison N/A",
+ "Score 0.32 => No Match"
+ ],
+ "reason": "Score 0.32 => No Match",
+ "closestMatchedPlay": {
+ "data": {
+ "artists": [
+ "Monique Bingham"
+ ],
+ "track": "Deep In The Bottom (of Africa) (feat. Black Coffee) - JazzWRLD Remix Edit (Radio Edit)",
+ "album": "Deep In The Bottom (Of Africa) The Remixes [Pt. 1]",
+ // @ts-ignore
+ "playDate": "2026-03-10T18:15:15.000Z"
+ },
+ "meta": {
+ "nowPlaying": false,
+ "mbid": "",
+ "source": "Lastfm",
+ "url": {
+ "web": "https://www.last.fm/music/Monique+Bingham/_/Deep+In+The+Bottom+(of+Africa)+(feat.+Black+Coffee)+-+JazzWRLD+Remix+Edit+(Radio+Edit)"
+ },
+ "lifecycle": {
+ "input": {
+ "artist": {
+ "url": "https://www.last.fm/music/Monique+Bingham",
+ "name": "Monique Bingham",
+ "image": [
+ {
+ "size": "small",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png"
+ },
+ {
+ "size": "medium",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png"
+ },
+ {
+ "size": "large",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png"
+ },
+ {
+ "size": "extralarge",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
+ }
+ ],
+ "mbid": ""
+ },
+ "date": {
+ "uts": "1773166515",
+ "#text": "10 Mar 2026, 18:15"
+ },
+ "mbid": "",
+ "name": "Deep In The Bottom (of Africa) (feat. Black Coffee) - JazzWRLD Remix Edit (Radio Edit)",
+ "image": [
+ {
+ "size": "small",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png"
+ },
+ {
+ "size": "medium",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png"
+ },
+ {
+ "size": "large",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png"
+ },
+ {
+ "size": "extralarge",
+ "#text": "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
+ }
+ ],
+ "url": "https://www.last.fm/music/Monique+Bingham/_/Deep+In+The+Bottom+(of+Africa)+(feat.+Black+Coffee)+-+JazzWRLD+Remix+Edit+(Radio+Edit)",
+ "streamable": "0",
+ "album": {
+ "mbid": "",
+ "#text": "Deep In The Bottom (Of Africa) The Remixes [Pt. 1]"
+ },
+ "loved": "0"
+ },
+ "original": {
+ "data": {
+ "track": "Deep In The Bottom (of Africa) (feat. Black Coffee) - JazzWRLD Remix Edit (Radio Edit)",
+ "album": "Deep In The Bottom (Of Africa) The Remixes [Pt. 1]",
+ "artists": [
+ "Monique Bingham"
+ ],
+ // @ts-ignore
+ "playDate": "2026-03-10T18:15:15.000Z"
+ },
+ "meta": {
+ "nowPlaying": false,
+ "mbid": "",
+ "source": "Lastfm",
+ "url": {
+ "web": "https://www.last.fm/music/Monique+Bingham/_/Deep+In+The+Bottom+(of+Africa)+(feat.+Black+Coffee)+-+JazzWRLD+Remix+Edit+(Radio+Edit)"
+ }
+ }
+ },
+ "steps": []
+ }
+ }
+ }
+ }
+ }
+}
+
+export const lastfmErrorExample = (): JsonPlayObject => ({
+ "data": {
+ "album": "Into The Deep",
+ "track": "Bab'omdala",
+ "duration": 318.875,
+ "isrc": "US23A9495906",
+ "artists": [
+ "Mpho.Wav"
+ ],
+ "albumArtists": [],
+ // @ts-ignore
+ "playDate": "2026-03-10T18:20:34.606Z",
+ // @ts-ignore
+ "playDateCompleted": "2026-03-10T18:20:34.606Z",
+ "meta": {
+ "spotify": {
+ "track": "3Uar9flTUlakmFt9nkJdNZ",
+ "artist": [
+ "4l8MDydHy2RGwcGscG0uCB"
+ ],
+ "albumArtist": [],
+ "album": "1TCD8jrQdFclnpYVQxjon2"
+ },
+ "brainz": {
+ "trackNumber": 1
+ }
+ }
+ },
+ "meta": {
+ "deviceId": "NoDevice-SingleUser",
+ "source": "Spotify",
+ "musicService": "Spotify",
+ "trackId": "3Uar9flTUlakmFt9nkJdNZ",
+ "scrobbleTsSOC": 2,
+ "newFromSource": false,
+ "url": {
+ "web": "https://open.spotify.com/track/3Uar9flTUlakmFt9nkJdNZ"
+ },
+ "art": {
+ "album": "https://i.scdn.co/image/ab67616d00001e02b0e9752208d1c426fcda8820"
+ },
+ lifecycle: lastfmErrorLifcycle
+ }
+ });
\ No newline at end of file