[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): Indicate failed scrobble is dead queued

FoxxMD (Jul 7, 2026, 2:32 AM UTC) b3c1ce6d 5daba47a

+24 -15
+13 -6
src/client/components/ActivityDetail.tsx
··· 1 1 import React, { ComponentProps, useState, Fragment, useEffect } from "react" 2 2 import { Accordion, For, Span, Stack, Text, Box, AbsoluteCenter, Button, Clipboard, Separator, HStack, Flex, Badge, IconButton, Container, Icon, useAccordionItemContext, Skeleton, SkeletonText, Collapsible, BadgeProps } from '@chakra-ui/react'; 3 - import { ComponentType, Second } from "../../core/Atomic"; 3 + import { CLIENT_DEAD_QUEUE, ComponentType, QUEUE_NAMES, Second } from "../../core/Atomic"; 4 4 import { PlayData } from "./PlayData"; 5 5 import { ErrorAlert } from "./ErrorAlert"; 6 6 import { AiOutlineExclamationCircle } from "react-icons/ai"; ··· 93 93 } 94 94 95 95 export interface ActivitySummaryProps extends SortPlaysByProps { 96 - activity: PlayApiCommon & {isNew?: boolean | Second} 96 + activity: PlayApiCommonDetailed & {isNew?: boolean | Second} 97 97 componentType: ComponentType 98 98 } 99 99 ··· 109 109 } = props; 110 110 const [updated, setUpdated] = useState<{lastUpdated: string, updated: boolean}>({lastUpdated: updatedAt, updated: false}); 111 111 useEffect(() => { 112 + // eslint-disable-next-line react-hooks/set-state-in-effect 112 113 setUpdated((old) => { 113 114 if(old.lastUpdated === activity.updatedAt) { 114 115 return {lastUpdated: activity.updatedAt, updated: false}; ··· 237 238 return <ErrorAlert error={error}/> 238 239 } 239 240 240 - return <ActivityDetails componentType={props.componentType} key={props.uid} activity={activity as PlayApiCommonDetailed}/> 241 + return <ActivityDetails componentType={props.componentType} key={props.uid} activity={activity}/> 241 242 } 242 243 243 - export const ActivityStateActions = (props: {activity: PlayApiCommon}) => { 244 + export const ActivityStateActions = (props: {activity: PlayApiCommonDetailed}) => { 244 245 let suffix: React.JSX.Element | null; 245 - let badgeProps: BadgeProps = {}; 246 + const badgeProps: BadgeProps = {}; 247 + const { 248 + activity: { 249 + queueStates = [] 250 + } = {} 251 + } = props; 246 252 if(props.activity.state === 'failed') { 247 253 suffix = <RetryButton size="xs" margin="1px" variant="subtle"/>; 248 254 badgeProps.paddingRight = 0; 249 255 } 256 + const hasDeadQueue = queueStates.some(x => x.queueName === CLIENT_DEAD_QUEUE && x.queueStatus === 'queued'); 250 257 return ( 251 258 <Stack> 252 259 <HStack> 253 - <PlayStateBadge {...badgeProps} minH="32px" alignItems="anchor-center" size="lg" state={props.activity.state} suffix={suffix} /> 260 + <PlayStateBadge {...badgeProps} minH="32px" alignItems="anchor-center" size="lg" hasDeadQueue={hasDeadQueue} state={props.activity.state} suffix={suffix} /> 254 261 </HStack> 255 262 <HStack justifyContent="flex-end"> 256 263 <DebugCopy variant="ghost" value={JSON.stringify(props.activity)}/>
+8 -6
src/client/components/Badges.tsx
··· 1 1 import { Badge, Separator, HStack } from "@chakra-ui/react"; 2 2 import React, { ComponentProps, useState, useCallback, useEffect } from "react"; 3 - import { COMPONENT_STATE, ComponentCommonApiJson, componentStateToFriendly, MsSseEvent, MsSseEventPayload, PlayApiCommon } from "../../core/Api"; 3 + import { COMPONENT_STATE, ComponentCommonApiJson, componentStateToFriendly, MsSseEvent, PlayApiCommon } from "../../core/Api"; 4 4 import { capitalize } from "../../core/StringUtils"; 5 - import { PlayerState } from "../../backend/common/infrastructure/config/source/mpd"; 6 - import {useSSE, useSSEContext, useSSEEvent} from "@flamefrontend/sse-runtime-react"; 5 + import {useSSEContext, useSSEEvent} from "@flamefrontend/sse-runtime-react"; 7 6 import { Second } from "../../core/Atomic"; 8 7 import { useTimeout } from 'react-use-timeout'; 9 8 10 - export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: PlayApiCommon['state'], suffix?: React.JSX.Element }) => { 9 + export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: PlayApiCommon['state'], suffix?: React.JSX.Element, hasDeadQueue?: boolean }) => { 11 10 12 11 const { state, suffix, ...rest } = props; 13 12 14 - let badgeColor = undefined, 15 - badgeText = capitalize(state); 13 + let badgeColor = undefined; 14 + let badgeText = capitalize(state); 16 15 17 16 switch (state) { 18 17 case 'queued': ··· 24 23 break; 25 24 case 'failed': 26 25 badgeColor = 'red'; 26 + if(props.hasDeadQueue) { 27 + badgeText = 'Dead Queued'; 28 + } 27 29 break; 28 30 case 'discarded': 29 31 badgeColor = 'grey';
+3 -3
src/client/components/playActivity/ListParts.tsx
··· 17 17 } 18 18 19 19 export interface GroupData { 20 - plays: PlayApiCommon[] 20 + plays: PlayApiCommonDetailed[] 21 21 date: Dayjs 22 22 } 23 23 24 24 export interface ActivityLogProps extends SortPlaysByProps { 25 - data: PlayApiCommon[] 25 + data: PlayApiCommonDetailed[] 26 26 componentId: number 27 27 componentType: ComponentType 28 28 render?: 'virtNormal' | 'virtDynamic' | 'virtExp' | 'accordian' ··· 106 106 return groupsReduced.groups; 107 107 } 108 108 109 - export const generateFlatItems = (data: PlayApiCommon[]) => { 109 + export const generateFlatItems = (data: PlayApiCommonDetailed[]) => { 110 110 // ensure there are no duplicates 111 111 // this may happen if a play is "bumped" from one "page" to another, based on offset, 112 112 // when new plays are inserted out of order (playedAt)