diff --git a/packages/integrations/src/calendar-types.ts b/packages/integrations/src/calendar-types.ts index f9b97b431..95399c4b1 100644 --- a/packages/integrations/src/calendar-types.ts +++ b/packages/integrations/src/calendar-types.ts @@ -1,7 +1,11 @@ +export const radarrReleaseTypes = ["inCinemas", "digitalRelease", "physicalRelease"] as const; +type ReleaseType = (typeof radarrReleaseTypes)[number]; + export interface CalendarEvent { name: string; subName: string; date: Date; + dates?: { type: ReleaseType; date: Date }[]; description?: string; thumbnail?: string; mediaInformation?: { diff --git a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts index 1ef79bf78..a562ce2d0 100644 --- a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts +++ b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts @@ -1,8 +1,10 @@ +import type { AtLeastOneOf } from "@homarr/common/types"; import { logger } from "@homarr/log"; import { z } from "@homarr/validation"; import { Integration } from "../../base/integration"; import type { CalendarEvent } from "../../calendar-types"; +import { radarrReleaseTypes } from "../../calendar-types"; export class RadarrIntegration extends Integration { /** @@ -37,19 +39,23 @@ export class RadarrIntegration extends Integration { }); const radarrCalendarEvents = await z.array(radarrCalendarEventSchema).parseAsync(await response.json()); - return radarrCalendarEvents.map( - (radarrCalendarEvent): CalendarEvent => ({ + 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: radarrCalendarEvent.inCinemas, + date: dates[0].date, + dates, mediaInformation: { type: "movie", }, links: this.getLinksForRadarrCalendarEvent(radarrCalendarEvent), - }), - ); + }; + }); } private getLinksForRadarrCalendarEvent = (event: z.infer) => { @@ -118,7 +124,18 @@ const radarrCalendarEventImageSchema = z.array( const radarrCalendarEventSchema = z.object({ title: z.string(), originalTitle: z.string(), - inCinemas: z.string().transform((value) => new Date(value)), + 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, diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index cb95a7629..dde760af2 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -23,6 +23,7 @@ const optionMapping: OptionMapping = { }, "mediaRequests-requestStats": {}, calendar: { + releaseType: (oldOptions) => [oldOptions.radarrReleaseType], filterFutureMonths: () => undefined, filterPastMonths: () => undefined, }, diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index e5e8a64ca..24a149631 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -1031,6 +1031,14 @@ export default { name: "Calendar", description: "Display events from your integrations in a calendar view within a certain relative time period", option: { + releaseType: { + label: "Radarr release type", + options: { + inCinemas: "In cinemas", + digitalRelease: "Digital release", + physicalRelease: "Physical release", + }, + }, filterPastMonths: { label: "Start from", }, diff --git a/packages/widgets/src/calendar/calendar-event-list.tsx b/packages/widgets/src/calendar/calendar-event-list.tsx index 5c2115cd5..22eaa8660 100644 --- a/packages/widgets/src/calendar/calendar-event-list.tsx +++ b/packages/widgets/src/calendar/calendar-event-list.tsx @@ -15,6 +15,7 @@ import { IconClock } from "@tabler/icons-react"; import dayjs from "dayjs"; import type { CalendarEvent } from "@homarr/integrations/types"; +import { useI18n } from "@homarr/translation/client"; import classes from "./calendar-event-list.module.css"; @@ -24,6 +25,7 @@ interface CalendarEventListProps { export const CalendarEventList = ({ events }: CalendarEventListProps) => { const { colorScheme } = useMantineColorScheme(); + const t = useI18n(); return ( { {event.subName} )} - + {event.name} - - - {dayjs(event.date.toString()).format("HH:mm")} - + {event.dates ? ( + + + {t( + `widget.calendar.option.releaseType.options.${event.dates.find(({ date }) => event.date === date)?.type ?? "inCinemas"}`, + )} + + + ) : ( + + + {dayjs(event.date).format("HH:mm")} + + )} {event.description && ( diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx index d773c7fe5..bcdca811c 100644 --- a/packages/widgets/src/calendar/component.tsx +++ b/packages/widgets/src/calendar/component.tsx @@ -6,12 +6,18 @@ import { Calendar } from "@mantine/dates"; import dayjs from "dayjs"; import { clientApi } from "@homarr/api/client"; +import type { CalendarEvent } from "@homarr/integrations/types"; import type { WidgetComponentProps } from "../definition"; import { CalendarDay } from "./calender-day"; import classes from "./component.module.css"; -export default function CalendarWidget({ isEditMode, integrationIds, itemId }: WidgetComponentProps<"calendar">) { +export default function CalendarWidget({ + isEditMode, + integrationIds, + itemId, + options, +}: WidgetComponentProps<"calendar">) { const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery( { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -80,9 +86,16 @@ export default function CalendarWidget({ isEditMode, integrationIds, itemId }: W padding: 0, }, }} - renderDay={(date) => { - const eventsForDate = events.filter((event) => dayjs(event.date).isSame(date, "day")); - return ; + renderDay={(tileDate) => { + const eventsForDate = events + .map((event) => ({ + ...event, + date: (event.dates?.filter(({ type }) => options.releaseType.includes(type)) ?? [event]).find(({ date }) => + dayjs(date).isSame(tileDate, "day"), + )?.date, + })) + .filter((event): event is CalendarEvent => Boolean(event.date)); + return ; }} /> ); diff --git a/packages/widgets/src/calendar/index.ts b/packages/widgets/src/calendar/index.ts index e3243038c..8afb3fe1e 100644 --- a/packages/widgets/src/calendar/index.ts +++ b/packages/widgets/src/calendar/index.ts @@ -1,6 +1,7 @@ import { IconCalendar } from "@tabler/icons-react"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { radarrReleaseTypes } from "@homarr/integrations/types"; import { z } from "@homarr/validation"; import { createWidgetDefinition } from "../definition"; @@ -9,6 +10,13 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("calendar", { icon: IconCalendar, options: optionsBuilder.from((factory) => ({ + releaseType: factory.multiSelect({ + defaultValue: ["inCinemas", "digitalRelease"], + options: radarrReleaseTypes.map((value) => ({ + value, + label: (t) => t(`widget.calendar.option.releaseType.options.${value}`), + })), + }), filterPastMonths: factory.number({ validate: z.number().min(2).max(9999), defaultValue: 2,