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

Add some error boundaries around components

FoxxMD (Jul 6, 2026, 6:34 PM UTC) e874ccc2 17e97b3a

+52 -71
+6 -8
src/client/components/Badges.tsx
··· 86 86 setComponentState(data.state); 87 87 },[data.state, setComponentState]); 88 88 89 - if(props.componentId !== undefined && props.live) { 90 - const client = useSSEContext<MsSseEvent>(); 91 - const connection = useSSEEvent(client, 'componentUpdate', (payload) => { 92 - if(payload.componentId === props.componentId && payload.data.state !== undefined) { 93 - setComponentState(payload.data.state); 94 - } 95 - }); 96 - } 89 + const client = useSSEContext<MsSseEvent>(); 90 + const connection = useSSEEvent(client, 'componentUpdate', (payload) => { 91 + if(props.componentId !== undefined && props.live && payload.componentId === props.componentId && payload.data.state !== undefined) { 92 + setComponentState(payload.data.state); 93 + } 94 + }); 97 95 98 96 let badgeColor = undefined; 99 97
+44 -39
src/client/components/chakraPlayer/Player.tsx
··· 18 18 import { InfoTip, ToggleTip } from "../ToggleTip"; 19 19 import { tanQueries } from "../../queries"; 20 20 import dayjs from "dayjs"; 21 + import { MSErrorBoundary } from "../ErrorBoundary"; 21 22 22 23 export interface PlayerProps { 23 24 data: SourcePlayerJson & {expiration?: string} ··· 149 150 150 151 const bufferTip = positionBuffer !== undefined ? <InfoTip positioning={{placement: "bottom-start"}} buttonProps={{height: 'var(--chakra-sizes-4)'}} content={bufferExplanation}/> : null; 151 152 152 - return <Container className="playerContainer" bg="bg.emphasized" borderWidth="1px" p="2" py="3" rounded="md"> 153 - <Stack gap="2"> 154 - <Flex gap="4" align="center"> 155 - {playArt !== undefined ? <Image minWidth="48px" flex="0" height="100%" width="100%" src={playArt}></Image> : null} 156 - <Center flex="1"> 157 - <Stack textAlign="center"> 158 - <Heading textWrap="balance" size="md">{calculated !== 'stopped' ? track : '-'}</Heading> 159 - <TextMuted>{calculated !== 'stopped' ? artists.map(x => x.name).join(' / ') : '-'}</TextMuted> 160 - </Stack> 161 - </Center> 162 - </Flex> 163 - <HStack gap="5"> 164 - <Text textStyle="sm">{positionTimestamp}</Text> 165 - <Box flex="1"> 166 - <LinearProgress 167 - variant={indeterminate ? undefined : "buffer"} 168 - 169 - value={positionProgress} 170 - valueBuffer={Math.max(bufferProgress ?? positionProgress, positionProgress)} 171 - /> 172 - </Box> 173 - <Text textStyle="xs">{durationTimestamp}</Text> 174 - </HStack> 175 - {/* <Progress.Root value={indeterminate || data.position === undefined || duration === undefined ? null : (data.position/duration) * 100} size="sm"> 176 - <HStack gap="5"> 177 - <Progress.Label>{positionTimestamp}</Progress.Label> 178 - <Progress.Track flex="1"> 179 - <Progress.Range /> 180 - </Progress.Track> 181 - <Progress.ValueText>{durationTimestamp}</Progress.ValueText> 182 - </HStack> 183 - </Progress.Root> */} 184 - <Flex alignItems="center"> 185 - <TextMuted>{['unknown', 'playing'].includes(calculated) && isNowPlaying ? 'Now Playing' : capitalize(calculated)}{!isNowPlaying ? bufferTip : null}</TextMuted> 186 - <Spacer /> 187 - <TextMuted>Listened: {isNowPlaying !== true && calculated !== 'stopped' && listenedDuration !== null ? `${listenedDuration.toFixed(0)}s` : '-'}{durPer}</TextMuted> 188 - </Flex> 189 - </Stack> 190 - </Container> 153 + return ( 154 + <MSErrorBoundary> 155 + <Container className="playerContainer" bg="bg.emphasized" borderWidth="1px" p="2" py="3" rounded="md"> 156 + <Stack gap="2"> 157 + <Flex gap="4" align="center"> 158 + {playArt !== undefined ? <Image minWidth="48px" flex="0" height="100%" width="100%" src={playArt}></Image> : null} 159 + <Center flex="1"> 160 + <Stack textAlign="center"> 161 + <Heading textWrap="balance" size="md">{calculated !== 'stopped' ? track : '-'}</Heading> 162 + <TextMuted>{calculated !== 'stopped' ? artists.map(x => x.name).join(' / ') : '-'}</TextMuted> 163 + </Stack> 164 + </Center> 165 + </Flex> 166 + <HStack gap="5"> 167 + <Text textStyle="sm">{positionTimestamp}</Text> 168 + <Box flex="1"> 169 + <LinearProgress 170 + variant={indeterminate ? undefined : "buffer"} 171 + 172 + value={positionProgress} 173 + valueBuffer={Math.max(bufferProgress ?? positionProgress, positionProgress)} 174 + /> 175 + </Box> 176 + <Text textStyle="xs">{durationTimestamp}</Text> 177 + </HStack> 178 + {/* <Progress.Root value={indeterminate || data.position === undefined || duration === undefined ? null : (data.position/duration) * 100} size="sm"> 179 + <HStack gap="5"> 180 + <Progress.Label>{positionTimestamp}</Progress.Label> 181 + <Progress.Track flex="1"> 182 + <Progress.Range /> 183 + </Progress.Track> 184 + <Progress.ValueText>{durationTimestamp}</Progress.ValueText> 185 + </HStack> 186 + </Progress.Root> */} 187 + <Flex alignItems="center"> 188 + <TextMuted>{['unknown', 'playing'].includes(calculated) && isNowPlaying ? 'Now Playing' : capitalize(calculated)}{!isNowPlaying ? bufferTip : null}</TextMuted> 189 + <Spacer /> 190 + <TextMuted>Listened: {isNowPlaying !== true && calculated !== 'stopped' && listenedDuration !== null ? `${listenedDuration.toFixed(0)}s` : '-'}{durPer}</TextMuted> 191 + </Flex> 192 + </Stack> 193 + </Container> 194 + </MSErrorBoundary> 195 + ) 191 196 192 197 } 193 198
+2 -24
src/client/components/msComponent/MSComponentDetailed.tsx
··· 73 73 74 74 const stateIsStarted = (state: ComponentState): boolean => state <= COMPONENT_STATE.MUTED; 75 75 76 - const ComponentSettings = (props: { data: ComponentCommonApiJson }) => { 77 - const [startChecked, setStartChecked] = useState<boolean>(stateIsStarted(props.data.state)); 78 - return ( 79 - <Stack> 80 - <Switch.Root 81 - checked={startChecked} 82 - onCheckedChange={(e) => setStartChecked(e.checked)} 83 - > 84 - <Switch.HiddenInput /> 85 - <Switch.Control> 86 - <Switch.Thumb> 87 - <Switch.ThumbIndicator fallback={<XIcon color="black" />}> 88 - <CheckIcon /> 89 - </Switch.ThumbIndicator> 90 - </Switch.Thumb> 91 - </Switch.Control> 92 - <Switch.Label>Start</Switch.Label> 93 - </Switch.Root> 94 - </Stack> 95 - ) 96 - } 97 - 98 76 const componentStateMenuItem = (Icon: IconType, value: string, name?: string) => (props: Pick<MenuItemProps, 'disabled'> = {}) => { 99 77 return (<Menu.Item key={value} value={value} {...props}><Icon/><Box flex="1">{name ?? capitalize(value)}</Box></Menu.Item>); 100 78 } ··· 208 186 <Flex justifyContent="flex-end" rowGap="6" flexDirection="row-reverse" wrap="wrap"> 209 187 <Box marginEnd="auto"><MSComponentStats {...props}/></Box> 210 188 </Flex> 211 - {props.live ? <PlayersContainerFetchable nowPlaying={isSource ? undefined : true} data={props.data}/> : <PlayersContainer nowPlaying={isSource ? undefined : true} data={props.data} live={props.live}/>} 189 + <MSErrorBoundary>{props.live ? <PlayersContainerFetchable nowPlaying={isSource ? undefined : true} data={props.data}/> : <PlayersContainer nowPlaying={isSource ? undefined : true} data={props.data} live={props.live}/>}</MSErrorBoundary> 212 190 <Heading size="3xl" width="100%">{isComponentTypeSource(props.data.mode) ? 'Plays' : 'Scrobbles'}</Heading> 213 - <ListContainerFilterable render="virtDynamic" componentType={props.data.mode} componentId={props.data.id}/> 191 + <MSErrorBoundary><ListContainerFilterable render="virtDynamic" componentType={props.data.mode} componentId={props.data.id}/></MSErrorBoundary> 214 192 </Flex> 215 193 </MSErrorBoundary> 216 194 )