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",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
|
||||||
category: ["mediaService"],
|
category: ["mediaService"],
|
||||||
},
|
},
|
||||||
|
emby: {
|
||||||
|
name: "Emby",
|
||||||
|
secretKinds: [["apiKey"]],
|
||||||
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
|
||||||
|
category: ["mediaService"],
|
||||||
|
},
|
||||||
plex: {
|
plex: {
|
||||||
name: "Plex",
|
name: "Plex",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration"
|
|||||||
import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration";
|
import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration";
|
||||||
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
|
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
|
||||||
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
|
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
|
||||||
|
import { EmbyIntegration } from "../emby/emby-integration";
|
||||||
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
||||||
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
||||||
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
||||||
@@ -74,4 +75,5 @@ export const integrationCreators = {
|
|||||||
dashDot: DashDotIntegration,
|
dashDot: DashDotIntegration,
|
||||||
tdarr: TdarrIntegration,
|
tdarr: TdarrIntegration,
|
||||||
proxmox: ProxmoxIntegration,
|
proxmox: ProxmoxIntegration,
|
||||||
|
emby: EmbyIntegration,
|
||||||
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;
|
} 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 { Jellyfin } from "@jellyfin/sdk";
|
||||||
|
import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||||
import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-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}`);
|
throw new Error(`Jellyfin server ${this.url("/")} returned a non successful status code: ${sessions.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessions.data.map((sessionInfo): StreamSession => {
|
return sessions.data
|
||||||
let nowPlaying: StreamSession["currentlyPlaying"] | null = null;
|
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
||||||
|
.filter((sessionInfo) => sessionInfo.DeviceId !== "homarr")
|
||||||
|
.map((sessionInfo): StreamSession => {
|
||||||
|
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||||
|
|
||||||
if (sessionInfo.NowPlayingItem) {
|
if (sessionInfo.NowPlayingItem) {
|
||||||
nowPlaying = {
|
currentlyPlaying = {
|
||||||
type: "tv",
|
type: convertJellyfinType(sessionInfo.NowPlayingItem.Type),
|
||||||
name: sessionInfo.NowPlayingItem.Name ?? "",
|
name: sessionInfo.NowPlayingItem.SeriesName ?? sessionInfo.NowPlayingItem.Name ?? "",
|
||||||
seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "",
|
seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "",
|
||||||
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
|
episodeName: sessionInfo.NowPlayingItem.EpisodeTitle,
|
||||||
albumName: sessionInfo.NowPlayingItem.Album ?? "",
|
albumName: sessionInfo.NowPlayingItem.Album ?? "",
|
||||||
episodeCount: sessionInfo.NowPlayingItem.EpisodeCount,
|
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;
|
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() {
|
createOptions() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
supportedIntegrations: ["jellyfin", "plex"],
|
supportedIntegrations: ["jellyfin", "plex", "emby"],
|
||||||
}).withDynamicImport(() => import("./component"));
|
}).withDynamicImport(() => import("./component"));
|
||||||
|
|||||||
Reference in New Issue
Block a user