feat: add readarr integration (#1446)

This commit is contained in:
Manuel
2024-11-20 22:23:25 +01:00
committed by GitHub
parent 72eda1f225
commit 563c5d8f3c
5 changed files with 119 additions and 6 deletions

View File

@@ -52,7 +52,6 @@ export const testConnectionAsync = async (
const { secrets: _, ...baseIntegration } = integration;
// @ts-expect-error - For now we expect an error here as not all integrations have been implemented
const integrationInstance = integrationCreator({
...baseIntegration,
decryptedSecrets,

View File

@@ -1,7 +1,6 @@
import dayjs from "dayjs";
import SuperJSON from "superjson";
import type { Modify } from "@homarr/common/types";
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { db } from "@homarr/db";
import { getItemsWithIntegrationsAsync } from "@homarr/db/queries";
@@ -26,9 +25,7 @@ export const mediaOrganizerJob = createCronJob("mediaOrganizer", EVERY_MINUTE).w
const end = dayjs().add(Number(options.filterFutureMonths), "months").toDate();
//Asserting the integration kind until all of them get implemented
const integrationInstance = integrationCreatorFromSecrets(
integration as Modify<typeof integration, { kind: "sonarr" | "radarr" | "lidarr" }>,
);
const integrationInstance = integrationCreatorFromSecrets(integration);
const events = await integrationInstance.getCalendarEventsAsync(start, end);

View File

@@ -14,6 +14,7 @@ import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
import { LidarrIntegration } from "../media-organizer/lidarr/lidarr-integration";
import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration";
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
@@ -66,4 +67,5 @@ export const integrationCreators = {
prowlarr: ProwlarrIntegration,
openmediavault: OpenMediaVaultIntegration,
lidarr: LidarrIntegration,
} satisfies Partial<Record<IntegrationKind, new (integration: IntegrationInput) => Integration>>;
readarr: ReadarrIntegration,
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;

View File

@@ -17,6 +17,7 @@ export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration";
export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integration";
// Types
export type { IntegrationInput } from "./base/integration";

View File

@@ -0,0 +1,114 @@
import { logger } from "@homarr/log";
import { z } from "@homarr/validation";
import type { CalendarEvent } from "../../calendar-types";
import { MediaOrganizerIntegration } from "../media-organizer-integration";
export class ReadarrIntegration extends MediaOrganizerIntegration {
public async testConnectionAsync(): Promise<void> {
await super.handleTestConnectionResponseAsync({
queryFunctionAsync: async () => {
return await fetch(`${this.integration.url}/api`, {
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
});
},
});
}
/**
* Gets the events in the Lidarr calendar between two dates.
* @param start The start date
* @param end The end date
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
*/
async getCalendarEventsAsync(
start: Date,
end: Date,
includeUnmonitored = true,
includeAuthor = true,
): Promise<CalendarEvent[]> {
const url = new URL(this.integration.url);
url.pathname = "/api/v1/calendar";
url.searchParams.append("start", start.toISOString());
url.searchParams.append("end", end.toISOString());
url.searchParams.append("unmonitored", includeUnmonitored.toString());
url.searchParams.append("includeAuthor", includeAuthor.toString());
const response = await fetch(url, {
headers: {
"X-Api-Key": super.getSecretValue("apiKey"),
},
});
const readarrCalendarEvents = await z.array(readarrCalendarEventSchema).parseAsync(await response.json());
return readarrCalendarEvents.map((readarrCalendarEvent): CalendarEvent => {
return {
name: readarrCalendarEvent.title,
subName: readarrCalendarEvent.author.authorName,
description: readarrCalendarEvent.overview,
thumbnail: this.chooseBestImageAsURL(readarrCalendarEvent),
date: readarrCalendarEvent.releaseDate,
mediaInformation: {
type: "audio",
},
links: this.getLinksForReadarrCalendarEvent(readarrCalendarEvent),
};
});
}
private getLinksForReadarrCalendarEvent = (event: z.infer<typeof readarrCalendarEventSchema>) => {
return [
{
href: `${this.integration.url}/author/${event.author.foreignAuthorId}`,
color: "#f5c518",
isDark: false,
logo: "/images/apps/readarr.svg",
name: "Readarr",
notificationColor: "#f5c518",
},
] satisfies CalendarEvent["links"];
};
private chooseBestImage = (
event: z.infer<typeof readarrCalendarEventSchema>,
): z.infer<typeof readarrCalendarEventSchema>["images"][number] | undefined => {
const flatImages = [...event.images];
const sortedImages = flatImages.sort(
(imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType),
);
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
return sortedImages[0];
};
private chooseBestImageAsURL = (event: z.infer<typeof readarrCalendarEventSchema>): string | undefined => {
const bestImage = this.chooseBestImage(event);
if (!bestImage) {
return undefined;
}
return `${this.integration.url}${bestImage.url}`;
};
}
const readarrCalendarEventImageSchema = z.array(
z.object({
coverType: z.enum(["screenshot", "poster", "banner", "fanart", "clearlogo", "cover"]),
url: z.string().transform((url) => url.replace(/\?lastWrite=[0-9]+/, "")), // returns a random string, needs to be removed for loading the image
}),
);
const readarrCalendarEventSchema = z.object({
title: z.string(),
overview: z.string().optional(),
images: readarrCalendarEventImageSchema,
links: z.array(
z.object({
name: z.string(),
url: z.string(),
}),
),
author: z.object({
authorName: z.string(),
foreignAuthorId: z.string(),
}),
releaseDate: z.string().transform((value) => new Date(value)),
});