feat(integrations): add ICal (#3980)

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Nicolas Newman
2025-09-17 11:57:50 -05:00
committed by GitHub
parent 0a1a75dc5f
commit fedbff3fd1
22 changed files with 395 additions and 196 deletions

View File

@@ -8,7 +8,7 @@ import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
import { mediaOrganizerPriorities } from "../media-organizer";
export class LidarrIntegration extends Integration implements ICalendarIntegration {
@@ -44,22 +44,28 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
const lidarrCalendarEvents = await z.array(lidarrCalendarEventSchema).parseAsync(await response.json());
return lidarrCalendarEvents.map((lidarrCalendarEvent): CalendarEvent => {
const imageSrc = this.chooseBestImage(lidarrCalendarEvent);
return {
name: lidarrCalendarEvent.title,
subName: lidarrCalendarEvent.artist.artistName,
description: lidarrCalendarEvent.overview,
thumbnail: this.chooseBestImageAsURL(lidarrCalendarEvent),
date: lidarrCalendarEvent.releaseDate,
mediaInformation: {
type: "audio",
},
title: lidarrCalendarEvent.title,
subTitle: lidarrCalendarEvent.artist.artistName,
description: lidarrCalendarEvent.overview ?? null,
startDate: lidarrCalendarEvent.releaseDate,
endDate: null,
image: imageSrc
? {
src: imageSrc.remoteUrl,
aspectRatio: { width: 7, height: 12 },
}
: null,
location: null,
indicatorColor: "cyan",
links: this.getLinksForLidarrCalendarEvent(lidarrCalendarEvent),
};
});
}
private getLinksForLidarrCalendarEvent = (event: z.infer<typeof lidarrCalendarEventSchema>) => {
const links: CalendarEvent["links"] = [];
const links: CalendarLink[] = [];
for (const link of event.artist.links) {
switch (link.name) {
@@ -70,7 +76,6 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
color: "#f5c518",
isDark: false,
logo: "/images/apps/vgmdb.svg",
notificationColor: "cyan",
});
break;
case "imdb":
@@ -80,7 +85,6 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
color: "#f5c518",
isDark: false,
logo: "/images/apps/imdb.png",
notificationColor: "cyan",
});
break;
case "last":
@@ -90,7 +94,6 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
color: "#cf222a",
isDark: false,
logo: "/images/apps/lastfm.svg",
notificationColor: "cyan",
});
break;
}

View File

