diff --git a/src/client/components/chakraPlayer/Player.tsx b/src/client/components/chakraPlayer/Player.tsx index 5d547104..9e6c95cc 100644 --- a/src/client/components/chakraPlayer/Player.tsx +++ b/src/client/components/chakraPlayer/Player.tsx @@ -17,11 +17,13 @@ import { InfoTip } from "../ToggleTip"; import { tanQueries } from "../../queries"; import dayjs from "dayjs"; import { MSErrorBoundary } from "../ErrorBoundary"; +import { PlaybackReportingPlayer } from "../icons/PlaybackReporting"; export interface PlayerProps { data: SourcePlayerJson & {expiration?: string} nowPlaying?: boolean sot?: SOURCE_SOT_TYPES + playbackReporting?: boolean } const bufferExplanation = (<> @@ -38,7 +40,8 @@ export const ChakraPlayer = (props: PlayerProps) => { const { data, nowPlaying = false, - sot = SOURCE_SOT.PLAYER + sot = SOURCE_SOT.PLAYER, + playbackReporting } = props; const { @@ -147,6 +150,11 @@ export const ChakraPlayer = (props: PlayerProps) => { const durationTimestamp = duration === undefined ? '-' : timeToHumanTimestamp(duration * 1000); const bufferTip = positionBuffer !== undefined ? : null; + + let playbackReportingTip: React.JSX.Element; + if(playbackReporting !== undefined) { + playbackReportingTip = + } return ( @@ -183,7 +191,7 @@ export const ChakraPlayer = (props: PlayerProps) => { */} - {['unknown', 'playing'].includes(calculated) && isNowPlaying ? 'Now Playing' : capitalize(calculated)}{!isNowPlaying ? bufferTip : null} + {['unknown', 'playing'].includes(calculated) && isNowPlaying ? 'Now Playing' : capitalize(calculated)}{!isNowPlaying ? bufferTip : null}{playbackReportingTip} Listened: {isNowPlaying !== true && calculated !== 'stopped' && listenedDuration !== null ? `${listenedDuration.toFixed(0)}s` : '-'}{durPer} diff --git a/src/client/components/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx index c7716b45..63295fe1 100644 --- a/src/client/components/icons/ChakraIcons.tsx +++ b/src/client/components/icons/ChakraIcons.tsx @@ -24,7 +24,8 @@ import { LuCalendar, LuRefreshCw, LuCopy, - LuClock + LuClock, + LuSparkles } from "react-icons/lu" import { VscDebugRestart } from 'react-icons/vsc'; import { MdOutlineFiberNew } from "react-icons/md"; @@ -193,4 +194,6 @@ export const ExclamationCircleIcon = makeChakraIcon(LuCircleAlert); export const ExclamationTriangleIcon = makeChakraIcon(LuTriangleAlert); -export const TimelineIndicatorIconQueued = makeChakraIcon(LuClock); \ No newline at end of file +export const TimelineIndicatorIconQueued = makeChakraIcon(LuClock); + +export const SparkleIcon = makeChakraIcon(LuSparkles); \ No newline at end of file diff --git a/src/client/components/icons/PlaybackReporting.tsx b/src/client/components/icons/PlaybackReporting.tsx new file mode 100644 index 00000000..f92077e9 --- /dev/null +++ b/src/client/components/icons/PlaybackReporting.tsx @@ -0,0 +1,62 @@ +import { IconButton, Text, Link } from "@chakra-ui/react" +import { ExclamationCircleIcon, SparkleIcon } from "./ChakraIcons" +import type React from "react" +import { ToggleTip } from "../ToggleTip" +import { LuSparkles } from "react-icons/lu" + +const playbackReportLink = Enhanced Playback Reporting + +export interface PlayBackReportingServerProps { + playbackReporting: boolean +} +export const PlaybackReportingServer = (props: PlayBackReportingServerProps) => ( + + This Subsonic server {props.playbackReporting ? 'supports' : 'does not support'} {playbackReportLink}. + }> + {props.playbackReporting ? : } + ); + +export interface PlayBackReportingPlayerProps { + playbackReporting: boolean + hasFields: boolean +} +export const PlaybackReportingPlayer = (props: PlayBackReportingPlayerProps) => { + if (!props.playbackReporting) { + return null; + } + let content: React.JSX.Element; + if (props.playbackReporting && props.hasFields) { + content = ( + + This Subsonic client is sending {playbackReportLink} fields. + + ); + } else { + content = ( + + This Subsonic client is not sending {playbackReportLink} fields. + + ); + } + + return ( + + {props.playbackReporting && props.hasFields ? : } + ) +} \ No newline at end of file diff --git a/src/client/components/msComponent/MSComponentDetailed.tsx b/src/client/components/msComponent/MSComponentDetailed.tsx index 798c51dd..c355e53b 100644 --- a/src/client/components/msComponent/MSComponentDetailed.tsx +++ b/src/client/components/msComponent/MSComponentDetailed.tsx @@ -23,6 +23,7 @@ import { tanQueries } from "../../queries/index.js"; import { MSErrorBoundary } from "../ErrorBoundary.js"; import type {IconType} from "react-icons/lib"; import { useIsWrapped } from "../../utils/hooks/useIsWrapped.js"; +import { PlaybackReportingServer } from "../icons/PlaybackReporting.js"; export const ComponentBackButton = (props: ComponentProps = {}) => { return ( @@ -32,11 +33,15 @@ export const ComponentBackButton = (props: ComponentProps}) => { +export const MSComponentName = (props: {data?: Pick & {playbackReporting?: boolean}}) => { if(props.data === undefined) { return ; } - return {props.data.name}; + let subsonicPlaybackReporting: React.JSX.Element; + if('playbackReporting' in props.data) { + subsonicPlaybackReporting = + } + return {props.data.name}{subsonicPlaybackReporting}; } export const MSComponentType = (props: {data?: Pick}) => { diff --git a/src/core/Api.ts b/src/core/Api.ts index c15976ef..a79fff09 100644 --- a/src/core/Api.ts +++ b/src/core/Api.ts @@ -117,6 +117,8 @@ export type ComponentSourceApiBase = { export type ComponentSourceApi = ComponentCommonApi & ComponentSourceApiBase; export type ComponentSourceApiJson = Replace, string>; +export type SubsonicSourceApiJson = ComponentSourceApiJson & { playbackReporting?: boolean } + export type ComponentsApiJson = ComponentSourceApiJson | ComponentClientApiJson; export const isComponentSourceApiJson = (data: ComponentCommonApiJson): data is ComponentSourceApiJson => { diff --git a/src/core/tests/utils/apiFixtures.ts b/src/core/tests/utils/apiFixtures.ts index 73933401..a38aa9f9 100644 --- a/src/core/tests/utils/apiFixtures.ts +++ b/src/core/tests/utils/apiFixtures.ts @@ -230,11 +230,11 @@ export const generateComponentApiJson = (data: Partial = {}) return generateClientApiJson({mode, type, ...data}); } -export const generateSourcePlayerJson = (data: Partial = {}, opts: {art?: boolean} = {}): SourcePlayerJson => { +export const generateSourcePlayerJson = (data: Partial = {}, opts: {art?: boolean, position?: boolean, status?: boolean} = {}): SourcePlayerJson => { const { platformId = `${faker.word.noun()}-${faker.word.adjective()}`, play = asJsonPlayObject(generatePlay()), - position = play.meta.trackProgressPosition ?? faker.number.int({min: 0, max: play.data.duration}), + position = opts.position === false ? undefined : (play.meta.trackProgressPosition ?? faker.number.int({min: 0, max: play.data.duration})), listenedDuration = play.data.listenedFor ?? faker.number.int({min: 0, max: play.data.duration}), playFirstSeenAt = dayjs().subtract(30, 's').toISOString(), playLastUpdatedAt = dayjs().subtract(10, 's').toISOString(), @@ -268,7 +268,7 @@ export const generateSourcePlayerJson = (data: Partial = {}, o playLastUpdatedAt, createdAt, status: { - reported, + reported: opts.status === false ? REPORTED_PLAYER_STATUSES.unknown : reported, calculated, stale, orphaned diff --git a/src/stories/component/Player.stories.tsx b/src/stories/component/Player.stories.tsx index d1343789..d337a438 100644 --- a/src/stories/component/Player.stories.tsx +++ b/src/stories/component/Player.stories.tsx @@ -122,4 +122,28 @@ export const PlayerLive = meta.story({ (Story) => ( options={sseProviderOptions}>), ], -}); \ No newline at end of file +}); + +export const SubsonicPlayer = meta.story({ + argTypes: { + playbackReporting: { + control: { type: 'select' }, + options: [undefined, false, true], + } + }, + args: { + data: generateSourcePlayerJson(undefined, {art: true}) + } +}); + +export const SubsonicPlayerNotEnhanced = meta.story({ + argTypes: { + playbackReporting: { + control: { type: 'select' }, + options: [undefined, false, true], + } + }, + args: { + data: generateSourcePlayerJson(undefined, {art: true, position: false, status: false}) + } +}); diff --git a/src/stories/pages/ComponentDetailed.stories.tsx b/src/stories/pages/ComponentDetailed.stories.tsx index 79857797..260a82d2 100644 --- a/src/stories/pages/ComponentDetailed.stories.tsx +++ b/src/stories/pages/ComponentDetailed.stories.tsx @@ -138,4 +138,16 @@ export const SourceDetailedSleeping = meta.story({ args: { data: generateSourceApiJson({sleeping: true, wakeAt: dayjs().add(45, 's').toISOString()}) } +}); + +export const SubsonicEnhanced = meta.story({ + args: { + data: {...generateClientApiJson(), playbackReporting: true} + } +}); + +export const SubsonicNotEnhanced = meta.story({ + args: { + data: {...generateClientApiJson(), playbackReporting: false} + } }); \ No newline at end of file