mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
feat(integration): add emby integration (#2466)
* feat(integration): add emby integration * fix: deepsource issue
This commit is contained in:
@@ -85,6 +85,12 @@ export const integrationDefs = {
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
|
||||
category: ["mediaService"],
|
||||
},
|
||||
emby: {
|
||||
name: "Emby",
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
|
||||
category: ["mediaService"],
|
||||
},
|
||||
plex: {
|
||||
name: "Plex",
|
||||
secretKinds: [["apiKey"]],
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration"
|
||||
import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration";
|
||||
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
|
||||
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
|
||||
import { EmbyIntegration } from "../emby/emby-integration";
|
||||
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
||||
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
||||
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
||||
@@ -74,4 +75,5 @@ export const integrationCreators = {
|
||||
dashDot: DashDotIntegration,
|
||||
tdarr: TdarrIntegration,
|
||||
proxmox: ProxmoxIntegration,
|
||||
emby: EmbyIntegration,
|
||||
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;
|
||||
|
||||
98
packages/integrations/src/emby/emby-integration.ts
Normal file
98
packages/integrations/src/emby/emby-integration.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import type { StreamSession } from "../interfaces/media-server/session";
|
||||
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
|
||||
|
||||
const sessionSchema = z.object({
|
||||
NowPlayingItem: z
|
||||
.object({
|
||||
Type: z.nativeEnum(BaseItemKind).optional(),
|
||||
SeriesName: z.string().nullish(),
|
||||
Name: z.string().nullish(),
|
||||
SeasonName: z.string().nullish(),
|
||||
EpisodeTitle: z.string().nullish(),
|
||||
Album: z.string().nullish(),
|
||||
EpisodeCount: z.number().nullish(),
|
||||
})
|
||||
.optional(),
|
||||
Id: z.string(),
|
||||
Client: z.string().nullish(),
|
||||
DeviceId: z.string().nullish(),
|
||||
DeviceName: z.string().nullish(),
|
||||
UserId: z.string().optional(),
|
||||
UserName: z.string().nullish(),
|
||||
});
|
||||
|
||||
export class EmbyIntegration extends Integration {
|
||||
private static readonly apiKeyHeader = "X-Emby-Token";
|
||||
private static readonly deviceId = "homarr-emby-integration";
|
||||
private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`;
|
||||
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
|
||||
await super.handleTestConnectionResponseAsync({
|
||||
queryFunctionAsync: async () => {
|
||||
return await fetchWithTrustedCertificatesAsync(super.url("/emby/System/Ping"), {
|
||||
headers: {
|
||||
[EmbyIntegration.apiKeyHeader]: apiKey,
|
||||
Authorization: EmbyIntegration.authorizationHeaderValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
const response = await fetchWithTrustedCertificatesAsync(super.url("/emby/Sessions"), {
|
||||
headers: {
|
||||
[EmbyIntegration.apiKeyHeader]: apiKey,
|
||||
Authorization: EmbyIntegration.authorizationHeaderValue,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Emby server ${this.integration.id} returned a non successful status code: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = z.array(sessionSchema).safeParse(await response.json());
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Emby server ${this.integration.id} returned an unexpected response: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return result.data
|
||||
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
||||
.filter((sessionInfo) => sessionInfo.DeviceId !== EmbyIntegration.deviceId)
|
||||
.map((sessionInfo): StreamSession => {
|
||||
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||
|
||||
if (sessionInfo.NowPlayingItem) {
|
||||
currentlyPlaying = {
|
||||
type: convertJellyfinType(sessionInfo.NowPlayingItem.Type),
|
||||
name: sessionInfo.NowPlayingItem.SeriesName ?? sessionInfo.NowPlayingItem.Name ?? "",
|
||||
seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "",
|
||||
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
|
||||
albumName: sessionInfo.NowPlayingItem.Album ?? "",
|
||||
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: `${sessionInfo.Id}`,
|
||||
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
||||
user: {
|
||||
profilePictureUrl: super.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||
userId: sessionInfo.UserId ?? "",
|
||||
username: sessionInfo.UserName ?? "",
|
||||
},
|
||||
currentlyPlaying,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
|
||||
@@ -34,31 +35,34 @@ export class JellyfinIntegration extends Integration {
|
||||
throw new Error(`Jellyfin server ${this.url("/")} returned a non successful status code: ${sessions.status}`);
|
||||
}
|
||||
|
||||
return sessions.data.map((sessionInfo): StreamSession => {
|
||||
let nowPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||
return sessions.data
|
||||
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
||||
.filter((sessionInfo) => sessionInfo.DeviceId !== "homarr")
|
||||
.map((sessionInfo): StreamSession => {
|
||||
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||
|
||||
if (sessionInfo.NowPlayingItem) {
|
||||
nowPlaying = {
|
||||
type: "tv",
|
||||
name: sessionInfo.NowPlayingItem.Name ?? "",
|
||||
seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "",
|
||||
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
|
||||
albumName: sessionInfo.NowPlayingItem.Album ?? "",
|
||||
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
|
||||
if (sessionInfo.NowPlayingItem) {
|
||||
currentlyPlaying = {
|
||||
type: convertJellyfinType(sessionInfo.NowPlayingItem.Type),
|
||||
name: sessionInfo.NowPlayingItem.SeriesName ?? sessionInfo.NowPlayingItem.Name ?? "",
|
||||
seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "",
|
||||
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
|
||||
albumName: sessionInfo.NowPlayingItem.Album ?? "",
|
||||
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: `${sessionInfo.Id}`,
|
||||
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
||||
user: {
|
||||
profilePictureUrl: this.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||
userId: sessionInfo.UserId ?? "",
|
||||
username: sessionInfo.UserName ?? "",
|
||||
},
|
||||
currentlyPlaying,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: `${sessionInfo.Id}`,
|
||||
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
||||
user: {
|
||||
profilePictureUrl: this.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||
userId: sessionInfo.UserId ?? "",
|
||||
username: sessionInfo.UserName ?? "",
|
||||
},
|
||||
currentlyPlaying: nowPlaying,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,3 +85,24 @@ export class JellyfinIntegration extends Integration {
|
||||
return apiClient;
|
||||
}
|
||||
}
|
||||
|
||||
export const convertJellyfinType = (
|
||||
kind: BaseItemKind | undefined,
|
||||
): Exclude<StreamSession["currentlyPlaying"], null>["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:
|
||||
default:
|
||||
return "tv";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,5 +7,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaServ
|
||||
createOptions() {
|
||||
return {};
|
||||
},
|
||||
supportedIntegrations: ["jellyfin", "plex"],
|
||||
supportedIntegrations: ["jellyfin", "plex", "emby"],
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
Reference in New Issue
Block a user