mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
feat(media-server): add stats for nerds (#4170)
This commit is contained in:
@@ -117,6 +117,7 @@ export class EmbyIntegration extends Integration implements IMediaServerIntegrat
|
||||
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
|
||||
albumName: sessionInfo.NowPlayingItem.Album ?? "",
|
||||
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
|
||||
metadata: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,30 @@ export interface StreamSession {
|
||||
episodeName?: string | null;
|
||||
albumName?: string | null;
|
||||
episodeCount?: number | null;
|
||||
metadata: {
|
||||
video: {
|
||||
resolution: {
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
frameRate: number | null;
|
||||
};
|
||||
audio: {
|
||||
channelCount: number | null;
|
||||
codec: string | null;
|
||||
};
|
||||
transcoding: {
|
||||
container: string | null;
|
||||
resolution: {
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
target: {
|
||||
audioCodec: string | null;
|
||||
videoCodec: string | null;
|
||||
};
|
||||
};
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,36 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte
|
||||
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
|
||||
albumName: sessionInfo.NowPlayingItem.Album ?? "",
|
||||
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
|
||||
metadata: {
|
||||
video: {
|
||||
resolution:
|
||||
sessionInfo.NowPlayingItem.Width && sessionInfo.NowPlayingItem.Height
|
||||
? {
|
||||
width: sessionInfo.NowPlayingItem.Width,
|
||||
height: sessionInfo.NowPlayingItem.Height,
|
||||
}
|
||||
: null,
|
||||
frameRate: sessionInfo.TranscodingInfo?.Framerate ?? null,
|
||||
},
|
||||
audio: {
|
||||
channelCount: sessionInfo.TranscodingInfo?.AudioChannels ?? null,
|
||||
codec: sessionInfo.TranscodingInfo?.AudioCodec ?? null,
|
||||
},
|
||||
transcoding: {
|
||||
resolution:
|
||||
sessionInfo.TranscodingInfo?.Width && sessionInfo.TranscodingInfo.Height
|
||||
? {
|
||||
width: sessionInfo.TranscodingInfo.Width,
|
||||
height: sessionInfo.TranscodingInfo.Height,
|
||||
}
|
||||
: null,
|
||||
target: {
|
||||
audioCodec: sessionInfo.TranscodingInfo?.AudioCodec ?? null,
|
||||
videoCodec: sessionInfo.TranscodingInfo?.VideoCodec ?? null,
|
||||
},
|
||||
container: sessionInfo.TranscodingInfo?.Container ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export class MediaServerMockService implements IMediaServerIntegration {
|
||||
episodeName: null,
|
||||
albumName: null,
|
||||
episodeCount: null,
|
||||
metadata: null,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
@@ -61,6 +61,7 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat
|
||||
episodeName: mediaElement.$.title ?? null,
|
||||
albumName: mediaElement.$.type === "track" ? (mediaElement.$.parentTitle ?? null) : null,
|
||||
episodeCount: mediaElement.$.index ?? null,
|
||||
metadata: null,
|
||||
},
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1980,7 +1980,25 @@
|
||||
"currentlyPlaying": "Currently playing",
|
||||
"user": "User",
|
||||
"name": "Name",
|
||||
"id": "Id"
|
||||
"id": "Id",
|
||||
"metadata": {
|
||||
"title": "Stats for nerds",
|
||||
"video": {
|
||||
"title": "Video",
|
||||
"resolution": "Resolution"
|
||||
},
|
||||
"audio": {
|
||||
"title": "Audio",
|
||||
"channelCount": "Audio channels",
|
||||
"codec": "Audio codec"
|
||||
},
|
||||
"transcoding": {
|
||||
"title": "Transcoding",
|
||||
"container": "Container",
|
||||
"resolution": "Resolution",
|
||||
"target": "Target codec"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloads": {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Avatar, Flex, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { Avatar, Divider, Flex, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconDeviceTv, IconHeadphones, IconMovie, IconVideo } from "@tabler/icons-react";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { getIconUrl, integrationDefs } from "@homarr/definitions";
|
||||
import type { StreamSession } from "@homarr/integrations";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
@@ -123,7 +124,7 @@ export default function MediaServerWidget({
|
||||
[currentStreams],
|
||||
);
|
||||
|
||||
const { openModal } = useModalAction(itemInfoModal);
|
||||
const { openModal } = useModalAction(ItemInfoModal);
|
||||
const table = useTranslatedMantineReactTable({
|
||||
columns,
|
||||
data: flatSessions,
|
||||
@@ -219,10 +220,16 @@ export default function MediaServerWidget({
|
||||
);
|
||||
}
|
||||
|
||||
const itemInfoModal = createModal<{ item: StreamSession }>(({ innerProps }) => {
|
||||
const ItemInfoModal = createModal<{ item: StreamSession }>(({ innerProps }) => {
|
||||
const t = useScopedI18n("widget.mediaServer.items");
|
||||
const Icon = innerProps.item.currentlyPlaying ? mediaTypeIconMap[innerProps.item.currentlyPlaying.type] : null;
|
||||
|
||||
const metadata = useMemo(() => {
|
||||
return innerProps.item.currentlyPlaying?.metadata
|
||||
? constructMetadata(innerProps.item.currentlyPlaying.metadata)
|
||||
: null;
|
||||
}, [innerProps.item.currentlyPlaying?.metadata]);
|
||||
|
||||
return (
|
||||
<Stack align="center">
|
||||
<Flex direction="column" gap="xs" align="center">
|
||||
@@ -255,6 +262,32 @@ const itemInfoModal = createModal<{ item: StreamSession }>(({ innerProps }) => {
|
||||
/>
|
||||
<NormalizedLine itemKey={t("name")} value={<Text>{innerProps.item.sessionName}</Text>} />
|
||||
<NormalizedLine itemKey={t("id")} value={<Text>{innerProps.item.sessionId}</Text>} />
|
||||
|
||||
{metadata ? (
|
||||
<Stack w="100%" gap={0}>
|
||||
<Divider label={t("metadata.title")} labelPosition="center" mt="lg" mb="sm" />
|
||||
|
||||
<Group align="flex-start">
|
||||
{objectEntries(metadata).map(([key, value], index) => (
|
||||
<Fragment key={key}>
|
||||
{index !== 0 && <Divider key={index} orientation="vertical" />}
|
||||
<Stack gap={4}>
|
||||
<Text fw="bold">{t(`metadata.${key}.title`)}</Text>
|
||||
|
||||
{Object.entries(value)
|
||||
.filter(([_, value]) => Boolean(value))
|
||||
.map(([innerKey, value]) => (
|
||||
<Group justify="space-between" w="100%" key={innerKey} wrap="nowrap">
|
||||
<Text>{t(`metadata.${key}.${innerKey}` as never)}</Text>
|
||||
<Text>{value}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}).withOptions({
|
||||
@@ -280,3 +313,23 @@ const mediaTypeIconMap = {
|
||||
video: IconVideo,
|
||||
audio: IconHeadphones,
|
||||
} satisfies Record<Exclude<StreamSession["currentlyPlaying"], null>["type"], TablerIcon>;
|
||||
|
||||
const constructMetadata = (metadata: Exclude<Exclude<StreamSession["currentlyPlaying"], null>["metadata"], null>) => ({
|
||||
video: {
|
||||
resolution: metadata.video.resolution
|
||||
? `${metadata.video.resolution.width}x${metadata.video.resolution.height}`
|
||||
: null,
|
||||
frameRate: metadata.video.frameRate,
|
||||
},
|
||||
audio: {
|
||||
channelCount: metadata.audio.channelCount,
|
||||
codec: metadata.audio.codec,
|
||||
},
|
||||
transcoding: {
|
||||
container: metadata.transcoding.container,
|
||||
resolution: metadata.transcoding.resolution
|
||||
? `${metadata.transcoding.resolution.width}x${metadata.transcoding.resolution.height}`
|
||||
: null,
|
||||
target: `${metadata.transcoding.target.videoCodec} ${metadata.transcoding.target.audioCodec}`.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user