mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
Merge branch 'gridstack' of https://github.com/manuel-rw/homarr into gridstack
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
"name": "Weather",
|
"name": "Weather",
|
||||||
"description": "Look up the current weather in your location",
|
"description": "Look up the current weather in your location",
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"title": "Settings for weather integration",
|
||||||
"displayInFahrenheit": {
|
"displayInFahrenheit": {
|
||||||
"label": "Display in Fahrenheit"
|
"label": "Display in Fahrenheit"
|
||||||
},
|
},
|
||||||
@@ -29,4 +30,4 @@
|
|||||||
"unknown": "Unknown"
|
"unknown": "Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx
Normal file
64
src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx
Normal file
@@ -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 <div>{date.getDate()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
withinPortal
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
transition="pop"
|
||||||
|
onClose={close}
|
||||||
|
opened={opened}
|
||||||
|
>
|
||||||
|
<Popover.Target>
|
||||||
|
<Box onClick={open}>
|
||||||
|
<DayIndicator color={'red'} position="bottom-start" medias={medias.books}>
|
||||||
|
<DayIndicator color={'yellow'} position="top-start" medias={medias.movies}>
|
||||||
|
<DayIndicator color={'blue'} position="top-end" medias={medias.tvShows}>
|
||||||
|
<DayIndicator color={'green'} position="bottom-end" medias={medias.musics}>
|
||||||
|
<div>{date.getDate()}</div>
|
||||||
|
</DayIndicator>
|
||||||
|
</DayIndicator>
|
||||||
|
</DayIndicator>
|
||||||
|
</DayIndicator>
|
||||||
|
</Box>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<MediaList medias={medias} />
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Indicator size={10} withBorder offset={5} color={color} position={position}>
|
||||||
|
{children}
|
||||||
|
</Indicator>
|
||||||
|
);
|
||||||
|
};
|
||||||
100
src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx
Normal file
100
src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx
Normal file
@@ -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 (
|
||||||
|
<Card className={className} withBorder p="xs">
|
||||||
|
<Calendar
|
||||||
|
month={month}
|
||||||
|
onMonthChange={setMonth}
|
||||||
|
size="xs"
|
||||||
|
fullWidth
|
||||||
|
onChange={() => {}}
|
||||||
|
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) => (
|
||||||
|
<CalendarDay date={date} medias={getReleasedMediasForDate(medias, date)} />
|
||||||
|
)}
|
||||||
|
></Calendar>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
66
src/components/Dashboard/Tiles/Calendar/MediaList.tsx
Normal file
66
src/components/Dashboard/Tiles/Calendar/MediaList.tsx
Normal file
@@ -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 (
|
||||||
|
<ScrollArea
|
||||||
|
offsetScrollbars
|
||||||
|
scrollbarSize={5}
|
||||||
|
pt={5}
|
||||||
|
className={classes.scrollArea}
|
||||||
|
styles={{
|
||||||
|
viewport: {
|
||||||
|
maxHeight: 450,
|
||||||
|
minHeight: 210,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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')}
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapMedias = (
|
||||||
|
medias: any[],
|
||||||
|
MediaComponent: (props: { media: any }) => JSX.Element | null,
|
||||||
|
containsLastItem: boolean
|
||||||
|
) => {
|
||||||
|
return medias.map((media, index) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<MediaComponent media={media} />
|
||||||
|
{containsLastItem && index === medias.length - 1 ? null : <MediaDivider />}
|
||||||
|
</React.Fragment>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const MediaDivider = () => <Divider variant="dashed" size="sm" my="xl" />;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}));
|
||||||
7
src/components/Dashboard/Tiles/Calendar/type.ts
Normal file
7
src/components/Dashboard/Tiles/Calendar/type.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface MediasType {
|
||||||
|
tvShows: any[]; // Sonarr
|
||||||
|
movies: any[]; // Radarr
|
||||||
|
musics: any[]; // Lidarr
|
||||||
|
books: any[]; // Readarr
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { openContextModalGeneric } from '../../../../tools/mantineModalManagerEx
|
|||||||
import { IntegrationsType } from '../../../../types/integration';
|
import { IntegrationsType } from '../../../../types/integration';
|
||||||
import { TileBaseType } from '../../../../types/tile';
|
import { TileBaseType } from '../../../../types/tile';
|
||||||
import { GenericTileMenu } from '../GenericTileMenu';
|
import { GenericTileMenu } from '../GenericTileMenu';
|
||||||
import { IntegrationChangePositionModalInnerProps } from '../IntegrationChangePositionModal';
|
import { IntegrationChangePositionModalInnerProps } from '../../Modals/ChangePosition/ChangeIntegrationPositionModal';
|
||||||
import { IntegrationRemoveModalInnerProps } from '../IntegrationRemoveModal';
|
import { IntegrationRemoveModalInnerProps } from '../IntegrationRemoveModal';
|
||||||
import {
|
import {
|
||||||
IntegrationEditModalInnerProps,
|
IntegrationEditModalInnerProps,
|
||||||
|
|||||||
64
src/components/Dashboard/Tiles/Service/ServicePing.tsx
Normal file
64
src/components/Dashboard/Tiles/Service/ServicePing.tsx
Normal file
@@ -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 (
|
||||||
|
<motion.div
|
||||||
|
style={{ position: 'absolute', bottom: 20, right: 20 }}
|
||||||
|
animate={{
|
||||||
|
scale: isOnline ? [1, 0.7, 1] : 1,
|
||||||
|
}}
|
||||||
|
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
withinPortal
|
||||||
|
radius="lg"
|
||||||
|
label={
|
||||||
|
isLoading
|
||||||
|
? t('states.loading')
|
||||||
|
: isOnline
|
||||||
|
? t('states.online', { response: data.status })
|
||||||
|
: t('states.offline', { response: data?.status })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Indicator
|
||||||
|
size={15}
|
||||||
|
color={isLoading ? 'yellow' : isOnline ? 'green' : 'red'}
|
||||||
|
children={null}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type PingState = 'loading' | 'down' | 'online';
|
||||||
@@ -7,6 +7,7 @@ import { useEditModeStore } from '../../Views/useEditModeStore';
|
|||||||
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
||||||
import { BaseTileProps } from '../type';
|
import { BaseTileProps } from '../type';
|
||||||
import { ServiceMenu } from './ServiceMenu';
|
import { ServiceMenu } from './ServiceMenu';
|
||||||
|
import { ServicePing } from './ServicePing';
|
||||||
|
|
||||||
interface ServiceTileProps extends BaseTileProps {
|
interface ServiceTileProps extends BaseTileProps {
|
||||||
service: ServiceType;
|
service: ServiceType;
|
||||||
@@ -59,7 +60,7 @@ export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
|||||||
{inner}
|
{inner}
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
)}
|
)}
|
||||||
{/*<ServicePing service={service} />*/}
|
<ServicePing service={service} />
|
||||||
</HomarrCardWrapper>
|
</HomarrCardWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { WeatherIcon } from './WeatherIcon';
|
|||||||
import { BaseTileProps } from '../type';
|
import { BaseTileProps } from '../type';
|
||||||
import { useWeatherForCity } from './useWeatherForCity';
|
import { useWeatherForCity } from './useWeatherForCity';
|
||||||
import { WeatherIntegrationType } from '../../../../types/integration';
|
import { WeatherIntegrationType } from '../../../../types/integration';
|
||||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
|
||||||
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
||||||
|
import { IntegrationsMenu } from '../Integrations/IntegrationsMenu';
|
||||||
|
|
||||||
interface WeatherTileProps extends BaseTileProps {
|
interface WeatherTileProps extends BaseTileProps {
|
||||||
module: WeatherIntegrationType | undefined;
|
module: WeatherIntegrationType | undefined;
|
||||||
@@ -45,8 +45,17 @@ export const WeatherTile = ({ className, module }: WeatherTileProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HomarrCardWrapper className={className}>
|
<HomarrCardWrapper className={className}>
|
||||||
|
<IntegrationsMenu
|
||||||
|
integration="weather"
|
||||||
|
module={module}
|
||||||
|
options={module?.properties}
|
||||||
|
labels={{
|
||||||
|
isFahrenheit: 'descriptor.settings.displayInFahrenheit.label',
|
||||||
|
location: 'descriptor.settings.location.label',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Center style={{ height: '100%' }}>
|
<Center style={{ height: '100%' }}>
|
||||||
<Group spacing="xl" noWrap align="center">
|
<Group spacing="md" noWrap align="center">
|
||||||
<WeatherIcon code={weather!.current_weather.weathercode} />
|
<WeatherIcon code={weather!.current_weather.weathercode} />
|
||||||
<Stack p={0} spacing={4}>
|
<Stack p={0} spacing={4}>
|
||||||
<Title order={2}>
|
<Title order={2}>
|
||||||
@@ -55,7 +64,7 @@ export const WeatherTile = ({ className, module }: WeatherTileProps) => {
|
|||||||
module?.properties.isFahrenheit
|
module?.properties.isFahrenheit
|
||||||
)}
|
)}
|
||||||
</Title>
|
</Title>
|
||||||
<Group spacing="sm">
|
<Group spacing="xs" noWrap>
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
{getPerferedUnit(
|
{getPerferedUnit(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IntegrationsType } from '../../../types/integration';
|
import { IntegrationsType } from '../../../types/integration';
|
||||||
|
import { CalendarTile } from './Calendar/CalendarTile';
|
||||||
import { ClockTile } from './Clock/ClockTile';
|
import { ClockTile } from './Clock/ClockTile';
|
||||||
import { EmptyTile } from './EmptyTile';
|
import { EmptyTile } from './EmptyTile';
|
||||||
import { ServiceTile } from './Service/ServiceTile';
|
import { ServiceTile } from './Service/ServiceTile';
|
||||||
@@ -34,7 +35,7 @@ export const Tiles: TileDefinitionProps = {
|
|||||||
maxHeight: 12,
|
maxHeight: 12,
|
||||||
},
|
},
|
||||||
calendar: {
|
calendar: {
|
||||||
component: EmptyTile, //CalendarTile,
|
component: CalendarTile,
|
||||||
minWidth: 4,
|
minWidth: 4,
|
||||||
maxWidth: 12,
|
maxWidth: 12,
|
||||||
minHeight: 5,
|
minHeight: 5,
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { getCookie } from 'cookies-next';
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { getConfig } from '../../../tools/getConfig';
|
import { getConfig } from '../../../tools/config/getConfig';
|
||||||
import { Config } from '../../../tools/types';
|
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
|
// Parse req.body as a ServiceItem
|
||||||
const { id } = req.body;
|
const { id } = req.body;
|
||||||
const { type } = req.query;
|
const { type } = req.query;
|
||||||
@@ -69,15 +68,97 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
// // Make a request to the URL
|
// // Make a request to the URL
|
||||||
// const response = await axios.get(url);
|
// const response = await axios.get(url);
|
||||||
// // Return the response
|
// // Return the response
|
||||||
}
|
}*/
|
||||||
|
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Filter out if the reuqest is a POST or a GET
|
// Filter out if the reuqest is a POST or a GET
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'GET') {
|
||||||
return Post(req, res);
|
return Get(req, res);
|
||||||
}
|
}
|
||||||
return res.status(405).json({
|
return res.status(405).json({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed',
|
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<ServiceIntegrationType['type'], string>([
|
||||||
|
['sonarr', '/api/calendar'],
|
||||||
|
['radarr', '/api/v3/calendar'],
|
||||||
|
['lidarr', '/api/v1/calendar'],
|
||||||
|
['readarr', '/api/v1/calendar'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
type ServiceIntegrationType = Exclude<ServiceType['integration'], undefined>;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const response = await ping.promise.probe(parsedUrl.hostname, {
|
const response = await ping.promise.probe(parsedUrl.hostname, {
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
// Return 200 if the alive property is true
|
// Return 200 if the alive property is true
|
||||||
if (response.alive) {
|
if (response.alive) {
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
7
src/tools/config/configExists.ts
Normal file
7
src/tools/config/configExists.ts
Normal file
@@ -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);
|
||||||
|
};
|
||||||
4
src/tools/config/generateConfigPath.ts
Normal file
4
src/tools/config/generateConfigPath.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const generateConfigPath = (configName: string) =>
|
||||||
|
path.join(process.cwd(), 'data/configs', `${configName}.json`);
|
||||||
9
src/tools/config/getConfig.ts
Normal file
9
src/tools/config/getConfig.ts
Normal file
@@ -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);
|
||||||
|
};
|
||||||
29
src/tools/config/getFallbackConfig.ts
Normal file
29
src/tools/config/getFallbackConfig.ts
Normal file
@@ -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: [],
|
||||||
|
});
|
||||||
7
src/tools/config/readConfig.ts
Normal file
7
src/tools/config/readConfig.ts
Normal file
@@ -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'));
|
||||||
|
}
|
||||||
8
src/tools/isToday.ts
Normal file
8
src/tools/isToday.ts
Normal file
@@ -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()
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user