@@ -1,15 +1,14 @@
import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import type { AtLeastOneOf } from "@homarr/common/types";
import { logger } from "@homarr/log";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration";
import { Integration } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
import { radarrReleaseTypes } from "../../interfaces/calendar/calendar-types";
import { mediaOrganizerPriorities } from "../media-organizer";
@@ -34,33 +33,44 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati
});
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<Exclude<CalendarEvent["dates"], undefined>[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),
};
return radarrCalendarEvents.flatMap((radarrCalendarEvent): CalendarEvent[] => {
const imageSrc = this.chooseBestImageAsURL(radarrCalendarEvent);
return radarrReleaseTypes
.map((releaseType) => ({ type: releaseType, date: radarrCalendarEvent[releaseType] }))
.filter((item) => item.date !== undefined)
.map((item) => ({
title: radarrCalendarEvent.title,
subTitle: radarrCalendarEvent.originalTitle,
description: radarrCalendarEvent.overview ?? null,
// Check is done above in the filter
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
startDate: item.date!,
endDate: null,
image: imageSrc
? {
src: imageSrc,
aspectRatio: { width: 7, height: 12 },
}
: null,
location: null,
metadata: {
type: "radarr",
releaseType: item.type,
},
indicatorColor: "yellow",
links: this.getLinksForRadarrCalendarEvent(radarrCalendarEvent),
}));
});
}
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
const links: CalendarEvent["links"] = [
const links: CalendarLink[] = [
{
href: this.url(`/movie/${event.titleSlug}`).toString(),
name: "Radarr",
logo: "/images/apps/radarr.svg",
color: undefined,
notificationColor: "yellow",
isDark: true,
},
];

View File

@@ -8,7 +8,7 @@ import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
import { mediaOrganizerPriorities } from "../media-organizer";
export class ReadarrIntegration extends Integration implements ICalendarIntegration {
@@ -50,15 +50,22 @@ export class ReadarrIntegration extends Integration implements ICalendarIntegrat
const readarrCalendarEvents = await z.array(readarrCalendarEventSchema).parseAsync(await response.json());
return readarrCalendarEvents.map((readarrCalendarEvent): CalendarEvent => {
const imageSrc = this.chooseBestImageAsURL(readarrCalendarEvent);
return {
name: readarrCalendarEvent.title,
subName: readarrCalendarEvent.author.authorName,
description: readarrCalendarEvent.overview,
thumbnail: this.chooseBestImageAsURL(readarrCalendarEvent),
date: readarrCalendarEvent.releaseDate,
mediaInformation: {
type: "audio",
},
title: readarrCalendarEvent.title,
subTitle: readarrCalendarEvent.author.authorName,
description: readarrCalendarEvent.overview ?? null,
startDate: readarrCalendarEvent.releaseDate,
endDate: null,
image: imageSrc
? {
src: imageSrc,
aspectRatio: { width: 7, height: 12 },
}
: null,
location: null,
indicatorColor: "#f5c518",
links: this.getLinksForReadarrCalendarEvent(readarrCalendarEvent),
};
});
@@ -72,9 +79,8 @@ export class ReadarrIntegration extends Integration implements ICalendarIntegrat
isDark: false,
logo: "/images/apps/readarr.svg",
name: "Readarr",
notificationColor: "#f5c518",
},
] satisfies CalendarEvent["links"];
] satisfies CalendarLink[];
};
private chooseBestImage = (

View File

@@ -8,7 +8,7 @@ import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
import { mediaOrganizerPriorities } from "../media-organizer";
export class SonarrIntegration extends Integration implements ICalendarIntegration {
@@ -33,33 +33,36 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
"X-Api-Key": super.getSecretValue("apiKey"),
},
});
const sonarCalendarEvents = await z.array(sonarrCalendarEventSchema).parseAsync(await response.json());
const sonarrCalendarEvents = await z.array(sonarrCalendarEventSchema).parseAsync(await response.json());
return sonarCalendarEvents.map(
(sonarCalendarEvent): CalendarEvent => ({
name: sonarCalendarEvent.title,
subName: sonarCalendarEvent.series.title,
description: sonarCalendarEvent.series.overview,
thumbnail: this.chooseBestImageAsURL(sonarCalendarEvent),
date: sonarCalendarEvent.airDateUtc,
mediaInformation: {
type: "tv",
episodeNumber: sonarCalendarEvent.episodeNumber,
seasonNumber: sonarCalendarEvent.seasonNumber,
},
links: this.getLinksForSonarCalendarEvent(sonarCalendarEvent),
}),
);
return sonarrCalendarEvents.map((event): CalendarEvent => {
const imageSrc = this.chooseBestImageAsURL(event);
return {
title: event.title,
subTitle: event.series.title,
description: event.series.overview ?? null,
startDate: event.airDateUtc,
endDate: null,
image: imageSrc
? {
src: imageSrc,
aspectRatio: { width: 7, height: 12 },
}
: null,
location: null,
indicatorColor: "blue",
links: this.getLinksForSonarrCalendarEvent(event),
};
});
}
private getLinksForSonarCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
const links: CalendarEvent["links"] = [
private getLinksForSonarrCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
const links: CalendarLink[] = [
{
href: this.url(`/series/${event.series.titleSlug}`).toString(),
name: "Sonarr",
logo: "/images/apps/sonarr.svg",
color: undefined,
notificationColor: "blue",
isDark: true,
},
];