feat: #1047 add overseerr search (#1411)

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2024-11-08 09:43:25 +01:00
committed by GitHub
parent 2a7d648049
commit aa503992af
25 changed files with 3661 additions and 52 deletions

View File

@@ -0,0 +1,3 @@
export interface ISearchableIntegration {
searchAsync(query: string): Promise<{ image?: string; name: string; link: string }[]>;
}

View File

@@ -1,13 +1,33 @@
import { z } from "@homarr/validation";
import { Integration } from "../base/integration";
import type { ISearchableIntegration } from "../base/searchable-integration";
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request";
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request";
/**
* Overseerr Integration. See https://api-docs.overseerr.dev
*/
export class OverseerrIntegration extends Integration {
export class OverseerrIntegration extends Integration implements ISearchableIntegration {
public async searchAsync(query: string): Promise<{ image?: string; name: string; link: string; text?: string }[]> {
const response = await fetch(`${this.integration.url}/api/v1/search?query=${query}`, {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
});
const schemaData = await searchSchema.parseAsync(await response.json());
if (!schemaData.results) {
return [];
}
return schemaData.results.map((result) => ({
name: "name" in result ? result.name : result.title,
link: `${this.integration.url}/${result.mediaType}/${result.id}`,
image: constructSearchResultImage(this.integration.url, result),
text: "overview" in result ? result.overview : undefined,
}));
}
public async testConnectionAsync(): Promise<void> {
const response = await fetch(`${this.integration.url}/api/v1/auth/me`, {
headers: {
@@ -180,6 +200,35 @@ interface MovieInformation {
releaseDate: string;
}
const searchSchema = z.object({
results: z
.array(
z.discriminatedUnion("mediaType", [
z.object({
id: z.number(),
mediaType: z.literal("tv"),
name: z.string(),
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
overview: z.string(),
}),
z.object({
id: z.number(),
mediaType: z.literal("movie"),
title: z.string(),
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
overview: z.string(),
}),
z.object({
id: z.number(),
mediaType: z.literal("person"),
name: z.string(),
profilePath: z.string().startsWith("/").endsWith(".jpg").nullable(),
}),
]),
)
.optional(),
});
const getRequestsSchema = z.object({
results: z
.array(
@@ -239,3 +288,32 @@ const getUsersSchema = z.object({
return val;
}),
});
const constructSearchResultImage = (
appUrl: string,
result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number],
) => {
const path = getResultImagePath(appUrl, result);
if (!path) {
return undefined;
}
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${path}`;
};
const getResultImagePath = (
appUrl: string,
result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number],
) => {
switch (result.mediaType) {
case "person":
return result.profilePath;
case "tv":
case "movie":
return result.posterPath;
default:
throw new Error(
`Unable to get search result image from media type '${(result as { mediaType: string }).mediaType}'`,
);
}
};

View File

@@ -4,3 +4,4 @@ export * from "./interfaces/health-monitoring/healt-monitoring";
export * from "./interfaces/indexer-manager/indexer";
export * from "./interfaces/media-requests/media-request";
export * from "./pi-hole/pi-hole-types";
export * from "./base/searchable-integration";