mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 09:25:47 +01:00
🐛 Media Session widget jellyfin sessions + translations (#1353)
This commit is contained in:
@@ -31,28 +31,5 @@
|
|||||||
"request": "request...",
|
"request": "request...",
|
||||||
"approved": "Request was approved!",
|
"approved": "Request was approved!",
|
||||||
"declined": "Request was declined!"
|
"declined": "Request was declined!"
|
||||||
},
|
|
||||||
"detail": {
|
|
||||||
"label": "Stats for nerds",
|
|
||||||
"id": "ID",
|
|
||||||
"device": "Device",
|
|
||||||
"video": {
|
|
||||||
"video":"Video",
|
|
||||||
"resolution": "Resolution",
|
|
||||||
"framerate": "Framerate",
|
|
||||||
"codec": "Video Codec"
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"audio": "Audio",
|
|
||||||
"channels": "Audio Channels",
|
|
||||||
"codec": "Audio Codec"
|
|
||||||
},
|
|
||||||
"transcoding": {
|
|
||||||
"transcoding": "Transcoding",
|
|
||||||
"context": "Context",
|
|
||||||
"requested": "Hardware Encoding Requested",
|
|
||||||
"source": "Source Codec",
|
|
||||||
"target": "Target Codec"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,72 +100,74 @@ const handleServer = async (app: ConfigAppType): Promise<GenericMediaServer | un
|
|||||||
const infoApi = await getSystemApi(api).getPublicSystemInfo();
|
const infoApi = await getSystemApi(api).getPublicSystemInfo();
|
||||||
await api.authenticateUserByName(username, password);
|
await api.authenticateUserByName(username, password);
|
||||||
const sessionApi = await getSessionApi(api);
|
const sessionApi = await getSessionApi(api);
|
||||||
const sessions = await sessionApi.getSessions();
|
const { data: sessions } = await sessionApi.getSessions();
|
||||||
return {
|
return {
|
||||||
type: 'jellyfin',
|
type: 'jellyfin',
|
||||||
appId: app.id,
|
appId: app.id,
|
||||||
serverAddress: app.url,
|
serverAddress: app.url,
|
||||||
version: infoApi.data.Version ?? undefined,
|
version: infoApi.data.Version ?? undefined,
|
||||||
sessions: sessions.data.map(
|
sessions: sessions
|
||||||
(session): GenericSessionInfo => ({
|
.filter((session) => session.NowPlayingItem)
|
||||||
id: session.Id ?? '?',
|
.map(
|
||||||
username: session.UserName ?? undefined,
|
(session): GenericSessionInfo => ({
|
||||||
sessionName: `${session.Client} (${session.DeviceName})`,
|
id: session.Id ?? '?',
|
||||||
supportsMediaControl: session.SupportsMediaControl ?? false,
|
username: session.UserName ?? undefined,
|
||||||
currentlyPlaying: session.NowPlayingItem
|
sessionName: `${session.Client} (${session.DeviceName})`,
|
||||||
? {
|
supportsMediaControl: session.SupportsMediaControl ?? false,
|
||||||
name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
|
currentlyPlaying: session.NowPlayingItem
|
||||||
seasonName: session.NowPlayingItem.SeasonName as string,
|
? {
|
||||||
episodeName: session.NowPlayingItem.Name as string,
|
name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
|
||||||
albumName: session.NowPlayingItem.Album as string,
|
seasonName: session.NowPlayingItem.SeasonName as string,
|
||||||
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
|
episodeName: session.NowPlayingItem.Name as string,
|
||||||
metadata: {
|
albumName: session.NowPlayingItem.Album as string,
|
||||||
video:
|
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
|
||||||
session.NowPlayingItem &&
|
metadata: {
|
||||||
session.NowPlayingItem.Width &&
|
video:
|
||||||
session.NowPlayingItem.Height
|
session.NowPlayingItem &&
|
||||||
|
session.NowPlayingItem.Width &&
|
||||||
|
session.NowPlayingItem.Height
|
||||||
|
? {
|
||||||
|
videoCodec: undefined,
|
||||||
|
width: session.NowPlayingItem.Width ?? undefined,
|
||||||
|
height: session.NowPlayingItem.Height ?? undefined,
|
||||||
|
bitrate: undefined,
|
||||||
|
videoFrameRate: session.TranscodingInfo?.Framerate
|
||||||
|
? String(session.TranscodingInfo?.Framerate)
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
audio: session.TranscodingInfo
|
||||||
? {
|
? {
|
||||||
videoCodec: undefined,
|
audioChannels: session.TranscodingInfo.AudioChannels ?? undefined,
|
||||||
width: session.NowPlayingItem.Width ?? undefined,
|
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
|
||||||
height: session.NowPlayingItem.Height ?? undefined,
|
|
||||||
bitrate: undefined,
|
|
||||||
videoFrameRate: session.TranscodingInfo?.Framerate
|
|
||||||
? String(session.TranscodingInfo?.Framerate)
|
|
||||||
: undefined,
|
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
audio: session.TranscodingInfo
|
transcoding: session.TranscodingInfo
|
||||||
? {
|
? {
|
||||||
audioChannels: session.TranscodingInfo.AudioChannels ?? undefined,
|
audioChannels: session.TranscodingInfo.AudioChannels ?? -1,
|
||||||
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
|
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
|
||||||
}
|
container: session.TranscodingInfo.Container ?? undefined,
|
||||||
: undefined,
|
width: session.TranscodingInfo.Width ?? undefined,
|
||||||
transcoding: session.TranscodingInfo
|
height: session.TranscodingInfo.Height ?? undefined,
|
||||||
? {
|
videoCodec: session.TranscodingInfo?.VideoCodec ?? undefined,
|
||||||
audioChannels: session.TranscodingInfo.AudioChannels ?? -1,
|
audioDecision: undefined,
|
||||||
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
|
context: undefined,
|
||||||
container: session.TranscodingInfo.Container ?? undefined,
|
duration: undefined,
|
||||||
width: session.TranscodingInfo.Width ?? undefined,
|
error: undefined,
|
||||||
height: session.TranscodingInfo.Height ?? undefined,
|
sourceAudioCodec: undefined,
|
||||||
videoCodec: session.TranscodingInfo?.VideoCodec ?? undefined,
|
sourceVideoCodec: undefined,
|
||||||
audioDecision: undefined,
|
timeStamp: undefined,
|
||||||
context: undefined,
|
transcodeHwRequested: undefined,
|
||||||
duration: undefined,
|
videoDecision: undefined,
|
||||||
error: undefined,
|
}
|
||||||
sourceAudioCodec: undefined,
|
: undefined,
|
||||||
sourceVideoCodec: undefined,
|
},
|
||||||
timeStamp: undefined,
|
type: convertJellyfinType(session.NowPlayingItem.Type),
|
||||||
transcodeHwRequested: undefined,
|
}
|
||||||
videoDecision: undefined,
|
: undefined,
|
||||||
}
|
userProfilePicture: undefined,
|
||||||
: undefined,
|
})
|
||||||
},
|
),
|
||||||
type: convertJellyfinType(session.NowPlayingItem.Type),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
userProfilePicture: undefined,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
import { Card, Divider, Flex, Grid, Group, Text } from '@mantine/core';
|
import { Card, Divider, Flex, Grid, Group, Text } from '@mantine/core';
|
||||||
import { IconDeviceMobile, IconId } from '@tabler/icons-react';
|
import { IconDeviceMobile, IconId } from '@tabler/icons-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { GenericSessionInfo } from '../../types/api/media-server/session-info';
|
import { GenericSessionInfo } from '../../types/api/media-server/session-info';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
export const DetailCollapseable = ({ session }: { session: GenericSessionInfo }) => {
|
export const DetailCollapseable = ({ session }: { session: GenericSessionInfo }) => {
|
||||||
let details: { title: string; metrics: { name: string; value: string | undefined }[] }[] = [];
|
let details: { title: string; metrics: { name: string; value: string | undefined }[] }[] = [];
|
||||||
const { t } = useTranslation('modules/media-server-list');
|
|
||||||
|
|
||||||
if (session.currentlyPlaying) {
|
if (session.currentlyPlaying) {
|
||||||
if (session.currentlyPlaying.metadata.video) {
|
if (session.currentlyPlaying.metadata.video) {
|
||||||
details = [
|
details = [
|
||||||
...details,
|
...details,
|
||||||
{
|
{
|
||||||
title: t('detail.video.'),
|
title: "Video",
|
||||||
metrics: [
|
metrics: [
|
||||||
{
|
{
|
||||||
name: t('detail.video.resolution'),
|
name: "Resolution",
|
||||||
value: `${session.currentlyPlaying.metadata.video.width}x${session.currentlyPlaying.metadata.video.height}`,
|
value: `${session.currentlyPlaying.metadata.video.width}x${session.currentlyPlaying.metadata.video.height}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('detail.video.framerate'),
|
name: "Framerate",
|
||||||
value: session.currentlyPlaying.metadata.video.videoFrameRate,
|
value: session.currentlyPlaying.metadata.video.videoFrameRate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('detail.video.codec'),
|
name: "Video Codec",
|
||||||
value: session.currentlyPlaying.metadata.video.videoCodec,
|
value: session.currentlyPlaying.metadata.video.videoCodec,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -41,14 +40,14 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
|||||||
details = [
|
details = [
|
||||||
...details,
|
...details,
|
||||||
{
|
{
|
||||||
title: t('detail.audio.audio'),
|
title: "Audio",
|
||||||
metrics: [
|
metrics: [
|
||||||
{
|
{
|
||||||
name: t('detail.audio.channels'),
|
name: "Audio Channels",
|
||||||
value: `${session.currentlyPlaying.metadata.audio.audioChannels}`,
|
value: `${session.currentlyPlaying.metadata.audio.audioChannels}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('detail.audio.codec'),
|
name: "Audio Codec",
|
||||||
value: session.currentlyPlaying.metadata.audio.audioCodec,
|
value: session.currentlyPlaying.metadata.audio.audioCodec,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -60,24 +59,24 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
|||||||
details = [
|
details = [
|
||||||
...details,
|
...details,
|
||||||
{
|
{
|
||||||
title: t('detail.transcoding.transcoding'),
|
title: "Transcoding",
|
||||||
metrics: [
|
metrics: [
|
||||||
{
|
{
|
||||||
name: t('detail.video.resolution'),
|
name: "Resolution",
|
||||||
value: `${session.currentlyPlaying.metadata.transcoding.width}x${session.currentlyPlaying.metadata.transcoding.height}`,
|
value: `${session.currentlyPlaying.metadata.transcoding.width}x${session.currentlyPlaying.metadata.transcoding.height}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('detail.transcoding.context'),
|
name: "Context",
|
||||||
value: session.currentlyPlaying.metadata.transcoding.context,
|
value: session.currentlyPlaying.metadata.transcoding.context,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('detail.transcoding.requested'),
|
name: "Hardware Encoding Requested",
|
||||||
value: session.currentlyPlaying.metadata.transcoding.transcodeHwRequested
|
value: session.currentlyPlaying.metadata.transcoding.transcodeHwRequested
|
||||||
? 'yes'
|
? 'yes'
|
||||||
: 'no',
|
: 'no',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('detail.transcoding.source'),
|
name: "Source Codec",
|
||||||
value:
|
value:
|
||||||
session.currentlyPlaying.metadata.transcoding.sourceAudioCodec ||
|
session.currentlyPlaying.metadata.transcoding.sourceAudioCodec ||
|
||||||
session.currentlyPlaying.metadata.transcoding.sourceVideoCodec
|
session.currentlyPlaying.metadata.transcoding.sourceVideoCodec
|
||||||
@@ -85,7 +84,7 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
|||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('detail.transcoding.target'),
|
name: "Target Codec",
|
||||||
value: `${session.currentlyPlaying.metadata.transcoding.videoCodec} ${session.currentlyPlaying.metadata.transcoding.audioCodec}`,
|
value: `${session.currentlyPlaying.metadata.transcoding.videoCodec} ${session.currentlyPlaying.metadata.transcoding.audioCodec}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -99,19 +98,19 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
|||||||
<Flex justify="space-between" mb="xs">
|
<Flex justify="space-between" mb="xs">
|
||||||
<Group>
|
<Group>
|
||||||
<IconId size={16} />
|
<IconId size={16} />
|
||||||
<Text>{t('detail.id')}</Text>
|
<Text>ID</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text>{session.id}</Text>
|
<Text>{session.id}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" mb="md">
|
<Flex justify="space-between" mb="md">
|
||||||
<Group>
|
<Group>
|
||||||
<IconDeviceMobile size={16} />
|
<IconDeviceMobile size={16} />
|
||||||
<Text>{t('detail.device')}</Text>
|
<Text>Device</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text>{session.sessionName}</Text>
|
<Text>{session.sessionName}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
{details.length > 0 && (
|
{details.length > 0 && (
|
||||||
<Divider label={t('detail.label')} labelPosition="center" mt="lg" mb="sm" />
|
<Divider label={"Stats for nerds"} labelPosition="center" mt="lg" mb="sm" />
|
||||||
)}
|
)}
|
||||||
<Grid>
|
<Grid>
|
||||||
{details.map((detail, index) => (
|
{details.map((detail, index) => (
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import { IconAlertTriangle, IconMovie } from '@tabler/icons-react';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import { AppAvatar } from '../../components/AppAvatar';
|
import { AppAvatar } from '../../components/AppAvatar';
|
||||||
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import { useGetMediaServers } from './useGetMediaServers';
|
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { TableRow } from './TableRow';
|
import { TableRow } from './TableRow';
|
||||||
|
import { useGetMediaServers } from './useGetMediaServers';
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
id: 'media-server',
|
id: 'media-server',
|
||||||
@@ -71,7 +70,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
|||||||
<Loader />
|
<Loader />
|
||||||
<Stack align="center" spacing={0}>
|
<Stack align="center" spacing={0}>
|
||||||
<Text>{t('descriptor.name')}</Text>
|
<Text>{t('descriptor.name')}</Text>
|
||||||
<Text color="dimmed">{t('descriptor.loading')}</Text>
|
<Text color="dimmed">{t('loading')}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -79,7 +78,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack h="100%">
|
<Stack h="100%">
|
||||||
<ScrollArea offsetScrollbars>
|
<ScrollArea offsetScrollbars h="100%">
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -99,7 +98,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
|||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<Group position="right" mt="auto">
|
<Group pos="absolute" bottom="15" right="15" mt="auto">
|
||||||
<Avatar.Group>
|
<Avatar.Group>
|
||||||
{data?.servers.map((server) => {
|
{data?.servers.map((server) => {
|
||||||
const app = config?.apps.find((x) => x.id === server.appId);
|
const app = config?.apps.find((x) => x.id === server.appId);
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { Flex, Stack, Text } from '@mantine/core';
|
import { Flex, Stack, Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
Icon,
|
|
||||||
IconDeviceTv,
|
IconDeviceTv,
|
||||||
IconHeadphones,
|
IconHeadphones,
|
||||||
IconMovie,
|
IconMovie,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
IconVideo,
|
IconVideo,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
|
|
||||||
import { GenericSessionInfo } from '../../types/api/media-server/session-info';
|
import { GenericSessionInfo } from '../../types/api/media-server/session-info';
|
||||||
|
|
||||||
export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo }) => {
|
export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo }) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (!session.currentlyPlaying) {
|
if (!session.currentlyPlaying) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon = (): Icon => {
|
const IconSelector = () => {
|
||||||
switch (session.currentlyPlaying?.type) {
|
switch (session.currentlyPlaying?.type) {
|
||||||
case 'audio':
|
case 'audio':
|
||||||
return IconHeadphones;
|
return IconHeadphones;
|
||||||
@@ -33,11 +30,12 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Test = Icon();
|
const Icon = IconSelector();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex wrap="nowrap" gap="sm" align="center">
|
<Flex wrap="nowrap" gap="sm" align="center">
|
||||||
<Test size={16} />
|
<Icon size={16} />
|
||||||
<Stack spacing={0} w={200}>
|
<Stack spacing={0}>
|
||||||
<Text lineClamp={1}>{session.currentlyPlaying.name}</Text>
|
<Text lineClamp={1}>{session.currentlyPlaying.name}</Text>
|
||||||
|
|
||||||
{session.currentlyPlaying.albumName ? (
|
{session.currentlyPlaying.albumName ? (
|
||||||
@@ -46,7 +44,7 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
|
|||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
session.currentlyPlaying.seasonName && (
|
session.currentlyPlaying.seasonName && (
|
||||||
<Text lineClamp={2} color="dimmed" size="xs">
|
<Text lineClamp={1} color="dimmed" size="xs">
|
||||||
{session.currentlyPlaying.seasonName} - {session.currentlyPlaying.episodeName}
|
{session.currentlyPlaying.seasonName} - {session.currentlyPlaying.episodeName}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user