feat: add jellyfin integration (#672)

* feat: #655 implement jellyfin media server

* fix: table overflow

* feat: pr feedback

* refactor: format

* refactor: merge existing code

* fix: code smells

* refactor: format commit
This commit is contained in:
Manuel
2024-07-03 20:06:57 +02:00
committed by GitHub
parent 1cf119c768
commit bb8640b162
25 changed files with 435 additions and 17 deletions

View File

@@ -23,7 +23,7 @@ export default async function getServerDataAsync({ integrationIds, itemId }: Wid
(
item,
): item is Exclude<Exclude<RouterOutputs["widget"]["calendar"]["findAllEvents"][number], null>, undefined> =>
item !== null && item !== undefined,
item !== null,
)
.flatMap((item) => item.data),
};

View File

@@ -12,6 +12,7 @@ import type { WidgetComponentProps } from "./definition";
import * as dnsHoleSummary from "./dns-hole/summary";
import * as iframe from "./iframe";
import type { WidgetImportRecord } from "./import";
import * as mediaServer from "./media-server";
import * as notebook from "./notebook";
import * as smartHomeEntityState from "./smart-home/entity-state";
import * as smartHomeExecuteAutomation from "./smart-home/execute-automation";
@@ -34,6 +35,7 @@ export const widgetImports = {
dnsHoleSummary,
"smartHome-entityState": smartHomeEntityState,
"smartHome-executeAutomation": smartHomeExecuteAutomation,
mediaServer,
calendar,
} satisfies WidgetImportRecord;

View File

@@ -0,0 +1,124 @@
"use client";
import { useMemo } from "react";
import { Avatar, Box, Group, Text } from "@mantine/core";
import { useListState } from "@mantine/hooks";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
import { clientApi } from "@homarr/api/client";
import type { StreamSession } from "@homarr/integrations";
import type { WidgetComponentProps } from "../definition";
export default function MediaServerWidget({
serverData,
integrationIds,
isEditMode,
}: WidgetComponentProps<"mediaServer">) {
const [currentStreams, currentStreamsHandlers] = useListState<{ integrationId: string; sessions: StreamSession[] }>(
serverData?.initialData ?? [],
);
const columns = useMemo<MRT_ColumnDef<StreamSession>[]>(
() => [
{
accessorKey: "sessionName",
header: "Name",
},
{
accessorKey: "user.username",
header: "User",
Cell: ({ row }) => (
<Group gap={"xs"}>
<Avatar src={row.original.user.profilePictureUrl} size={"sm"} />
<Text>{row.original.user.username}</Text>
</Group>
),
},
{
accessorKey: "currentlyPlaying", // currentlyPlaying.name can be undefined which results in a warning. This is why we use currentlyPlaying instead of currentlyPlaying.name
header: "Currently playing",
Cell: ({ row }) => {
if (row.original.currentlyPlaying) {
return (
<div>
<span>{row.original.currentlyPlaying.name}</span>
</div>
);
}
return null;
},
},
],
[],
);
clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription(
{
integrationIds,
},
{
enabled: !isEditMode,
onData(data) {
currentStreamsHandlers.applyWhere(
(pair) => pair.integrationId === data.integrationId,
(pair) => {
return {
...pair,
sessions: data.data,
};
},
);
},
},
);
// Only render the flat list of sessions when the currentStreams change
// Otherwise it will always create a new array reference and cause the table to re-render
const flatSessions = useMemo(() => currentStreams.flatMap((pair) => pair.sessions), [currentStreams]);
const table = useMantineReactTable({
columns,
data: flatSessions,
enableRowSelection: false,
enableColumnOrdering: false,
enableFullScreenToggle: false,
enableGlobalFilter: false,
enableDensityToggle: false,
enableFilters: false,
enablePagination: true,
enableSorting: true,
enableHiding: false,
enableTopToolbar: false,
enableColumnActions: false,
enableStickyHeader: true,
initialState: {
density: "xs",
},
mantinePaperProps: {
display: "flex",
h: "100%",
withBorder: false,
style: {
flexDirection: "column",
},
},
mantineTableProps: {
style: {
tableLayout: "fixed",
},
},
mantineTableContainerProps: {
style: {
flexGrow: 5,
},
},
});
return (
<Box h="100%">
<MantineReactTable table={table} />
</Box>
);
}

View File

@@ -0,0 +1,11 @@
import { IconVideo } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
export const { componentLoader, definition, serverDataLoader } = createWidgetDefinition("mediaServer", {
icon: IconVideo,
options: {},
supportedIntegrations: ["jellyfin"],
})
.withServerData(() => import("./serverData"))
.withDynamicImport(() => import("./component"));

View File

@@ -0,0 +1,21 @@
"use server";
import { api } from "@homarr/api/server";
import type { WidgetProps } from "../definition";
export default async function getServerDataAsync({ integrationIds }: WidgetProps<"mediaServer">) {
if (integrationIds.length === 0) {
return {
initialData: [],
};
}
const currentStreams = await api.widget.mediaServer.getCurrentStreams({
integrationIds,
});
return {
initialData: currentStreams,
};
}