feat(integrations): add ICal (#3980)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user