import type { AtLeastOneOf } from "@homarr/common/types"; import { logger } from "@homarr/log"; import { z } from "@homarr/validation"; import type { CalendarEvent } from "../../calendar-types"; import { radarrReleaseTypes } from "../../calendar-types"; import { MediaOrganizerIntegration } from "../media-organizer-integration"; export class RadarrIntegration extends MediaOrganizerIntegration { /** * Gets the events in the Radarr 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): Promise { const url = this.url("/api/v3/calendar", { start, end, unmonitored: includeUnmonitored, }); const response = await fetch(url, { headers: { "X-Api-Key": super.getSecretValue("apiKey"), }, }); const radarrCalendarEvents = await z.array(radarrCalendarEventSchema).parseAsync(await response.json()); return radarrCalendarEvents.map((radarrCalendarEvent): CalendarEvent => { const dates = radarrReleaseTypes .map((type) => (radarrCalendarEvent[type] ? { type, date: radarrCalendarEvent[type] } : undefined)) .filter((date) => date) as AtLeastOneOf[number]>; return { name: radarrCalendarEvent.title, subName: radarrCalendarEvent.originalTitle, description: radarrCalendarEvent.overview, thumbnail: this.chooseBestImageAsURL(radarrCalendarEvent), date: dates[0].date, dates, mediaInformation: { type: "movie", }, links: this.getLinksForRadarrCalendarEvent(radarrCalendarEvent), }; }); } private getLinksForRadarrCalendarEvent = (event: z.infer) => { const links: CalendarEvent["links"] = [ { href: this.url(`/movie/${event.titleSlug}`).toString(), name: "Radarr", logo: "/images/apps/radarr.svg", color: undefined, notificationColor: "yellow", isDark: true, }, ]; if (event.imdbId) { links.push({ href: `https://www.imdb.com/title/${event.imdbId}/`, name: "IMDb", color: "#f5c518", isDark: false, logo: "/images/apps/imdb.svg", }); } return links; }; private chooseBestImage = ( event: z.infer, ): z.infer["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): string | undefined => { const bestImage = this.chooseBestImage(event); if (!bestImage) { return undefined; } return bestImage.remoteUrl; }; public async testConnectionAsync(): Promise { await super.handleTestConnectionResponseAsync({ queryFunctionAsync: async () => { return await fetch(this.url("/api"), { headers: { "X-Api-Key": super.getSecretValue("apiKey") }, }); }, }); } } const radarrCalendarEventImageSchema = z.array( z.object({ coverType: z.enum(["screenshot", "poster", "banner", "fanart", "clearlogo"]), remoteUrl: z.string().url(), }), ); const radarrCalendarEventSchema = z.object({ title: z.string(), originalTitle: z.string(), inCinemas: z .string() .transform((value) => new Date(value)) .optional(), physicalRelease: z .string() .transform((value) => new Date(value)) .optional(), digitalRelease: z .string() .transform((value) => new Date(value)) .optional(), overview: z.string().optional(), titleSlug: z.string(), images: radarrCalendarEventImageSchema, imdbId: z.string().optional(), });