feat(widgets): add media release widget (#3219)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { parseStringPromise } from "xml2js";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ParseError } from "@homarr/common/server";
|
||||
import { ImageProxy } from "@homarr/image-proxy";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
@@ -10,9 +12,10 @@ import { TestConnectionError } from "../base/test-connection/test-connection-err
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types";
|
||||
import type { IMediaReleasesIntegration, MediaRelease } from "../types";
|
||||
import type { PlexResponse } from "./interface";
|
||||
|
||||
export class PlexIntegration extends Integration implements IMediaServerIntegration {
|
||||
export class PlexIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration {
|
||||
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||
const token = super.getSecretValue("apiKey");
|
||||
|
||||
@@ -66,6 +69,93 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat
|
||||
return medias;
|
||||
}
|
||||
|
||||
public async getMediaReleasesAsync(): Promise<MediaRelease[]> {
|
||||
const token = super.getSecretValue("apiKey");
|
||||
const machineIdentifier = await this.getMachineIdentifierAsync();
|
||||
const response = await fetchWithTrustedCertificatesAsync(super.url("/library/recentlyAdded"), {
|
||||
headers: {
|
||||
"X-Plex-Token": token,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await recentlyAddedSchema.parseAsync(await response.json());
|
||||
const imageProxy = new ImageProxy();
|
||||
|
||||
const images =
|
||||
data.MediaContainer.Metadata?.flatMap((item) => [
|
||||
{
|
||||
mediaKey: item.key,
|
||||
type: "poster",
|
||||
url: item.Image.find((image) => image?.type === "coverPoster")?.url,
|
||||
},
|
||||
{
|
||||
mediaKey: item.key,
|
||||
type: "backdrop",
|
||||
url: item.Image.find((image) => image?.type === "background")?.url,
|
||||
},
|
||||
]).filter(
|
||||
(image): image is { mediaKey: string; type: "poster" | "backdrop"; url: string } => image.url !== undefined,
|
||||
) ?? [];
|
||||
|
||||
const proxiedImages = await Promise.all(
|
||||
images.map(async (image) => {
|
||||
const imageUrl = super.url(image.url as `/${string}`);
|
||||
const proxiedImageUrl = await imageProxy
|
||||
.createImageAsync(imageUrl.toString(), {
|
||||
"X-Plex-Token": token,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.debug(new Error("Failed to proxy image", { cause: error }));
|
||||
return undefined;
|
||||
});
|
||||
return {
|
||||
mediaKey: image.mediaKey,
|
||||
type: image.type,
|
||||
url: proxiedImageUrl,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
data.MediaContainer.Metadata?.map((item) => {
|
||||
return {
|
||||
id: item.Media.at(0)?.id.toString() ?? item.key,
|
||||
type: item.type === "movie" ? "movie" : item.type === "tv" ? "tv" : "unknown",
|
||||
title: item.title,
|
||||
subtitle: item.tagline,
|
||||
description: item.summary,
|
||||
releaseDate: item.originallyAvailableAt
|
||||
? new Date(item.originallyAvailableAt)
|
||||
: new Date(item.addedAt * 1000),
|
||||
imageUrls: {
|
||||
poster: proxiedImages.find((image) => image.mediaKey === item.key && image.type === "poster")?.url,
|
||||
backdrop: proxiedImages.find((image) => image.mediaKey === item.key && image.type === "backdrop")?.url,
|
||||
},
|
||||
producer: item.studio,
|
||||
rating: item.rating?.toFixed(1),
|
||||
tags: item.Genre.map((genre) => genre.tag),
|
||||
href: super
|
||||
.url(`/web/index.html#!/server/${machineIdentifier}/details?key=${encodeURIComponent(item.key)}`)
|
||||
.toString(),
|
||||
length: item.duration ? Math.round(item.duration / 1000) : undefined,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
private async getMachineIdentifierAsync(): Promise<string> {
|
||||
const token = super.getSecretValue("apiKey");
|
||||
const response = await fetchWithTrustedCertificatesAsync(super.url("/identity"), {
|
||||
headers: {
|
||||
"X-Plex-Token": token,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const data = await identitySchema.parseAsync(await response.json());
|
||||
return data.MediaContainer.machineIdentifier;
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const token = super.getSecretValue("apiKey");
|
||||
|
||||
@@ -111,3 +201,50 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://plexapi.dev/api-reference/library/get-recently-added
|
||||
const recentlyAddedSchema = z.object({
|
||||
MediaContainer: z.object({
|
||||
Metadata: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
studio: z.string().optional(),
|
||||
type: z.string(), // For example "movie"
|
||||
title: z.string(),
|
||||
summary: z.string().optional(),
|
||||
duration: z.number().optional(),
|
||||
addedAt: z.number(),
|
||||
rating: z.number().optional(),
|
||||
tagline: z.string().optional(),
|
||||
originallyAvailableAt: z.string().optional(),
|
||||
Media: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
}),
|
||||
),
|
||||
Image: z.array(
|
||||
z
|
||||
.object({
|
||||
type: z.string(), // for example "coverPoster" or "background"
|
||||
url: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
Genre: z.array(
|
||||
z.object({
|
||||
tag: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// https://plexapi.dev/api-reference/server/get-server-identity
|
||||
const identitySchema = z.object({
|
||||
MediaContainer: z.object({
|
||||
machineIdentifier: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user