🏗️ Migrate media servers to tRPC

This commit is contained in:
Meier Lukas
2023-06-10 14:48:34 +02:00
parent 6eaf155b64
commit ed5e548257
3 changed files with 238 additions and 11 deletions

View File

@@ -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, enabled,
refetchInterval: 10 * 1000, refetchInterval: 10 * 1000,
}); }
);
};

View File

@@ -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

View 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;
}
};