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:
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
124
packages/widgets/src/media-server/component.tsx
Normal file
124
packages/widgets/src/media-server/component.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
packages/widgets/src/media-server/index.ts
Normal file
11
packages/widgets/src/media-server/index.ts
Normal 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"));
|
||||
21
packages/widgets/src/media-server/serverData.ts
Normal file
21
packages/widgets/src/media-server/serverData.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user