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 { const token = super.getSecretValue("apiKey"); const response = await fetch(this.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(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 { const token = super.getSecretValue("apiKey"); await super.handleTestConnectionResponseAsync({ queryFunctionAsync: async () => { return await fetch(this.url("/"), { headers: { "X-Plex-Token": token, }, }); }, handleResponseAsync: async (response) => { try { const result = await response.text(); await PlexIntegration.parseXml(result); return; } catch { throw new IntegrationTestConnectionError("invalidCredentials"); } }, }); } static parseXml(xml: string): Promise { return parseStringPromise(xml) as Promise; } static getCurrentlyPlayingType(type: string): NonNullable["type"] { switch (type) { case "movie": return "movie"; case "episode": return "video"; case "track": return "audio"; default: return "video"; } } }