feat: plex integration (#1342)

* feat: plex integration

* feat: plex integration

* fix: DeepSource error

* fix: lint error

* fix: pnpm-lock

* fix: lint error

* fix: errors

* fix: pnpm-lock

* fix: reviewed changes

* fix: reviewed changes

* fix: reviewed changes

* fix: pnpm-lock
This commit is contained in:
Yossi Hillali
2024-10-23 17:18:55 +03:00
committed by GitHub
parent d4919dc908
commit cf9b0581a4
7 changed files with 190 additions and 12 deletions

View File

@@ -17,6 +17,7 @@ import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration"
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
import { PlexIntegration } from "../plex/plex-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
import type { Integration, IntegrationInput } from "./integration";
@@ -51,6 +52,7 @@ export const integrationCreators = {
adGuardHome: AdGuardHomeIntegration,
homeAssistant: HomeAssistantIntegration,
jellyfin: JellyfinIntegration,
plex: PlexIntegration,
sonarr: SonarrIntegration,
radarr: RadarrIntegration,
sabNzbd: SabnzbdIntegration,

View File

@@ -1,30 +1,31 @@
// General integrations
export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration";
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration";
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
// Types
export type { IntegrationInput } from "./base/integration";
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
export type { StreamSession } from "./interfaces/media-server/session";
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
export type { IntegrationInput } from "./base/integration";
// Schemas
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";

View File

@@ -0,0 +1,37 @@
interface MediaContainer {
Video?: Session[];
Track?: Session[];
}
interface Session {
User?: {
$: {
id: string;
title: string;
thumb?: string;
};
}[];
Player?: {
$: {
product: string;
title: string;
};
}[];
Session?: {
$: {
id: string;
};
}[];
$: {
grandparentTitle?: string;
parentTitle?: string;
title?: string;
index?: number;
type: string;
live?: string;
};
}
export interface PlexResponse {
MediaContainer: MediaContainer;
}

View File

@@ -0,0 +1,103 @@
import { parseStringPromise } from "xml2js";
import { logger } from "@homarr/log";
import { Integration } from "../base/integration";
import { IntegrationTestConnectionError } from "../base/test-connection-error";
import type { StreamSession } from "../interfaces/media-server/session";
import type { PlexResponse } from "./interface";
export class PlexIntegration extends Integration {
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
const token = super.getSecretValue("apiKey");
const response = await fetch(`${this.integration.url}/status/sessions`, {
headers: {
"X-Plex-Token": token,
},
});
const body = await response.text();
// convert xml response to objects, as there is no JSON api
const data = await PlexIntegration.parseXml<PlexResponse>(body);
const mediaContainer = data.MediaContainer;
const mediaElements = [mediaContainer.Video ?? [], mediaContainer.Track ?? []].flat();
// no sessions are open or available
if (mediaElements.length === 0) {
logger.info("No active video sessions found in MediaContainer");
return [];
}
const medias = mediaElements
.map((mediaElement): StreamSession | undefined => {
const userElement = mediaElement.User ? mediaElement.User[0] : undefined;
const playerElement = mediaElement.Player ? mediaElement.Player[0] : undefined;
const sessionElement = mediaElement.Session ? mediaElement.Session[0] : undefined;
if (!playerElement) {
return undefined;
}
return {
sessionId: sessionElement?.$.id ?? "unknown",
sessionName: `${playerElement.$.product} (${playerElement.$.title})`,
user: {
userId: userElement?.$.id ?? "Anonymous",
username: userElement?.$.title ?? "Anonymous",
profilePictureUrl: userElement?.$.thumb ?? null,
},
currentlyPlaying: {
type: mediaElement.$.live === "1" ? "tv" : PlexIntegration.getCurrentlyPlayingType(mediaElement.$.type),
name: mediaElement.$.grandparentTitle ?? mediaElement.$.title ?? "Unknown",
seasonName: mediaElement.$.parentTitle,
episodeName: mediaElement.$.title ?? null,
albumName: mediaElement.$.type === "track" ? (mediaElement.$.parentTitle ?? null) : null,
episodeCount: mediaElement.$.index ?? null,
},
};
})
.filter((session): session is StreamSession => session !== undefined);
return medias;
}
public async testConnectionAsync(): Promise<void> {
const token = super.getSecretValue("apiKey");
await super.handleTestConnectionResponseAsync({
queryFunctionAsync: async () => {
return await fetch(this.integration.url, {
headers: {
"X-Plex-Token": token,
},
});
},
handleResponseAsync: async (response) => {
try {
const result = await response.text();
await PlexIntegration.parseXml<PlexResponse>(result);
return;
} catch {
throw new IntegrationTestConnectionError("invalidCredentials");
}
},
});
}
static parseXml<T>(xml: string): Promise<T> {
return parseStringPromise(xml) as Promise<T>;
}
static getCurrentlyPlayingType(type: string): NonNullable<StreamSession["currentlyPlaying"]>["type"] {
switch (type) {
case "movie":
return "movie";
case "episode":
return "video";
case "track":
return "audio";
default:
return "video";
}
}
}