mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
🏗️ Migrate media servers to tRPC
This commit is contained in:
@@ -1,17 +1,20 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useConfigContext } from '~/config/provider';
|
||||||
import { MediaServersResponseType } from '../../../types/api/media-server/response';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
interface GetMediaServersParams {
|
interface GetMediaServersParams {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGetMediaServers = ({ enabled }: GetMediaServersParams) =>
|
export const useGetMediaServers = ({ enabled }: GetMediaServersParams) => {
|
||||||
useQuery({
|
const { name: configName } = useConfigContext();
|
||||||
queryKey: ['media-servers'],
|
|
||||||
queryFn: async (): Promise<MediaServersResponseType> => {
|
return api.mediaServer.all.useQuery(
|
||||||
const response = await fetch('/api/modules/media-server');
|
{
|
||||||
return response.json();
|
configName: configName!,
|
||||||
},
|
},
|
||||||
enabled,
|
{
|
||||||
refetchInterval: 10 * 1000,
|
enabled,
|
||||||
});
|
refetchInterval: 10 * 1000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { dashDotRouter } from './routers/dash-dot';
|
|||||||
import { dnsHoleRouter } from './routers/dns-hole';
|
import { dnsHoleRouter } from './routers/dns-hole';
|
||||||
import { downloadRouter } from './routers/download';
|
import { downloadRouter } from './routers/download';
|
||||||
import { mediaRequestsRouter } from './routers/media-request';
|
import { mediaRequestsRouter } from './routers/media-request';
|
||||||
|
import { mediaServerRouter } from './routers/media-server';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -24,6 +25,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
dnsHole: dnsHoleRouter,
|
dnsHole: dnsHoleRouter,
|
||||||
download: downloadRouter,
|
download: downloadRouter,
|
||||||
mediaRequest: mediaRequestsRouter,
|
mediaRequest: mediaRequestsRouter,
|
||||||
|
mediaServer: mediaServerRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
222
src/server/api/routers/media-server.ts
Normal file
222
src/server/api/routers/media-server.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { Jellyfin } from '@jellyfin/sdk';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models';
|
||||||
|
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
||||||
|
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||||
|
import Consola from 'consola';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { getConfig } from '~/tools/config/getConfig';
|
||||||
|
import { PlexClient } from '~/tools/server/sdk/plex/plexClient';
|
||||||
|
import { GenericMediaServer } from '~/types/api/media-server/media-server';
|
||||||
|
import { MediaServersResponseType } from '~/types/api/media-server/response';
|
||||||
|
import { GenericCurrentlyPlaying, GenericSessionInfo } from '~/types/api/media-server/session-info';
|
||||||
|
import { ConfigAppType } from '~/types/app';
|
||||||
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
|
const jellyfin = new Jellyfin({
|
||||||
|
clientInfo: {
|
||||||
|
name: 'Homarr',
|
||||||
|
version: '0.0.1',
|
||||||
|
},
|
||||||
|
deviceInfo: {
|
||||||
|
name: 'Homarr Jellyfin Widget',
|
||||||
|
id: 'homarr-jellyfin-widget',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mediaServerRouter = createTRPCRouter({
|
||||||
|
all: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
configName: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const config = getConfig(input.configName);
|
||||||
|
|
||||||
|
const apps = config.apps.filter((app) =>
|
||||||
|
['jellyfin', 'plex'].includes(app.integration?.type ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
|
const servers = await Promise.all(
|
||||||
|
apps.map(async (app): Promise<GenericMediaServer | undefined> => {
|
||||||
|
try {
|
||||||
|
return await handleServer(app);
|
||||||
|
} catch (error) {
|
||||||
|
Consola.error(
|
||||||
|
`failed to communicate with media server '${app.name}' (${app.id}): ${error}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
serverAddress: app.url,
|
||||||
|
sessions: [],
|
||||||
|
success: false,
|
||||||
|
version: undefined,
|
||||||
|
type: undefined,
|
||||||
|
appId: app.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
servers: servers.filter(
|
||||||
|
(server): server is Exclude<typeof server, undefined> => server !== undefined
|
||||||
|
),
|
||||||
|
} satisfies MediaServersResponseType;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleServer = async (app: ConfigAppType): Promise<GenericMediaServer | undefined> => {
|
||||||
|
switch (app.integration?.type) {
|
||||||
|
case 'jellyfin': {
|
||||||
|
const username = app.integration.properties.find((x) => x.field === 'username');
|
||||||
|
|
||||||
|
if (!username || !username.value) {
|
||||||
|
return {
|
||||||
|
appId: app.id,
|
||||||
|
serverAddress: app.url,
|
||||||
|
sessions: [],
|
||||||
|
type: 'jellyfin',
|
||||||
|
version: undefined,
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = app.integration.properties.find((x) => x.field === 'password');
|
||||||
|
|
||||||
|
if (!password || !password.value) {
|
||||||
|
return {
|
||||||
|
appId: app.id,
|
||||||
|
serverAddress: app.url,
|
||||||
|
sessions: [],
|
||||||
|
type: 'jellyfin',
|
||||||
|
version: undefined,
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = jellyfin.createApi(app.url);
|
||||||
|
const infoApi = await getSystemApi(api).getPublicSystemInfo();
|
||||||
|
await api.authenticateUserByName(username.value, password.value);
|
||||||
|
const sessionApi = await getSessionApi(api);
|
||||||
|
const sessions = await sessionApi.getSessions();
|
||||||
|
return {
|
||||||
|
type: 'jellyfin',
|
||||||
|
appId: app.id,
|
||||||
|
serverAddress: app.url,
|
||||||
|
version: infoApi.data.Version ?? undefined,
|
||||||
|
sessions: sessions.data.map(
|
||||||
|
(session): GenericSessionInfo => ({
|
||||||
|
id: session.Id ?? '?',
|
||||||
|
username: session.UserName ?? undefined,
|
||||||
|
sessionName: `${session.Client} (${session.DeviceName})`,
|
||||||
|
supportsMediaControl: session.SupportsMediaControl ?? false,
|
||||||
|
currentlyPlaying: session.NowPlayingItem
|
||||||
|
? {
|
||||||
|
name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
|
||||||
|
seasonName: session.NowPlayingItem.SeasonName as string,
|
||||||
|
episodeName: session.NowPlayingItem.Name as string,
|
||||||
|
albumName: session.NowPlayingItem.Album as string,
|
||||||
|
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
|
||||||
|
metadata: {
|
||||||
|
video:
|
||||||
|
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
|
||||||
|
? {
|
||||||
|
audioChannels: session.TranscodingInfo.AudioChannels ?? undefined,
|
||||||
|
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
transcoding: session.TranscodingInfo
|
||||||
|
? {
|
||||||
|
audioChannels: session.TranscodingInfo.AudioChannels ?? -1,
|
||||||
|
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
|
||||||
|
container: session.TranscodingInfo.Container ?? undefined,
|
||||||
|
width: session.TranscodingInfo.Width ?? undefined,
|
||||||
|
height: session.TranscodingInfo.Height ?? undefined,
|
||||||
|
videoCodec: session.TranscodingInfo?.VideoCodec ?? undefined,
|
||||||
|
audioDecision: undefined,
|
||||||
|
context: undefined,
|
||||||
|
duration: undefined,
|
||||||
|
error: undefined,
|
||||||
|
sourceAudioCodec: undefined,
|
||||||
|
sourceVideoCodec: undefined,
|
||||||
|
timeStamp: undefined,
|
||||||
|
transcodeHwRequested: undefined,
|
||||||
|
videoDecision: undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
type: convertJellyfinType(session.NowPlayingItem.Type),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
userProfilePicture: undefined,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'plex': {
|
||||||
|
const apiKey = app.integration.properties.find((x) => x.field === 'apiKey');
|
||||||
|
|
||||||
|
if (!apiKey || !apiKey.value) {
|
||||||
|
return {
|
||||||
|
serverAddress: app.url,
|
||||||
|
sessions: [],
|
||||||
|
type: 'plex',
|
||||||
|
appId: app.id,
|
||||||
|
version: undefined,
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const plexClient = new PlexClient(app.url, apiKey.value);
|
||||||
|
const sessions = await plexClient.getSessions();
|
||||||
|
return {
|
||||||
|
serverAddress: app.url,
|
||||||
|
sessions,
|
||||||
|
type: 'plex',
|
||||||
|
version: undefined,
|
||||||
|
appId: app.id,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
Consola.warn(
|
||||||
|
`media-server api entered a fallback case. This should normally not happen and must be reported. Cause: '${app.name}' (${app.id})`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertJellyfinType = (kind: BaseItemKind | undefined): GenericCurrentlyPlaying['type'] => {
|
||||||
|
switch (kind) {
|
||||||
|
case BaseItemKind.Audio:
|
||||||
|
case BaseItemKind.MusicVideo:
|
||||||
|
return 'audio';
|
||||||
|
case BaseItemKind.Episode:
|
||||||
|
case BaseItemKind.Video:
|
||||||
|
return 'video';
|
||||||
|
case BaseItemKind.Movie:
|
||||||
|
return 'movie';
|
||||||
|
case BaseItemKind.TvChannel:
|
||||||
|
case BaseItemKind.TvProgram:
|
||||||
|
case BaseItemKind.LiveTvChannel:
|
||||||
|
case BaseItemKind.LiveTvProgram:
|
||||||
|
return 'tv';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user