diff --git a/public/locales/en/modules/weather.json b/public/locales/en/modules/weather.json index 405c36263..816b907b0 100644 --- a/public/locales/en/modules/weather.json +++ b/public/locales/en/modules/weather.json @@ -3,6 +3,7 @@ "name": "Weather", "description": "Look up the current weather in your location", "settings": { + "title": "Settings for weather integration", "displayInFahrenheit": { "label": "Display in Fahrenheit" }, @@ -29,4 +30,4 @@ "unknown": "Unknown" } } -} \ No newline at end of file +} diff --git a/src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx b/src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx new file mode 100644 index 000000000..830dc5901 --- /dev/null +++ b/src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx @@ -0,0 +1,64 @@ +import { Box, Indicator, IndicatorProps, Popover } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { MediaList } from './MediaList'; +import { MediasType } from './type'; + +interface CalendarDayProps { + date: Date; + medias: MediasType; +} + +export const CalendarDay = ({ date, medias }: CalendarDayProps) => { + const [opened, { close, open }] = useDisclosure(false); + + if (medias.totalCount === 0) { + return
{date.getDate()}
; + } + + return ( + + + + + + + +
{date.getDate()}
+
+
+
+
+
+
+ + + +
+ ); +}; + +interface DayIndicatorProps { + color: string; + medias: any[]; + children: JSX.Element; + position: IndicatorProps['position']; +} + +const DayIndicator = ({ color, medias, children, position }: DayIndicatorProps) => { + if (medias.length === 0) return children; + + return ( + + {children} + + ); +}; diff --git a/src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx b/src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx new file mode 100644 index 000000000..77220068a --- /dev/null +++ b/src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx @@ -0,0 +1,100 @@ +import { Card, createStyles, MantineThemeColors, useMantineTheme } from '@mantine/core'; +import { Calendar } from '@mantine/dates'; +import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useColorTheme } from '../../../../tools/color'; +import { isToday } from '../../../../tools/isToday'; +import { CalendarIntegrationType } from '../../../../types/integration'; +import { BaseTileProps } from '../type'; +import { CalendarDay } from './CalendarDay'; +import { MediasType } from './type'; + +interface CalendarTileProps extends BaseTileProps { + module: CalendarIntegrationType | undefined; +} + +export const CalendarTile = ({ className, module }: CalendarTileProps) => { + const { secondaryColor } = useColorTheme(); + const { name: configName } = useConfigContext(); + const { classes, cx } = useStyles(secondaryColor); + const { colorScheme, colors } = useMantineTheme(); + const [month, setMonth] = useState(new Date()); + + const { data: medias } = useQuery({ + queryKey: ['calendar/medias', { month: month.getMonth(), year: month.getFullYear() }], + queryFn: async () => + (await ( + await fetch( + `/api/modules/calendar?year=${month.getFullYear()}&month=${ + month.getMonth() + 1 + }&configName=${configName}` + ) + ).json()) as MediasType, + }); + + if (!module) return <>; + + return ( + + {}} + firstDayOfWeek={module.properties?.isWeekStartingAtSunday ? 'sunday' : 'monday'} + dayStyle={(date) => ({ + margin: 1, + backgroundColor: isToday(date) + ? colorScheme === 'dark' + ? colors.dark[5] + : colors.gray[0] + : undefined, + })} + styles={{ + calendarHeader: { + marginRight: 40, + marginLeft: 40, + }, + }} + allowLevelChange={false} + dayClassName={(_, modifiers) => cx({ [classes.weekend]: modifiers.weekend })} + renderDay={(date) => ( + + )} + > + + ); +}; + +const useStyles = createStyles((theme, secondaryColor: keyof MantineThemeColors) => ({ + weekend: { + color: `${secondaryColor} !important`, + }, +})); + +const getReleasedMediasForDate = (medias: MediasType | undefined, date: Date): MediasType => { + const books = + medias?.books.filter((b) => new Date(b.releaseDate).toDateString() === date.toDateString()) ?? + []; + const movies = + medias?.movies.filter((m) => new Date(m.inCinemas).toDateString() === date.toDateString()) ?? + []; + const musics = + medias?.musics.filter((m) => new Date(m.releaseDate).toDateString() === date.toDateString()) ?? + []; + const tvShows = + medias?.tvShows.filter( + (tv) => new Date(tv.airDateUtc).toDateString() === date.toDateString() + ) ?? []; + const totalCount = medias ? books.length + movies.length + musics.length + tvShows.length : 0; + + return { + books, + movies, + musics, + tvShows, + totalCount, + }; +}; diff --git a/src/components/Dashboard/Tiles/Calendar/MediaList.tsx b/src/components/Dashboard/Tiles/Calendar/MediaList.tsx new file mode 100644 index 000000000..5a33ae71c --- /dev/null +++ b/src/components/Dashboard/Tiles/Calendar/MediaList.tsx @@ -0,0 +1,66 @@ +import { createStyles, Divider, ScrollArea } from '@mantine/core'; +import React from 'react'; +import { + LidarrMediaDisplay, + RadarrMediaDisplay, + ReadarrMediaDisplay, + SonarrMediaDisplay, +} from '../../../../modules/common'; +import { MediasType } from './type'; + +interface MediaListProps { + medias: MediasType; +} + +export const MediaList = ({ medias }: MediaListProps) => { + const { classes } = useStyles(); + const lastMediaType = getLastMediaType(medias); + + return ( + + {mapMedias(medias.tvShows, SonarrMediaDisplay, lastMediaType === 'tv-show')} + {mapMedias(medias.movies, RadarrMediaDisplay, lastMediaType === 'movie')} + {mapMedias(medias.musics, LidarrMediaDisplay, lastMediaType === 'music')} + {mapMedias(medias.books, ReadarrMediaDisplay, lastMediaType === 'book')} + + ); +}; + +const mapMedias = ( + medias: any[], + MediaComponent: (props: { media: any }) => JSX.Element | null, + containsLastItem: boolean +) => { + return medias.map((media, index) => ( + + + {containsLastItem && index === medias.length - 1 ? null : } + + )); +}; + +const MediaDivider = () => ; + +const getLastMediaType = (medias: MediasType) => { + if (medias.books.length >= 1) return 'book'; + if (medias.musics.length >= 1) return 'music'; + if (medias.movies.length >= 1) return 'movie'; + return 'tv-show'; +}; + +const useStyles = createStyles(() => ({ + scrollArea: { + width: 400, + }, +})); diff --git a/src/components/Dashboard/Tiles/Calendar/type.ts b/src/components/Dashboard/Tiles/Calendar/type.ts new file mode 100644 index 000000000..c9b3b6bb9 --- /dev/null +++ b/src/components/Dashboard/Tiles/Calendar/type.ts @@ -0,0 +1,7 @@ +export interface MediasType { + tvShows: any[]; // Sonarr + movies: any[]; // Radarr + musics: any[]; // Lidarr + books: any[]; // Readarr + totalCount: number; +} diff --git a/src/components/Dashboard/Tiles/Integrations/IntegrationsMenu.tsx b/src/components/Dashboard/Tiles/Integrations/IntegrationsMenu.tsx index ac04fac11..3455bd340 100644 --- a/src/components/Dashboard/Tiles/Integrations/IntegrationsMenu.tsx +++ b/src/components/Dashboard/Tiles/Integrations/IntegrationsMenu.tsx @@ -4,7 +4,7 @@ import { openContextModalGeneric } from '../../../../tools/mantineModalManagerEx import { IntegrationsType } from '../../../../types/integration'; import { TileBaseType } from '../../../../types/tile'; import { GenericTileMenu } from '../GenericTileMenu'; -import { IntegrationChangePositionModalInnerProps } from '../IntegrationChangePositionModal'; +import { IntegrationChangePositionModalInnerProps } from '../../Modals/ChangePosition/ChangeIntegrationPositionModal'; import { IntegrationRemoveModalInnerProps } from '../IntegrationRemoveModal'; import { IntegrationEditModalInnerProps, diff --git a/src/components/Dashboard/Tiles/Service/ServicePing.tsx b/src/components/Dashboard/Tiles/Service/ServicePing.tsx new file mode 100644 index 000000000..21460b0d9 --- /dev/null +++ b/src/components/Dashboard/Tiles/Service/ServicePing.tsx @@ -0,0 +1,64 @@ +import { Indicator, Tooltip } from '@mantine/core'; +import { useQuery } from '@tanstack/react-query'; +import { motion } from 'framer-motion'; +import { useTranslation } from 'next-i18next'; +import { useConfigContext } from '../../../../config/provider'; +import { ServiceType } from '../../../../types/service'; + +interface ServicePingProps { + service: ServiceType; +} + +export const ServicePing = ({ service }: ServicePingProps) => { + const { t } = useTranslation('modules/ping'); + const { config } = useConfigContext(); + const active = + (config?.settings.customization.layout.enabledPing && service.network.enabledStatusChecker) ?? + false; + const { data, isLoading } = useQuery({ + queryKey: [`ping/${service.id}`], + queryFn: async () => { + const response = await fetch(`/api/modules/ping?url=${encodeURI(service.url)}`); + const isOk = service.network.okStatus.includes(response.status); + return { + status: response.status, + state: isOk ? 'online' : 'down', + }; + }, + enabled: active, + }); + + const isOnline = data?.state === 'online'; + + if (!active) return null; + + return ( + + + + + + ); +}; + +type PingState = 'loading' | 'down' | 'online'; diff --git a/src/components/Dashboard/Tiles/Service/ServiceTile.tsx b/src/components/Dashboard/Tiles/Service/ServiceTile.tsx index 827d50e2a..0bfd32263 100644 --- a/src/components/Dashboard/Tiles/Service/ServiceTile.tsx +++ b/src/components/Dashboard/Tiles/Service/ServiceTile.tsx @@ -7,6 +7,7 @@ import { useEditModeStore } from '../../Views/useEditModeStore'; import { HomarrCardWrapper } from '../HomarrCardWrapper'; import { BaseTileProps } from '../type'; import { ServiceMenu } from './ServiceMenu'; +import { ServicePing } from './ServicePing'; interface ServiceTileProps extends BaseTileProps { service: ServiceType; @@ -59,7 +60,7 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => { {inner} )} - {/**/} + ); }; diff --git a/src/components/Dashboard/Tiles/Weather/WeatherTile.tsx b/src/components/Dashboard/Tiles/Weather/WeatherTile.tsx index 7d022f49f..281ed7fd6 100644 --- a/src/components/Dashboard/Tiles/Weather/WeatherTile.tsx +++ b/src/components/Dashboard/Tiles/Weather/WeatherTile.tsx @@ -4,8 +4,8 @@ import { WeatherIcon } from './WeatherIcon'; import { BaseTileProps } from '../type'; import { useWeatherForCity } from './useWeatherForCity'; import { WeatherIntegrationType } from '../../../../types/integration'; -import { useCardStyles } from '../../../layout/useCardStyles'; import { HomarrCardWrapper } from '../HomarrCardWrapper'; +import { IntegrationsMenu } from '../Integrations/IntegrationsMenu'; interface WeatherTileProps extends BaseTileProps { module: WeatherIntegrationType | undefined; @@ -45,8 +45,17 @@ export const WeatherTile = ({ className, module }: WeatherTileProps) => { return ( +
- + @@ -55,7 +64,7 @@ export const WeatherTile = ({ className, module }: WeatherTileProps) => { module?.properties.isFahrenheit )} - +
{getPerferedUnit( diff --git a/src/components/Dashboard/Tiles/tilesDefinitions.tsx b/src/components/Dashboard/Tiles/tilesDefinitions.tsx index bf9fc4452..e16ad78ad 100644 --- a/src/components/Dashboard/Tiles/tilesDefinitions.tsx +++ b/src/components/Dashboard/Tiles/tilesDefinitions.tsx @@ -1,4 +1,5 @@ import { IntegrationsType } from '../../../types/integration'; +import { CalendarTile } from './Calendar/CalendarTile'; import { ClockTile } from './Clock/ClockTile'; import { EmptyTile } from './EmptyTile'; import { ServiceTile } from './Service/ServiceTile'; @@ -34,7 +35,7 @@ export const Tiles: TileDefinitionProps = { maxHeight: 12, }, calendar: { - component: EmptyTile, //CalendarTile, + component: CalendarTile, minWidth: 4, maxWidth: 12, minHeight: 5, diff --git a/src/modules/calendar/mediaExample.ts b/src/modules/calendar/mediaExample.ts deleted file mode 100644 index acdbc0352..000000000 --- a/src/modules/calendar/mediaExample.ts +++ /dev/null @@ -1,408 +0,0 @@ -export const medias = [ - { - title: 'Doctor Strange in the Multiverse of Madness', - originalTitle: 'Doctor Strange in the Multiverse of Madness', - originalLanguage: { - id: 1, - name: 'English', - }, - alternateTitles: [ - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange 2', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 1, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Доктор Стрэндж 2', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 11, - name: 'Russian', - }, - id: 2, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doutor Estranho 2', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 3, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange v multivesmíre šialenstva', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 4, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange 2: El multiverso de la locura', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 3, - name: 'Spanish', - }, - id: 5, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doktor Strange Deliliğin Çoklu Evreninde', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 17, - name: 'Turkish', - }, - id: 6, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'মহাবিশ্বের পাগলামিতে অদ্ভুত চিকিৎসক', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 7, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'จอมเวทย์มหากาฬ ในมัลติเวิร์สมหาภัย', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 28, - name: 'Thai', - }, - id: 8, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: "Marvel Studios' Doctor Strange in the Multiverse of Madness", - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 9, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange en el Multiverso de la Locura de Marvel Studios', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 3, - name: 'Spanish', - }, - id: 10, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doktors Streindžs neprāta multivisumā', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 11, - }, - ], - secondaryYearSourceId: 0, - sortTitle: 'doctor strange in multiverse madness', - sizeOnDisk: 0, - status: 'announced', - overview: - 'Doctor Strange, with the help of mystical allies both old and new, traverses the mind-bending and dangerous alternate realities of the Multiverse to confront a mysterious new adversary.', - inCinemas: '2022-05-04T00:00:00Z', - images: [ - { - coverType: 'poster', - url: 'https://image.tmdb.org/t/p/original/wRnbWt44nKjsFPrqSmwYki5vZtF.jpg', - }, - { - coverType: 'fanart', - url: 'https://image.tmdb.org/t/p/original/ndCSoasjIZAMMDIuMxuGnNWu4DU.jpg', - }, - ], - website: 'https://www.marvel.com/movies/doctor-strange-in-the-multiverse-of-madness', - year: 2022, - hasFile: false, - youTubeTrailerId: 'aWzlQ2N6qqg', - studio: 'Marvel Studios', - path: '/config/Doctor Strange in the Multiverse of Madness (2022)', - qualityProfileId: 1, - monitored: true, - minimumAvailability: 'announced', - isAvailable: true, - folderName: '/config/Doctor Strange in the Multiverse of Madness (2022)', - runtime: 126, - cleanTitle: 'doctorstrangeinmultiversemadness', - imdbId: 'tt9419884', - tmdbId: 453395, - titleSlug: '453395', - certification: 'PG-13', - genres: ['Fantasy', 'Action', 'Adventure'], - tags: [], - added: '2022-04-29T20:52:33Z', - ratings: { - tmdb: { - votes: 0, - value: 0, - type: 'user', - }, - }, - collection: { - name: 'Doctor Strange Collection', - tmdbId: 618529, - images: [], - }, - id: 1, - }, - { - title: 'Doctor Strange in the Multiverse of Madness', - originalTitle: 'Doctor Strange in the Multiverse of Madness', - originalLanguage: { - id: 1, - name: 'English', - }, - alternateTitles: [ - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange 2', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 1, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Доктор Стрэндж 2', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 11, - name: 'Russian', - }, - id: 2, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doutor Estranho 2', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 3, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange v multivesmíre šialenstva', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 4, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange 2: El multiverso de la locura', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 3, - name: 'Spanish', - }, - id: 5, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doktor Strange Deliliğin Çoklu Evreninde', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 17, - name: 'Turkish', - }, - id: 6, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'মহাবিশ্বের পাগলামিতে অদ্ভুত চিকিৎসক', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 7, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'จอมเวทย์มหากาฬ ในมัลติเวิร์สมหาภัย', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 28, - name: 'Thai', - }, - id: 8, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: "Marvel Studios' Doctor Strange in the Multiverse of Madness", - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 9, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doctor Strange en el Multiverso de la Locura de Marvel Studios', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 3, - name: 'Spanish', - }, - id: 10, - }, - { - sourceType: 'tmdb', - movieId: 1, - title: 'Doktors Streindžs neprāta multivisumā', - sourceId: 0, - votes: 0, - voteCount: 0, - language: { - id: 1, - name: 'English', - }, - id: 11, - }, - ], - secondaryYearSourceId: 0, - sortTitle: 'doctor strange in multiverse madness', - sizeOnDisk: 0, - status: 'announced', - overview: - 'Doctor Strange, with the help of mystical allies both old and new, traverses the mind-bending and dangerous alternate realities of the Multiverse to confront a mysterious new adversary.', - inCinemas: '2022-05-05T00:00:00Z', - images: [ - { - coverType: 'poster', - url: 'https://image.tmdb.org/t/p/original/wRnbWt44nKjsFPrqSmwYki5vZtF.jpg', - }, - { - coverType: 'fanart', - url: 'https://image.tmdb.org/t/p/original/ndCSoasjIZAMMDIuMxuGnNWu4DU.jpg', - }, - ], - website: 'https://www.marvel.com/movies/doctor-strange-in-the-multiverse-of-madness', - year: 2022, - hasFile: false, - youTubeTrailerId: 'aWzlQ2N6qqg', - studio: 'Marvel Studios', - path: '/config/Doctor Strange in the Multiverse of Madness (2022)', - qualityProfileId: 1, - monitored: true, - minimumAvailability: 'announced', - isAvailable: true, - folderName: '/config/Doctor Strange in the Multiverse of Madness (2022)', - runtime: 126, - cleanTitle: 'doctorstrangeinmultiversemadness', - imdbId: 'tt9419884', - tmdbId: 453395, - titleSlug: '453395', - certification: 'PG-13', - genres: ['Fantasy', 'Action', 'Adventure'], - tags: [], - added: '2022-04-29T20:52:33Z', - ratings: { - tmdb: { - votes: 0, - value: 0, - type: 'user', - }, - }, - collection: { - name: 'Doctor Strange Collection', - tmdbId: 618529, - images: [], - }, - id: 2, - }, -]; diff --git a/src/pages/api/modules/calendar.ts b/src/pages/api/modules/calendar.ts index 0bb41e545..36beae2b9 100644 --- a/src/pages/api/modules/calendar.ts +++ b/src/pages/api/modules/calendar.ts @@ -1,10 +1,9 @@ import axios from 'axios'; -import { getCookie } from 'cookies-next'; import { NextApiRequest, NextApiResponse } from 'next'; -import { getConfig } from '../../../tools/getConfig'; -import { Config } from '../../../tools/types'; +import { getConfig } from '../../../tools/config/getConfig'; +import { ServiceIntegrationApiKeyType, ServiceType } from '../../../types/service'; -async function Post(req: NextApiRequest, res: NextApiResponse) { +/*async function Post(req: NextApiRequest, res: NextApiResponse) { // Parse req.body as a ServiceItem const { id } = req.body; const { type } = req.query; @@ -69,15 +68,97 @@ async function Post(req: NextApiRequest, res: NextApiResponse) { // // Make a request to the URL // const response = await axios.get(url); // // Return the response -} +}*/ export default async (req: NextApiRequest, res: NextApiResponse) => { // Filter out if the reuqest is a POST or a GET - if (req.method === 'POST') { - return Post(req, res); + if (req.method === 'GET') { + return Get(req, res); } return res.status(405).json({ statusCode: 405, message: 'Method not allowed', }); }; + +async function Get(req: NextApiRequest, res: NextApiResponse) { + // Parse req.body as a ServiceItem + const { + month: monthString, + year: yearString, + configName, + } = req.query as { month: string; year: string; configName: string }; + + const month = parseInt(monthString); + const year = parseInt(yearString); + + if (isNaN(month) || isNaN(year) || !configName) { + return res.status(400).json({ + statusCode: 400, + message: 'Missing required parameter in url: year, month or configName', + }); + } + + const config = getConfig(configName); + + const mediaServiceIntegrationTypes: ServiceIntegrationType['type'][] = [ + 'sonarr', + 'radarr', + 'readarr', + 'lidarr', + ]; + const mediaServices = config.services.filter( + (service) => + service.integration && mediaServiceIntegrationTypes.includes(service.integration.type) + ); + + const medias = await Promise.all( + await mediaServices.map(async (service) => { + const integration = service.integration as ServiceIntegrationApiKeyType; + const endpoint = IntegrationTypeEndpointMap.get(integration.type); + if (!endpoint) + return { + type: integration.type, + items: [], + }; + + // Get the origin URL + let { href: origin } = new URL(service.url); + if (origin.endsWith('/')) { + origin = origin.slice(0, -1); + } + + const start = new Date(year, month - 1, 1); // First day of month + const end = new Date(year, month, 0); // Last day of month + + console.log( + `${origin}${endpoint}?apiKey=${integration.properties.apiKey}&end=${end}&start=${start}` + ); + return await axios + .get( + `${origin}${endpoint}?apiKey=${ + integration.properties.apiKey + }&end=${end.toISOString()}&start=${start.toISOString()}` + ) + .then((x) => ({ type: integration.type, items: x.data as any[] })); + }) + ); + + // FIXME: I need an integration for each of them + return res.status(200).json({ + tvShows: medias.filter((m) => m.type === 'sonarr').flatMap((m) => m.items), + movies: medias.filter((m) => m.type === 'radarr').flatMap((m) => m.items), + books: medias.filter((m) => m.type === 'readarr').flatMap((m) => m.items), + musics: medias.filter((m) => m.type === 'lidarr').flatMap((m) => m.items), + totalCount: medias.reduce((p, c) => p + c.items.length, 0), + }); +} + +const IntegrationTypeEndpointMap = new Map([ + ['sonarr', '/api/calendar'], + ['radarr', '/api/v3/calendar'], + ['lidarr', '/api/v1/calendar'], + ['readarr', '/api/v1/calendar'], +]); + +type ServiceIntegrationType = Exclude; diff --git a/src/pages/api/modules/ping.ts b/src/pages/api/modules/ping.ts index 1ac40498b..3ed773ee2 100644 --- a/src/pages/api/modules/ping.ts +++ b/src/pages/api/modules/ping.ts @@ -10,6 +10,9 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { const response = await ping.promise.probe(parsedUrl.hostname, { timeout: 1, }); + + console.log(response); + // Return 200 if the alive property is true if (response.alive) { return res.status(200).end(); diff --git a/src/tools/config/configExists.ts b/src/tools/config/configExists.ts new file mode 100644 index 000000000..f6c05b523 --- /dev/null +++ b/src/tools/config/configExists.ts @@ -0,0 +1,7 @@ +import fs from 'fs'; +import { generateConfigPath } from './generateConfigPath'; + +export const configExists = (name: string) => { + const path = generateConfigPath(name); + return fs.existsSync(path); +}; diff --git a/src/tools/config/generateConfigPath.ts b/src/tools/config/generateConfigPath.ts new file mode 100644 index 000000000..c1f2339a7 --- /dev/null +++ b/src/tools/config/generateConfigPath.ts @@ -0,0 +1,4 @@ +import path from 'path'; + +export const generateConfigPath = (configName: string) => + path.join(process.cwd(), 'data/configs', `${configName}.json`); diff --git a/src/tools/config/getConfig.ts b/src/tools/config/getConfig.ts new file mode 100644 index 000000000..3e0424f05 --- /dev/null +++ b/src/tools/config/getConfig.ts @@ -0,0 +1,9 @@ +import { ConfigType } from '../../types/config'; +import { configExists } from './configExists'; +import { getFallbackConfig } from './getFallbackConfig'; +import { readConfig } from './readConfig'; + +export const getConfig = (name: string): ConfigType => { + if (!configExists(name)) return getFallbackConfig(); + return readConfig(name); +}; diff --git a/src/tools/config/getFallbackConfig.ts b/src/tools/config/getFallbackConfig.ts new file mode 100644 index 000000000..835866fa4 --- /dev/null +++ b/src/tools/config/getFallbackConfig.ts @@ -0,0 +1,29 @@ +import { ConfigType } from '../../types/config'; + +export const getFallbackConfig = (name?: string): ConfigType => ({ + schemaVersion: '1.0.0', + configProperties: { + name: name ?? 'default', + }, + categories: [], + integrations: {}, + services: [], + settings: { + common: { + searchEngine: { + type: 'google', + }, + }, + customization: { + colors: {}, + layout: { + enabledDocker: true, + enabledLeftSidebar: true, + enabledPing: true, + enabledRightSidebar: true, + enabledSearchbar: true, + }, + }, + }, + wrappers: [], +}); diff --git a/src/tools/config/readConfig.ts b/src/tools/config/readConfig.ts new file mode 100644 index 000000000..c038d88b0 --- /dev/null +++ b/src/tools/config/readConfig.ts @@ -0,0 +1,7 @@ +import fs from 'fs'; +import { generateConfigPath } from './generateConfigPath'; + +export function readConfig(name: string) { + const path = generateConfigPath(name); + return JSON.parse(fs.readFileSync(path, 'utf8')); +} diff --git a/src/tools/isToday.ts b/src/tools/isToday.ts new file mode 100644 index 000000000..8ca8441f0 --- /dev/null +++ b/src/tools/isToday.ts @@ -0,0 +1,8 @@ +export const isToday = (date: Date) => { + const today = new Date(); + return ( + today.getDate() === date.getDate() && + today.getMonth() === date.getMonth() && + date.getFullYear() === date.getFullYear() + ); +};