feat(media-server): add option to only show playing sessions (#2899)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import type { StreamSession } from "@homarr/integrations";
|
import type { StreamSession } from "@homarr/integrations";
|
||||||
@@ -14,10 +15,13 @@ const createMediaServerIntegrationMiddleware = (action: IntegrationAction) =>
|
|||||||
export const mediaServerRouter = createTRPCRouter({
|
export const mediaServerRouter = createTRPCRouter({
|
||||||
getCurrentStreams: publicProcedure
|
getCurrentStreams: publicProcedure
|
||||||
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
||||||
.query(async ({ ctx }) => {
|
.input(z.object({ showOnlyPlaying: z.boolean() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const innerHandler = mediaServerRequestHandler.handler(integration, {});
|
const innerHandler = mediaServerRequestHandler.handler(integration, {
|
||||||
|
showOnlyPlaying: input.showOnlyPlaying,
|
||||||
|
});
|
||||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
return {
|
return {
|
||||||
integrationId: integration.id,
|
integrationId: integration.id,
|
||||||
@@ -29,11 +33,14 @@ export const mediaServerRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
subscribeToCurrentStreams: publicProcedure
|
subscribeToCurrentStreams: publicProcedure
|
||||||
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
||||||
.subscription(({ ctx }) => {
|
.input(z.object({ showOnlyPlaying: z.boolean() }))
|
||||||
|
.subscription(({ ctx, input }) => {
|
||||||
return observable<{ integrationId: string; data: StreamSession[] }>((emit) => {
|
return observable<{ integrationId: string; data: StreamSession[] }>((emit) => {
|
||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
for (const integration of ctx.integrations) {
|
for (const integration of ctx.integrations) {
|
||||||
const innerHandler = mediaServerRequestHandler.handler(integration, {});
|
const innerHandler = mediaServerRequestHandler.handler(integration, {
|
||||||
|
showOnlyPlaying: input.showOnlyPlaying,
|
||||||
|
});
|
||||||
|
|
||||||
const unsubscribe = innerHandler.subscribe((sessions) => {
|
const unsubscribe = innerHandler.subscribe((sessions) => {
|
||||||
emit.next({
|
emit.next({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const mediaServerJob = createCronJob("mediaServer", EVERY_5_SECONDS).with
|
|||||||
createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, {
|
createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, {
|
||||||
widgetKinds: ["mediaServer"],
|
widgetKinds: ["mediaServer"],
|
||||||
getInput: {
|
getInput: {
|
||||||
mediaServer: () => ({}),
|
mediaServer: ({ showOnlyPlaying }) => ({ showOnlyPlaying }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
|
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import type { StreamSession } from "../interfaces/media-server/session";
|
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||||
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
|
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
|
||||||
|
|
||||||
const sessionSchema = z.object({
|
const sessionSchema = z.object({
|
||||||
@@ -47,7 +47,7 @@ export class EmbyIntegration extends Integration {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetchWithTrustedCertificatesAsync(super.url("/emby/Sessions"), {
|
const response = await fetchWithTrustedCertificatesAsync(super.url("/emby/Sessions"), {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -69,6 +69,7 @@ export class EmbyIntegration extends Integration {
|
|||||||
return result.data
|
return result.data
|
||||||
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
||||||
.filter((sessionInfo) => sessionInfo.DeviceId !== EmbyIntegration.deviceId)
|
.filter((sessionInfo) => sessionInfo.DeviceId !== EmbyIntegration.deviceId)
|
||||||
|
.filter((sessionInfo) => !options.showOnlyPlaying || sessionInfo.NowPlayingItem !== undefined)
|
||||||
.map((sessionInfo): StreamSession => {
|
.map((sessionInfo): StreamSession => {
|
||||||
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export interface StreamSession {
|
|||||||
episodeCount?: number | null;
|
episodeCount?: number | null;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CurrentSessionsInput {
|
||||||
|
showOnlyPlaying: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api";
|
|||||||
import { createAxiosCertificateInstanceAsync } from "@homarr/certificates/server";
|
import { createAxiosCertificateInstanceAsync } from "@homarr/certificates/server";
|
||||||
|
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import type { StreamSession } from "../interfaces/media-server/session";
|
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||||
|
|
||||||
export class JellyfinIntegration extends Integration {
|
export class JellyfinIntegration extends Integration {
|
||||||
private readonly jellyfin: Jellyfin = new Jellyfin({
|
private readonly jellyfin: Jellyfin = new Jellyfin({
|
||||||
@@ -26,7 +26,7 @@ export class JellyfinIntegration extends Integration {
|
|||||||
await systemApi.getPingSystem();
|
await systemApi.getPingSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||||
const api = await this.getApiAsync();
|
const api = await this.getApiAsync();
|
||||||
const sessionApi = getSessionApi(api);
|
const sessionApi = getSessionApi(api);
|
||||||
const sessions = await sessionApi.getSessions();
|
const sessions = await sessionApi.getSessions();
|
||||||
@@ -38,6 +38,7 @@ export class JellyfinIntegration extends Integration {
|
|||||||
return sessions.data
|
return sessions.data
|
||||||
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
||||||
.filter((sessionInfo) => sessionInfo.DeviceId !== "homarr")
|
.filter((sessionInfo) => sessionInfo.DeviceId !== "homarr")
|
||||||
|
.filter((sessionInfo) => !options.showOnlyPlaying || sessionInfo.NowPlayingItem !== undefined)
|
||||||
.map((sessionInfo): StreamSession => {
|
.map((sessionInfo): StreamSession => {
|
||||||
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { logger } from "@homarr/log";
|
|||||||
|
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||||
import type { StreamSession } from "../interfaces/media-server/session";
|
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||||
import type { PlexResponse } from "./interface";
|
import type { PlexResponse } from "./interface";
|
||||||
|
|
||||||
export class PlexIntegration extends Integration {
|
export class PlexIntegration extends Integration {
|
||||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||||
const token = super.getSecretValue("apiKey");
|
const token = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/status/sessions"), {
|
const response = await fetchWithTrustedCertificatesAsync(this.url("/status/sessions"), {
|
||||||
|
|||||||
@@ -133,7 +133,9 @@ const optionMapping: OptionMapping = {
|
|||||||
automationId: (oldOptions) => oldOptions.automationId,
|
automationId: (oldOptions) => oldOptions.automationId,
|
||||||
displayName: (oldOptions) => oldOptions.displayName,
|
displayName: (oldOptions) => oldOptions.displayName,
|
||||||
},
|
},
|
||||||
mediaServer: {},
|
mediaServer: {
|
||||||
|
showOnlyPlaying: () => undefined,
|
||||||
|
},
|
||||||
indexerManager: {
|
indexerManager: {
|
||||||
openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab,
|
openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-
|
|||||||
export const mediaServerRequestHandler = createCachedIntegrationRequestHandler<
|
export const mediaServerRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
StreamSession[],
|
StreamSession[],
|
||||||
IntegrationKindByCategory<"mediaService">,
|
IntegrationKindByCategory<"mediaService">,
|
||||||
Record<string, never>
|
{
|
||||||
|
showOnlyPlaying: boolean;
|
||||||
|
}
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, input) {
|
||||||
const integrationInstance = await createIntegrationAsync(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getCurrentSessionsAsync();
|
return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying });
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "seconds"),
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
queryKey: "mediaServerSessions",
|
queryKey: "mediaServerSessions",
|
||||||
|
|||||||
@@ -1766,7 +1766,12 @@
|
|||||||
"mediaServer": {
|
"mediaServer": {
|
||||||
"name": "Current media server streams",
|
"name": "Current media server streams",
|
||||||
"description": "Show the current streams on your media servers",
|
"description": "Show the current streams on your media servers",
|
||||||
"option": {},
|
"option": {
|
||||||
|
"showOnlyPlaying": {
|
||||||
|
"label": "Show only currently playing",
|
||||||
|
"description": "Disabling this will not work for plex"
|
||||||
|
}
|
||||||
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"currentlyPlaying": "Currently playing",
|
"currentlyPlaying": "Currently playing",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
|
|||||||
@@ -17,10 +17,15 @@ import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
|||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
|
|
||||||
export default function MediaServerWidget({ integrationIds, isEditMode }: WidgetComponentProps<"mediaServer">) {
|
export default function MediaServerWidget({
|
||||||
|
options,
|
||||||
|
integrationIds,
|
||||||
|
isEditMode,
|
||||||
|
}: WidgetComponentProps<"mediaServer">) {
|
||||||
const [currentStreams] = clientApi.widget.mediaServer.getCurrentStreams.useSuspenseQuery(
|
const [currentStreams] = clientApi.widget.mediaServer.getCurrentStreams.useSuspenseQuery(
|
||||||
{
|
{
|
||||||
integrationIds,
|
integrationIds,
|
||||||
|
showOnlyPlaying: options.showOnlyPlaying,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
@@ -80,21 +85,25 @@ export default function MediaServerWidget({ integrationIds, isEditMode }: Widget
|
|||||||
clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription(
|
clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription(
|
||||||
{
|
{
|
||||||
integrationIds,
|
integrationIds,
|
||||||
|
showOnlyPlaying: options.showOnlyPlaying,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !isEditMode,
|
enabled: !isEditMode,
|
||||||
onData(data) {
|
onData(data) {
|
||||||
utils.widget.mediaServer.getCurrentStreams.setData({ integrationIds }, (previousData) => {
|
utils.widget.mediaServer.getCurrentStreams.setData(
|
||||||
return previousData?.map((pair) => {
|
{ integrationIds, showOnlyPlaying: options.showOnlyPlaying },
|
||||||
if (pair.integrationId === data.integrationId) {
|
(previousData) => {
|
||||||
return {
|
return previousData?.map((pair) => {
|
||||||
...pair,
|
if (pair.integrationId === data.integrationId) {
|
||||||
sessions: data.data,
|
return {
|
||||||
};
|
...pair,
|
||||||
}
|
sessions: data.data,
|
||||||
return pair;
|
};
|
||||||
});
|
}
|
||||||
});
|
return pair;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { IconVideo } from "@tabler/icons-react";
|
import { IconVideo } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../definition";
|
import { createWidgetDefinition } from "../definition";
|
||||||
|
import { optionsBuilder } from "../options";
|
||||||
|
|
||||||
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
|
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
|
||||||
icon: IconVideo,
|
icon: IconVideo,
|
||||||
createOptions() {
|
createOptions() {
|
||||||
return {};
|
return optionsBuilder.from((factory) => ({
|
||||||
|
showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
supportedIntegrations: ["jellyfin", "plex", "emby"],
|
supportedIntegrations: ["jellyfin", "plex", "emby"],
|
||||||
}).withDynamicImport(() => import("./component"));
|
}).withDynamicImport(() => import("./component"));
|
||||||
|
|||||||
Reference in New Issue
Block a user