[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(ui): Implement scrobble action

FoxxMD (Mar 19, 2026, 6:38 PM UTC) 9f9f5655 e6218113

+121 -25
+23 -17
src/client/components/ActivityTimeline.tsx
··· 6 6 import { IoMdCodeDownload } from "react-icons/io"; 7 7 import { BiWrench } from "react-icons/bi"; 8 8 import { IoMusicalNoteOutline } from "react-icons/io5"; 9 + import { TbDatabaseEdit } from "react-icons/tb"; 9 10 import { MdFiberNew } from "react-icons/md"; 10 11 import { capitalize } from "../../core/StringUtils"; 11 12 import { shortTodayAwareFormat, todayAwareFormat } from "../../core/TimeUtils"; ··· 13 14 import { ChakraCodeBlockShort, ChakraPlainBlockShort } from "./CodeBlock"; 14 15 import { TransformSteps } from "./TransformSteps"; 15 16 import { ScrobbleMatchResult } from "./ScrobbleMatchResult"; 17 + import { ScrobbleActionResult } from "./ScrobbleActionResult"; 16 18 17 19 18 20 export interface ActivityDetailProps { ··· 35 37 steps = [], 36 38 scrobble: { 37 39 match, 38 - } = {} 40 + payload 41 + } = {}, 42 + scrobble 39 43 }, 40 44 } = {} 41 45 } = play; ··· 112 116 </Timeline.Content> 113 117 </Timeline.Item> 114 118 ) : null} 115 - {/* <Timeline.Item> 116 - <Timeline.Connector> 117 - <Timeline.Separator /> 118 - <Timeline.Indicator> 119 - <Icon fontSize="xs"> 120 - <IoMusicalNoteOutline /> 121 - </Icon> 122 - </Timeline.Indicator> 123 - </Timeline.Connector> 124 - <Timeline.Content> 125 - <Timeline.Title> 126 - <Span color="fg.muted">Created new</Span> Play 127 - </Timeline.Title> 128 - test 129 - </Timeline.Content> 130 - </Timeline.Item> */} 119 + {payload !== undefined ? ( 120 + <Timeline.Item> 121 + <Timeline.Connector> 122 + <Timeline.Separator /> 123 + <Timeline.Indicator> 124 + <Icon fontSize="xs"> 125 + <TbDatabaseEdit /> 126 + </Icon> 127 + </Timeline.Indicator> 128 + </Timeline.Connector> 129 + <Timeline.Content gap="4"> 130 + <Timeline.Title> 131 + <Span color="fg.muted">Attmpted to</Span> Scrobble 132 + </Timeline.Title> 133 + <ScrobbleActionResult result={scrobble} scrobbler="Koito"/> 134 + </Timeline.Content> 135 + </Timeline.Item> 136 + ) : null} 131 137 </Timeline.Root> 132 138 ) 133 139 }
+84
src/client/components/ScrobbleActionResult.tsx
··· 1 + import { ComponentProps, Fragment } from "react" 2 + import { Timeline, Icon, Span, Stack, Heading, Tabs, DataList, Alert, List } from '@chakra-ui/react'; 3 + import { JsonPlayObject, LifecycleStep, PlayLifecycle, PlayMatchResult, ScrobbleResult } from "../../core/Atomic"; 4 + import { PlayData } from "./PlayData"; 5 + import { ErrorAlert } from "./ErrorAlert"; 6 + import { BiWrench } from "react-icons/bi"; 7 + import { IoMusicalNoteOutline } from "react-icons/io5"; 8 + import { HiOutlineCloudUpload, HiOutlineCloudDownload } from "react-icons/hi"; 9 + import { LuCheck, LuCircleX, LuX } from "react-icons/lu"; 10 + import { ChakraCodeBlockShort, ChakraPlainBlock, ChakraPlainBlockShort } from "./CodeBlock"; 11 + import { JsonDiffPatch } from "./JsonDiff"; 12 + import { formatNumber, jdiff } from "../../core/DataUtils"; 13 + import { capitalize } from "../../core/StringUtils"; 14 + 15 + export interface ScrobbleActionResultProps { 16 + result: ScrobbleResult, 17 + scrobbler?: string 18 + } 19 + 20 + export const ScrobbleActionResult = (props: ScrobbleActionResultProps) => { 21 + 22 + const { 23 + result: { 24 + error, 25 + warnings = [], 26 + payload, 27 + response, 28 + mergedScrobble 29 + } = {}, 30 + scrobbler 31 + } = props; 32 + 33 + return ( 34 + <Timeline.Root size="lg" variant="subtle" maxW="lg" css={{ "--timeline-separator-display": 'block' }}> 35 + <Timeline.Item> 36 + <Timeline.Connector> 37 + <Timeline.Separator /> 38 + <Timeline.Indicator> 39 + <Icon fontSize="xs"> 40 + <HiOutlineCloudUpload /> 41 + </Icon> 42 + </Timeline.Indicator> 43 + </Timeline.Connector> 44 + <Timeline.Content> 45 + <Timeline.Title> 46 + <Span color="fg.muted">Sent</Span> Scrobble Payload{scrobbler !== undefined ? <Fragment><Span color="fg.muted">to</Span> {capitalize(scrobbler)}</Fragment> : null} 47 + </Timeline.Title> 48 + <ChakraCodeBlockShort code={payload} language="json" maxLines={20} /> 49 + </Timeline.Content> 50 + </Timeline.Item> 51 + {response !== undefined ? ( 52 + <Timeline.Item> 53 + <Timeline.Connector> 54 + <Timeline.Separator /> 55 + <Timeline.Indicator> 56 + <Icon fontSize="xs"> 57 + <HiOutlineCloudDownload /> 58 + </Icon> 59 + </Timeline.Indicator> 60 + </Timeline.Connector> 61 + <Timeline.Content> 62 + <Timeline.Title> 63 + <Span color="fg.muted">Received</Span> Response{scrobbler !== undefined ? <Fragment><Span color="fg.muted">from</Span> {capitalize(scrobbler)}</Fragment> : null}{warnings.length > 0 ? <Span color="orange.solid"> with warnings</Span> : null} 64 + </Timeline.Title> 65 + <ChakraCodeBlockShort code={response} language="json" maxLines={20} /> 66 + {warnings.length > 0 ? ( 67 + <Alert.Root status="warning"> 68 + <Alert.Indicator /> 69 + <Alert.Content> 70 + <Alert.Title>Warnings in Response</Alert.Title> 71 + <Alert.Description> 72 + <List.Root> 73 + {warnings.map((x) => <List.Item>{x}</List.Item>)} 74 + </List.Root> 75 + </Alert.Description> 76 + </Alert.Content> 77 + </Alert.Root> 78 + ) : null} 79 + </Timeline.Content> 80 + </Timeline.Item> 81 + ) : null} 82 + </Timeline.Root> 83 + ) 84 + }
+10 -8
src/core/Atomic.ts
··· 292 292 293 293 export type ErrorLike = Error | ErrorObject; 294 294 295 + export interface ScrobbleResult { 296 + match?: PlayMatchResult 297 + payload?: ScrobblePayload 298 + warnings?: string[] 299 + error?: Error | ErrorObject 300 + response?: ScrobbleResponse 301 + mergedScrobble?: PlayObjectLifecycleless 302 + } 303 + 295 304 export interface PlayLifecycle { 296 305 input?: object 297 306 original: PlayObjectLifecycleless 298 307 steps: LifecycleStep[] 299 - scrobble?: { 300 - match?: PlayMatchResult 301 - payload?: ScrobblePayload 302 - warnings?: string[] 303 - error?: Error | ErrorObject 304 - response?: ScrobbleResponse 305 - mergedScrobble?: PlayObjectLifecycleless 306 - } 308 + scrobble?: ScrobbleResult 307 309 } 308 310 309 311 export interface LifecycleStep {
+4
src/stories/ActivityTimeline.stories.tsx
··· 928 928 "mbid": "379db622-cc58-4bbf-9a3e-a7fa50b25fd1", 929 929 "duration": 320 930 930 }, 931 + "warnings": [ 932 + "test warning", 933 + "missing data in response" 934 + ], 931 935 "response": { 932 936 "scrobbles": { 933 937 "scrobble": {