diff --git a/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx b/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx
index a618f6d2edd..77ae999ec79 100644
--- a/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx
+++ b/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx
@@ -71,7 +71,7 @@ export const EditAppModal = ({
behaviour: {
externalUrl: (url: string) => {
if (url === undefined || url.length < 1) {
- return null;
+ return 'External URI is required';
}
if (!url.match(appUrlWithAnyProtocolRegex)) {
diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx
index a352773843a..671570cb03a 100644
--- a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx
+++ b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx
@@ -101,7 +101,7 @@ export const AvailableElementTypes = ({
},
behaviour: {
isOpeningNewTab: true,
- externalUrl: '',
+ externalUrl: 'https://homarr.dev',
},
area: {
diff --git a/src/modules/Docker/ContainerActionBar.tsx b/src/modules/Docker/ContainerActionBar.tsx
index 518fd49df2e..31a792a5d96 100644
--- a/src/modules/Docker/ContainerActionBar.tsx
+++ b/src/modules/Docker/ContainerActionBar.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Button, Group } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import {
@@ -18,7 +17,6 @@ import { RouterInputs, api } from '~/utils/api';
import { useConfigContext } from '../../config/provider';
import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions';
-import { MatchingImages, ServiceType, tryMatchPort } from '../../tools/types';
import { AppType } from '../../types/app';
export interface ContainerActionBarProps {
@@ -28,10 +26,15 @@ export interface ContainerActionBarProps {
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
const { t } = useTranslation('modules/docker');
- const [isLoading, setisLoading] = useState(false);
+ const [isLoading, setLoading] = useState(false);
const { config } = useConfigContext();
- const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0];
+
const sendDockerCommand = useDockerActionMutation();
+ if (!config) {
+ return null;
+ }
+ const getLowestWrapper = () =>
+ config.wrappers.sort((wrapper1, wrapper2) => wrapper1.position - wrapper2.position)[0];
if (process.env.DISABLE_EDIT_MODE === 'true') {
return null;
@@ -42,10 +45,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
}
onClick={() => {
- setisLoading(true);
+ setLoading(true);
setTimeout(() => {
reload();
- setisLoading(false);
+ setLoading(false);
}, 750);
}}
variant="light"
@@ -57,8 +60,8 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
}
- onClick={() =>
- Promise.all(selected.map((container) => sendDockerCommand(container, 'restart')))
+ onClick={async () =>
+ await Promise.all(selected.map((container) => sendDockerCommand(container, 'restart')))
}
variant="light"
color="orange"
@@ -69,8 +72,8 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
}
- onClick={() =>
- Promise.all(selected.map((container) => sendDockerCommand(container, 'stop')))
+ onClick={async () =>
+ await Promise.all(selected.map((container) => sendDockerCommand(container, 'stop')))
}
variant="light"
color="red"
@@ -81,8 +84,8 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
}
- onClick={() =>
- Promise.all(selected.map((container) => sendDockerCommand(container, 'start')))
+ onClick={async () =>
+ await Promise.all(selected.map((container) => sendDockerCommand(container, 'start')))
}
variant="light"
color="green"
@@ -96,8 +99,8 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
color="red"
variant="light"
radius="md"
- onClick={() =>
- Promise.all(selected.map((container) => sendDockerCommand(container, 'remove')))
+ onClick={async () =>
+ await Promise.all(selected.map((container) => sendDockerCommand(container, 'remove')))
}
disabled={selected.length === 0}
>
@@ -108,29 +111,32 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
color="indigo"
variant="light"
radius="md"
- disabled={selected.length === 0 || selected.length > 1}
+ disabled={selected.length !== 1}
onClick={() => {
- const app = tryMatchService(selected.at(0)!);
- const containerUrl = `http://localhost:${selected[0].Ports[0]?.PublicPort ?? 0}`;
+ const containerInfo = selected[0];
+
+ const port = containerInfo.Ports.at(0)?.PublicPort;
+ const address = port ? `http://localhost:${port}` : `http://localhost`;
+ const name = containerInfo.Names.at(0) ?? 'App';
+
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
modal: 'editApp',
zIndex: 202,
innerProps: {
app: {
id: uuidv4(),
- name: app.name ? app.name : selected[0].Names[0].substring(1),
- url: containerUrl,
+ name: name,
+ url: address,
appearance: {
- iconUrl: app.icon ? app.icon : '/imgs/logo/logo.png',
+ iconUrl: '/imgs/logo/logo.png',
},
network: {
enabledStatusChecker: true,
- statusCodes: ['200'],
- okStatus: [200],
+ statusCodes: ['200', '301', '302']
},
behaviour: {
isOpeningNewTab: true,
- externalUrl: '',
+ externalUrl: address
},
area: {
type: 'wrapper',
@@ -204,36 +210,3 @@ const useDockerActionMutation = () => {
);
};
};
-
-/**
- * @deprecated legacy code
- */
-function tryMatchType(imageName: string): ServiceType {
- const match = MatchingImages.find(({ image }) => imageName.includes(image));
- if (match) {
- return match.type;
- }
- // TODO: Remove this legacy code
- return 'Other';
-}
-
-/**
- * @deprecated
- * @param container the container to match
- * @returns a new service
- */
-const tryMatchService = (container: Dockerode.ContainerInfo | undefined) => {
- if (container === undefined) return {};
- const name = container.Names[0].substring(1);
- const type = tryMatchType(container.Image);
- const port = tryMatchPort(type.toLowerCase())?.value ?? container.Ports[0]?.PublicPort;
- return {
- name,
- id: container.Id,
- type: tryMatchType(container.Image),
- url: `localhost${port ? `:${port}` : ''}`,
- icon: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${name
- .replace(/\s+/g, '-')
- .toLowerCase()}.png`,
- };
-};
diff --git a/src/modules/readme.md b/src/modules/readme.md
deleted file mode 100644
index 0b04f57301e..00000000000
--- a/src/modules/readme.md
+++ /dev/null
@@ -1,9 +0,0 @@
-**Each module has a set of rules:**
-- Exported Typed IModule element (Unique Name, description, component, ...)
-- Needs to be in a new folder
-- Needs to be exported in the modules/newmodule/index.tsx of the new folder
-- Needs to be imported in the modules/index.tsx file
-- Needs to look good when wrapped with the modules/ModuleWrapper component
-- Needs to be put somewhere fitting in the app (While waiting for the big AppStore overhall)
-- Any API Calls need to be safe and done on the widget itself (via useEffect or similar)
-- You can't add a package (unless there is a very specific need for it. Contact [@Ajnart](ajnart@pm.me) or make a [Discussion](https://github.com/ajnart/homarr/discussions/new).
diff --git a/src/pages/api/docker/DockerSingleton.ts b/src/pages/api/docker/DockerSingleton.ts
deleted file mode 100644
index 9694059bed0..00000000000
--- a/src/pages/api/docker/DockerSingleton.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import Docker from 'dockerode';
-
-export default class DockerSingleton extends Docker {
- private static dockerInstance: DockerSingleton;
-
- private constructor() {
- super();
- }
-
- public static getInstance(): DockerSingleton {
- if (!DockerSingleton.dockerInstance) {
- DockerSingleton.dockerInstance = new Docker({
- // If env variable DOCKER_HOST is not set, it will use the default socket
- ...(process.env.DOCKER_HOST && { host: process.env.DOCKER_HOST }),
- // Same thing for docker port
- ...(process.env.DOCKER_PORT && { port: process.env.DOCKER_PORT }),
- });
- }
- return DockerSingleton.dockerInstance;
- }
-}
diff --git a/src/pages/api/docker/container/[id].tsx b/src/pages/api/docker/container/[id].tsx
deleted file mode 100644
index 6853f651780..00000000000
--- a/src/pages/api/docker/container/[id].tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import DockerSingleton from '../DockerSingleton';
-
-const docker = DockerSingleton.getInstance();
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- // Get the slug of the request
- const { id } = req.query as { id: string };
- const { action } = req.query;
- // Get the action on the request (start, stop, restart)
- if (action !== 'start' && action !== 'stop' && action !== 'restart' && action !== 'remove') {
- return res.status(400).json({
- statusCode: 400,
- message: 'Invalid action',
- });
- }
- if (!id) {
- return res.status(400).json({
- message: 'Missing ID',
- });
- }
- // Get the container with the ID
- const container = docker.getContainer(id);
- const startAction = async () => {
- switch (action) {
- case 'remove':
- return container.remove();
- case 'start':
- return container.start();
- case 'stop':
- return container.stop();
- case 'restart':
- return container.restart();
- default:
- return Promise;
- }
- };
- try {
- await startAction();
- return res.status(200).json({
- statusCode: 200,
- message: `Container ${id} ${action}ed`,
- });
- } catch (err) {
- return res.status(500).json(err);
- }
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a Put or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/docker/containers.tsx b/src/pages/api/docker/containers.tsx
deleted file mode 100644
index a198c33df99..00000000000
--- a/src/pages/api/docker/containers.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import DockerSingleton from './DockerSingleton';
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- try {
- const docker = DockerSingleton.getInstance();
- const containers = await docker.listContainers({ all: true });
- res.status(200).json(containers);
- } catch (err) {
- res.status(500).json({ err });
- }
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/icons/index.ts b/src/pages/api/icons/index.ts
deleted file mode 100644
index 38d0eab0b3c..00000000000
--- a/src/pages/api/icons/index.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import { JsdelivrIconsRepository } from '../../../tools/server/images/jsdelivr-icons-repository';
-import { LocalIconsRepository } from '../../../tools/server/images/local-icons-repository';
-import { UnpkgIconsRepository } from '../../../tools/server/images/unpkg-icons-repository';
-
-const Get = async (request: NextApiRequest, response: NextApiResponse) => {
- const respositories = [
- new LocalIconsRepository(),
- new JsdelivrIconsRepository(
- JsdelivrIconsRepository.tablerRepository,
- 'Walkxcode Dashboard Icons',
- 'Walkxcode on Github'
- ),
- new UnpkgIconsRepository(
- UnpkgIconsRepository.tablerRepository,
- 'Tabler Icons',
- 'Tabler Icons - GitHub (MIT)'
- ),
- new JsdelivrIconsRepository(
- JsdelivrIconsRepository.papirusRepository,
- 'Papirus Icons',
- 'Papirus Development Team on GitHub (Apache 2.0)'
- ),
- new JsdelivrIconsRepository(
- JsdelivrIconsRepository.homelabSvgAssetsRepository,
- 'Homelab Svg Assets',
- 'loganmarchione on GitHub (MIT)'
- ),
- ];
- const fetches = respositories.map((rep) => rep.fetch());
- const data = await Promise.all(fetches);
- return response.status(200).json(data);
-};
-
-export default async (request: NextApiRequest, response: NextApiResponse) => {
- if (request.method === 'GET') {
- return Get(request, response);
- }
- return response.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/migrate.ts b/src/pages/api/migrate.ts
deleted file mode 100644
index b0bb1cb1a58..00000000000
--- a/src/pages/api/migrate.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import fs from 'fs';
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import { backendMigrateConfig } from '../../tools/config/backendMigrateConfig';
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Gets all the config files
- const configs = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
- // If there is no config, redirect to the index
- configs.every((config) => {
- const configData = JSON.parse(fs.readFileSync(`./data/configs/${config}`, 'utf8'));
- if (!configData.schemaVersion) {
- // Migrate the config
- backendMigrateConfig(configData, config.replace('.json', ''));
- }
- return config;
- });
- return res.status(200).json({
- success: true,
- message: 'Configs migrated',
- });
-};
diff --git a/src/pages/api/modules/calendar.ts b/src/pages/api/modules/calendar.ts
deleted file mode 100644
index 76c289dd129..00000000000
--- a/src/pages/api/modules/calendar.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import axios from 'axios';
-import Consola from 'consola';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { z } from 'zod';
-import { checkIntegrationsType } from '~/tools/client/app-properties';
-
-import { getConfig } from '../../../tools/config/getConfig';
-import { AppIntegrationType, IntegrationType } from '../../../types/app';
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
-
-const getQuerySchema = z.object({
- month: z
- .string()
- .regex(/^\d+$/)
- .transform((x) => parseInt(x, 10)),
- year: z
- .string()
- .regex(/^\d+$/)
- .transform((x) => parseInt(x, 10)),
- widgetId: z.string().uuid(),
- configName: z.string(),
-});
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- const parseResult = getQuerySchema.safeParse(req.query);
-
- if (!parseResult.success) {
- return res.status(400).json({
- statusCode: 400,
- message: 'Invalid query parameters, please specify the widgetId, month, year and configName',
- });
- }
-
- // Parse req.body as a AppItem
- const { month, year, widgetId, configName } = parseResult.data;
-
- const config = getConfig(configName);
-
- // Find the calendar widget in the config
- const calendar = config.widgets.find((w) => w.type === 'calendar' && w.id === widgetId);
- const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false;
-
- const mediaAppIntegrationTypes = [
- 'sonarr',
- 'radarr',
- 'readarr',
- 'lidarr',
- ] as const satisfies readonly IntegrationType[];
- const mediaApps = config.apps.filter((app) =>
- checkIntegrationsType(app.integration, mediaAppIntegrationTypes)
- );
-
- const IntegrationTypeEndpointMap = new Map([
- ['sonarr', useSonarrv4 ? '/api/v3/calendar' : '/api/calendar'],
- ['radarr', '/api/v3/calendar'],
- ['lidarr', '/api/v1/calendar'],
- ['readarr', '/api/v1/calendar'],
- ]);
-
- try {
- const medias = await Promise.all(
- await mediaApps.map(async (app) => {
- const integration = app.integration!;
- const endpoint = IntegrationTypeEndpointMap.get(integration.type);
- if (!endpoint) {
- return {
- type: integration.type,
- items: [],
- success: false,
- };
- }
-
- // Get the origin URL
- let { href: origin } = new URL(app.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
-
- const apiKey = integration.properties.find((x) => x.field === 'apiKey')?.value;
- if (!apiKey) return { type: integration.type, items: [], success: false };
- return axios
- .get(
- `${origin}${endpoint}?apiKey=${apiKey}&end=${end.toISOString()}&start=${start.toISOString()}&includeSeries=true&includeEpisodeFile=true&includeEpisodeImages=true`
- )
- .then((x) => ({ type: integration.type, items: x.data as any[], success: true }))
- .catch((err) => {
- Consola.error(
- `failed to process request to app '${integration.type}' (${app.id}): ${err}`
- );
- return {
- type: integration.type,
- items: [],
- success: false,
- };
- });
- })
- );
-
- const results = await Promise.all(medias);
- const countFailed = results.filter((x) => !x.success).length;
- if (countFailed > 0) {
- Consola.warn(`A total of ${countFailed} apps for the calendar widget failed`);
- }
-
- 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),
- });
- } catch (error) {
- Consola.error(`Error while requesting media from your app. Check your configuration. ${error}`);
-
- return res.status(500).json({
- tvShows: [],
- movies: [],
- books: [],
- musics: [],
- totalCount: 0,
- });
- }
-}
diff --git a/src/pages/api/modules/dashdot/info.ts b/src/pages/api/modules/dashdot/info.ts
deleted file mode 100644
index 76f93ad9883..00000000000
--- a/src/pages/api/modules/dashdot/info.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import axios from 'axios';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { z } from 'zod';
-
-import { getConfig } from '../../../../tools/config/getConfig';
-import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile';
-
-const getQuerySchema = z.object({
- configName: z.string(),
- widgetId: z.string().uuid(),
-});
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- const parseResult = getQuerySchema.safeParse(req.query);
-
- if (!parseResult.success) {
- return res.status(400).json({
- statusCode: 400,
- message: 'Invalid query parameters, please specify the widgetId and configName',
- });
- }
-
- const { configName, widgetId } = parseResult.data;
-
- const config = getConfig(configName);
-
- const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId);
-
- if (!dashDotWidget) {
- return res.status(400).json({
- message: 'There is no dashdot widget defined',
- });
- }
-
- const dashDotUrl = (dashDotWidget as IDashDotTile).properties.url;
-
- if (!dashDotUrl) {
- return res.status(400).json({
- message: 'Dashdot url must be defined in config',
- });
- }
-
- // Get the origin URL
- const url = dashDotUrl.endsWith('/')
- ? dashDotUrl.substring(0, dashDotUrl.length - 1)
- : dashDotUrl;
- const response = await axios.get(`${url}/info`);
-
- // Return the response
- return res.status(200).json(response.data);
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/dashdot/storage.ts b/src/pages/api/modules/dashdot/storage.ts
deleted file mode 100644
index feefd58c5d3..00000000000
--- a/src/pages/api/modules/dashdot/storage.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import axios from 'axios';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { z } from 'zod';
-
-import { getConfig } from '../../../../tools/config/getConfig';
-import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile';
-
-const getQuerySchema = z.object({
- configName: z.string(),
- widgetId: z.string().uuid(),
-});
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- const parseResult = getQuerySchema.safeParse(req.query);
-
- if (!parseResult.success) {
- return res.status(400).json({
- statusCode: 400,
- message: 'Invalid query parameters, please specify the widgetId and configName',
- });
- }
-
- const { configName, widgetId } = parseResult.data;
-
- const config = getConfig(configName);
- const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId);
-
- if (!dashDotWidget) {
- return res.status(400).json({
- message: 'There is no dashdot widget defined',
- });
- }
-
- const dashDotUrl = (dashDotWidget as IDashDotTile).properties.url;
-
- if (!dashDotUrl) {
- return res.status(400).json({
- message: 'Dashdot url must be defined in config',
- });
- }
-
- // Get the origin URL
- const url = dashDotUrl.endsWith('/')
- ? dashDotUrl.substring(0, dashDotUrl.length - 1)
- : dashDotUrl;
- const response = await axios.get(`${url}/load/storage`);
- // Return the response
- return res.status(200).json(response.data);
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/dns-hole/control.ts b/src/pages/api/modules/dns-hole/control.ts
deleted file mode 100644
index 7cfd625071c..00000000000
--- a/src/pages/api/modules/dns-hole/control.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/* eslint-disable no-await-in-loop */
-import { getCookie } from 'cookies-next';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { z } from 'zod';
-
-import { findAppProperty } from '../../../../tools/client/app-properties';
-import { getConfig } from '../../../../tools/config/getConfig';
-import { AdGuard } from '../../../../tools/server/sdk/adGuard/adGuard';
-import { PiHoleClient } from '../../../../tools/server/sdk/pihole/piHole';
-import { ConfigAppType } from '../../../../types/app';
-
-const getQuerySchema = z.object({
- action: z.enum(['enable', 'disable']),
-});
-
-export const Post = async (request: NextApiRequest, response: NextApiResponse) => {
- const configName = getCookie('config-name', { req: request });
- const config = getConfig(configName?.toString() ?? 'default');
-
- const parseResult = getQuerySchema.safeParse(request.query);
-
- if (!parseResult.success) {
- response.status(400).json({ message: 'invalid query parameters, please specify the status' });
- return;
- }
-
- const applicableApps = config.apps.filter(
- (x) => x.integration?.type && ['pihole', 'adGuardHome'].includes(x.integration?.type)
- );
-
- for (let i = 0; i < applicableApps.length; i += 1) {
- const app = applicableApps[i];
-
- if (app.integration?.type === 'pihole') {
- await processPiHole(app, parseResult.data.action === 'enable');
- return;
- }
-
- await processAdGuard(app, parseResult.data.action === 'disable');
- }
-
- response.status(200).json({});
-};
-
-const processAdGuard = async (app: ConfigAppType, enable: boolean) => {
- const adGuard = new AdGuard(
- app.url,
- findAppProperty(app, 'username'),
- findAppProperty(app, 'password')
- );
-
- if (enable) {
- await adGuard.disable();
- return;
- }
-
- await adGuard.enable();
-};
-
-const processPiHole = async (app: ConfigAppType, enable: boolean) => {
- const pihole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey'));
-
- if (enable) {
- await pihole.enable();
- return;
- }
-
- await pihole.disable();
-};
-
-export default async (request: NextApiRequest, response: NextApiResponse) => {
- if (request.method === 'POST') {
- return Post(request, response);
- }
-
- return response.status(405).json({});
-};
diff --git a/src/pages/api/modules/dns-hole/summary.spec.ts b/src/pages/api/modules/dns-hole/summary.spec.ts
deleted file mode 100644
index cccd09460ec..00000000000
--- a/src/pages/api/modules/dns-hole/summary.spec.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-import Consola from 'consola';
-import { createMocks } from 'node-mocks-http';
-import { describe, expect, it, vi } from 'vitest';
-
-import { ConfigType } from '../../../../types/config';
-import GetSummary from './summary';
-
-const mockedGetConfig = vi.fn();
-
-describe('DNS hole', () => {
- it('combine and return aggregated data', async () => {
- // arrange
- const { req, res } = createMocks({
- method: 'GET',
- });
-
- vi.mock('./../../../../tools/config/getConfig.ts', () => ({
- get getConfig() {
- return mockedGetConfig;
- },
- }));
-
- mockedGetConfig.mockReturnValue({
- apps: [
- {
- url: 'http://pi.hole',
- integration: {
- type: 'pihole',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: 'hf3829fj238g8',
- },
- ],
- },
- },
- ],
- } as ConfigType);
- const errorLogSpy = vi.spyOn(Consola, 'error');
- const warningLogSpy = vi.spyOn(Consola, 'warn');
-
- fetchMock.mockResponse((request) => {
- if (request.url === 'http://pi.hole/admin/api.php?summaryRaw&auth=hf3829fj238g8') {
- return JSON.stringify({
- domains_being_blocked: 780348,
- dns_queries_today: 36910,
- ads_blocked_today: 9700,
- ads_percentage_today: 26.280142,
- unique_domains: 6217,
- queries_forwarded: 12943,
- queries_cached: 13573,
- clients_ever_seen: 20,
- unique_clients: 17,
- dns_queries_all_types: 36910,
- reply_UNKNOWN: 947,
- reply_NODATA: 3313,
- reply_NXDOMAIN: 1244,
- reply_CNAME: 5265,
- reply_IP: 25635,
- reply_DOMAIN: 97,
- reply_RRNAME: 4,
- reply_SERVFAIL: 28,
- reply_REFUSED: 0,
- reply_NOTIMP: 0,
- reply_OTHER: 0,
- reply_DNSSEC: 0,
- reply_NONE: 0,
- reply_BLOB: 377,
- dns_queries_all_replies: 36910,
- privacy_level: 0,
- status: 'enabled',
- gravity_last_updated: {
- file_exists: true,
- absolute: 1682216493,
- relative: {
- days: 5,
- hours: 17,
- minutes: 52,
- },
- },
- });
- }
-
- return Promise.reject(new Error(`Bad url: ${request.url}`));
- });
-
- // Act
- await GetSummary(req, res);
-
- // Assert
- expect(res._getStatusCode()).toBe(200);
- expect(res.finished).toBe(true);
- expect(JSON.parse(res._getData())).toEqual({
- adsBlockedToday: 9700,
- adsBlockedTodayPercentage: 0.26280140883229475,
- dnsQueriesToday: 36910,
- domainsBeingBlocked: 780348,
- status: [
- {
- status: 'enabled',
- },
- ],
- });
-
- expect(errorLogSpy).not.toHaveBeenCalled();
- expect(warningLogSpy).not.toHaveBeenCalled();
-
- errorLogSpy.mockRestore();
- });
-
- it('combine and return aggregated data when multiple instances', async () => {
- // arrange
- const { req, res } = createMocks({
- method: 'GET',
- });
-
- vi.mock('./../../../../tools/config/getConfig.ts', () => ({
- get getConfig() {
- return mockedGetConfig;
- },
- }));
-
- mockedGetConfig.mockReturnValue({
- apps: [
- {
- id: 'app1',
- url: 'http://pi.hole',
- integration: {
- type: 'pihole',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: 'hf3829fj238g8',
- },
- ],
- },
- },
- {
- id: 'app2',
- url: 'http://pi2.hole',
- integration: {
- type: 'pihole',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: 'ayaka',
- },
- ],
- },
- },
- ],
- } as ConfigType);
- const errorLogSpy = vi.spyOn(Consola, 'error');
- const warningLogSpy = vi.spyOn(Consola, 'warn');
-
- fetchMock.mockResponse((request) => {
- if (request.url === 'http://pi.hole/admin/api.php?summaryRaw&auth=hf3829fj238g8') {
- return JSON.stringify({
- domains_being_blocked: 3,
- dns_queries_today: 8,
- ads_blocked_today: 5,
- ads_percentage_today: 26,
- unique_domains: 4,
- queries_forwarded: 2,
- queries_cached: 2,
- clients_ever_seen: 2,
- unique_clients: 3,
- dns_queries_all_types: 3,
- reply_UNKNOWN: 2,
- reply_NODATA: 3,
- reply_NXDOMAIN: 5,
- reply_CNAME: 6,
- reply_IP: 5,
- reply_DOMAIN: 3,
- reply_RRNAME: 2,
- reply_SERVFAIL: 2,
- reply_REFUSED: 0,
- reply_NOTIMP: 0,
- reply_OTHER: 0,
- reply_DNSSEC: 0,
- reply_NONE: 0,
- reply_BLOB: 1,
- dns_queries_all_replies: 36910,
- privacy_level: 0,
- status: 'enabled',
- gravity_last_updated: {
- file_exists: true,
- absolute: 1682216493,
- relative: {
- days: 5,
- hours: 17,
- minutes: 52,
- },
- },
- });
- }
-
- if (request.url === 'http://pi2.hole/admin/api.php?summaryRaw&auth=ayaka') {
- return JSON.stringify({
- domains_being_blocked: 1,
- dns_queries_today: 3,
- ads_blocked_today: 2,
- ads_percentage_today: 47,
- unique_domains: 4,
- queries_forwarded: 4,
- queries_cached: 2,
- clients_ever_seen: 2,
- unique_clients: 2,
- dns_queries_all_types: 1,
- reply_UNKNOWN: 3,
- reply_NODATA: 2,
- reply_NXDOMAIN: 1,
- reply_CNAME: 3,
- reply_IP: 2,
- reply_DOMAIN: 97,
- reply_RRNAME: 4,
- reply_SERVFAIL: 28,
- reply_REFUSED: 0,
- reply_NOTIMP: 0,
- reply_OTHER: 0,
- reply_DNSSEC: 0,
- reply_NONE: 0,
- reply_BLOB: 2,
- dns_queries_all_replies: 4,
- privacy_level: 0,
- status: 'disabled',
- gravity_last_updated: {
- file_exists: true,
- absolute: 1682216493,
- relative: {
- days: 5,
- hours: 17,
- minutes: 52,
- },
- },
- });
- }
-
- return Promise.reject(new Error(`Bad url: ${request.url}`));
- });
-
- // Act
- await GetSummary(req, res);
-
- // Assert
- expect(res._getStatusCode()).toBe(200);
- expect(res.finished).toBe(true);
- expect(JSON.parse(res._getData())).toStrictEqual({
- adsBlockedToday: 7,
- adsBlockedTodayPercentage: 0.6363636363636364,
- dnsQueriesToday: 11,
- domainsBeingBlocked: 4,
- status: [
- {
- appId: 'app1',
- status: 'enabled',
- },
- {
- appId: 'app2',
- status: 'disabled',
- },
- ],
- });
-
- expect(errorLogSpy).not.toHaveBeenCalled();
- expect(warningLogSpy).not.toHaveBeenCalled();
-
- errorLogSpy.mockRestore();
- });
-});
diff --git a/src/pages/api/modules/dns-hole/summary.ts b/src/pages/api/modules/dns-hole/summary.ts
deleted file mode 100644
index 67ea33f1c7c..00000000000
--- a/src/pages/api/modules/dns-hole/summary.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-/* eslint-disable no-await-in-loop */
-import Consola from 'consola';
-import { getCookie } from 'cookies-next';
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import { findAppProperty } from '../../../../tools/client/app-properties';
-import { getConfig } from '../../../../tools/config/getConfig';
-import { AdGuard } from '../../../../tools/server/sdk/adGuard/adGuard';
-import { PiHoleClient } from '../../../../tools/server/sdk/pihole/piHole';
-import { AdStatistics } from '../../../../widgets/dnshole/type';
-
-export const Get = async (request: NextApiRequest, response: NextApiResponse) => {
- const configName = getCookie('config-name', { req: request });
- const config = getConfig(configName?.toString() ?? 'default');
-
- const applicableApps = config.apps.filter(
- (x) => x.integration?.type && ['pihole', 'adGuardHome'].includes(x.integration?.type)
- );
-
- const data: AdStatistics = {
- domainsBeingBlocked: 0,
- adsBlockedToday: 0,
- adsBlockedTodayPercentage: 0,
- dnsQueriesToday: 0,
- status: [],
- };
-
- const adsBlockedTodayPercentageArr: number[] = [];
-
- for (let i = 0; i < applicableApps.length; i += 1) {
- const app = applicableApps[i];
-
- try {
- switch (app.integration?.type) {
- case 'pihole': {
- const piHole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey'));
- const summary = await piHole.getSummary();
-
- data.domainsBeingBlocked += summary.domains_being_blocked;
- data.adsBlockedToday += summary.ads_blocked_today;
- data.dnsQueriesToday += summary.dns_queries_today;
- data.status.push({
- status: summary.status,
- appId: app.id,
- });
- adsBlockedTodayPercentageArr.push(summary.ads_percentage_today);
- break;
- }
- case 'adGuardHome': {
- const adGuard = new AdGuard(
- app.url,
- findAppProperty(app, 'username'),
- findAppProperty(app, 'password')
- );
-
- const stats = await adGuard.getStats();
- const status = await adGuard.getStatus();
- const countFilteredDomains = await adGuard.getCountFilteringDomains();
-
- const blockedQueriesToday = stats.blocked_filtering.reduce((prev, sum) => prev + sum, 0);
- const queriesToday = stats.dns_queries.reduce((prev, sum) => prev + sum, 0);
- data.adsBlockedToday = blockedQueriesToday;
- data.domainsBeingBlocked += countFilteredDomains;
- data.dnsQueriesToday += queriesToday;
- data.status.push({
- status: status.protection_enabled ? 'enabled' : 'disabled',
- appId: app.id,
- });
- adsBlockedTodayPercentageArr.push((queriesToday / blockedQueriesToday) * 100);
- break;
- }
- default: {
- Consola.error(`Integration communication for app ${app.id} failed: unknown type`);
- break;
- }
- }
- } catch (err) {
- Consola.error(`Failed to communicate with DNS hole at ${app.url}: ${err}`);
- }
- }
-
- data.adsBlockedTodayPercentage = data.adsBlockedToday / data.dnsQueriesToday;
- if (Number.isNaN(data.adsBlockedTodayPercentage)) {
- data.adsBlockedTodayPercentage = 0;
- }
- return response.status(200).json(data);
-};
-
-export default async (request: NextApiRequest, response: NextApiResponse) => {
- if (request.method === 'GET') {
- return Get(request, response);
- }
-
- return response.status(405);
-};
diff --git a/src/pages/api/modules/downloads/index.ts b/src/pages/api/modules/downloads/index.ts
deleted file mode 100644
index 107524a9104..00000000000
--- a/src/pages/api/modules/downloads/index.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import { Deluge } from '@ctrl/deluge';
-import { QBittorrent } from '@ctrl/qbittorrent';
-import { AllClientData } from '@ctrl/shared-torrent';
-import { Transmission } from '@ctrl/transmission';
-import Consola from 'consola';
-import { getCookie } from 'cookies-next';
-import dayjs from 'dayjs';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { Client } from 'sabnzbd-api';
-import { findAppProperty } from '~/tools/client/app-properties';
-
-import { NzbgetClient } from '../../../../server/api/routers/usenet/nzbget/nzbget-client';
-import { NzbgetQueueItem, NzbgetStatus } from '../../../../server/api/routers/usenet/nzbget/types';
-import { getConfig } from '../../../../tools/config/getConfig';
-import {
- NormalizedDownloadAppStat,
- NormalizedDownloadQueueResponse,
-} from '../../../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
-import { ConfigAppType, IntegrationField } from '../../../../types/app';
-import { UsenetQueueItem } from '../../../../widgets/useNet/types';
-
-const Get = async (request: NextApiRequest, response: NextApiResponse) => {
- const configName = getCookie('config-name', { req: request });
- const config = getConfig(configName?.toString() ?? 'default');
-
- const failedClients: string[] = [];
-
- const clientData: Promise[] = config.apps.map(async (app) => {
- try {
- const response = await GetDataFromClient(app);
-
- if (!response) {
- return {
- success: false,
- } as NormalizedDownloadAppStat;
- }
-
- return response;
- } catch (err: any) {
- Consola.error(
- `Error communicating with your download client '${app.name}' (${app.id}): ${err}`
- );
- failedClients.push(app.id);
- return {
- success: false,
- } as NormalizedDownloadAppStat;
- }
- });
-
- const settledPromises = await Promise.allSettled(clientData);
-
- const data: NormalizedDownloadAppStat[] = settledPromises
- .filter((x) => x.status === 'fulfilled')
- .map((promise) => (promise as PromiseFulfilledResult).value)
- .filter((x) => x !== undefined && x.type !== undefined);
-
- const responseBody = { apps: data, failedApps: failedClients } as NormalizedDownloadQueueResponse;
-
- if (failedClients.length > 0) {
- Consola.warn(
- `${failedClients.length} download clients failed. Please check your configuration and the above log`
- );
- }
-
- return response.status(200).json(responseBody);
-};
-
-const GetDataFromClient = async (
- app: ConfigAppType
-): Promise => {
- const reduceTorrent = (data: AllClientData): NormalizedDownloadAppStat => ({
- type: 'torrent',
- appId: app.id,
- success: true,
- torrents: data.torrents,
- totalDownload: data.torrents
- .map((torrent) => torrent.downloadSpeed)
- .reduce((acc, torrent) => acc + torrent, 0),
- totalUpload: data.torrents
- .map((torrent) => torrent.uploadSpeed)
- .reduce((acc, torrent) => acc + torrent, 0),
- });
-
- const findField = (app: ConfigAppType, field: IntegrationField) =>
- app.integration?.properties.find((x) => x.field === field)?.value ?? undefined;
-
- switch (app.integration?.type) {
- case 'deluge': {
- return reduceTorrent(
- await new Deluge({
- baseUrl: app.url,
- password: findField(app, 'password'),
- }).getAllData()
- );
- }
- case 'transmission': {
- return reduceTorrent(
- await new Transmission({
- baseUrl: app.url,
- username: findField(app, 'username'),
- password: findField(app, 'password'),
- }).getAllData()
- );
- }
- case 'qBittorrent': {
- return reduceTorrent(
- await new QBittorrent({
- baseUrl: app.url,
- username: findField(app, 'username'),
- password: findField(app, 'password'),
- }).getAllData()
- );
- }
- case 'sabnzbd': {
- const { origin } = new URL(app.url);
- const client = new Client(origin, findField(app, 'apiKey') ?? '');
- const queue = await client.queue();
- const items: UsenetQueueItem[] = queue.slots.map((slot) => {
- const [hours, minutes, seconds] = slot.timeleft.split(':');
- const eta = dayjs.duration({
- hour: parseInt(hours, 10),
- minutes: parseInt(minutes, 10),
- seconds: parseInt(seconds, 10),
- } as any);
-
- return {
- id: slot.nzo_id,
- eta: eta.asSeconds(),
- name: slot.filename,
- progress: parseFloat(slot.percentage),
- size: parseFloat(slot.mb) * 1000 * 1000,
- state: slot.status.toLowerCase() as any,
- };
- });
- const killobitsPerSecond = Number(queue.kbpersec);
- const bytesPerSecond = killobitsPerSecond * 1024; // convert killobytes to bytes
- return {
- type: 'usenet',
- appId: app.id,
- totalDownload: bytesPerSecond,
- nzbs: items,
- success: true,
- };
- }
- case 'nzbGet': {
- const url = new URL(app.url);
- const options = {
- host: url.hostname,
- port: url.port,
- login: findAppProperty(app, 'username'),
- hash: findAppProperty(app, 'password'),
- };
-
- const nzbGet = NzbgetClient(options);
- const nzbgetQueue: NzbgetQueueItem[] = await new Promise((resolve, reject) => {
- nzbGet.listGroups((err: any, result: NzbgetQueueItem[]) => {
- if (!err) {
- resolve(result);
- } else {
- Consola.error(`Error while listing groups: ${err}`);
- reject(err);
- }
- });
- });
- if (!nzbgetQueue) {
- throw new Error('Error while getting NZBGet queue');
- }
-
- const nzbgetStatus: NzbgetStatus = await new Promise((resolve, reject) => {
- nzbGet.status((err: any, result: NzbgetStatus) => {
- if (!err) {
- resolve(result);
- } else {
- Consola.error(`Error while retrieving NZBGet stats: ${err}`);
- reject(err);
- }
- });
- });
-
- if (!nzbgetStatus) {
- throw new Error('Error while getting NZBGet status');
- }
-
- const nzbgetItems: UsenetQueueItem[] = nzbgetQueue.map((item: NzbgetQueueItem) => ({
- id: item.NZBID.toString(),
- name: item.NZBName,
- progress: (item.DownloadedSizeMB / item.FileSizeMB) * 100,
- eta: (item.RemainingSizeMB * 1000000) / nzbgetStatus.DownloadRate,
- // Multiple MB to get bytes
- size: item.FileSizeMB * 1000 * 1000,
- state: getNzbgetState(item.Status),
- }));
-
- return {
- type: 'usenet',
- appId: app.id,
- nzbs: nzbgetItems,
- success: true,
- totalDownload: 0,
- };
- }
- default:
- return undefined;
- }
-};
-
-export default async (request: NextApiRequest, response: NextApiResponse) => {
- if (request.method === 'GET') {
- return Get(request, response);
- }
-
- return response.status(405);
-};
-
-function getNzbgetState(status: string) {
- switch (status) {
- case 'QUEUED':
- return 'queued';
- case 'PAUSED ':
- return 'paused';
- default:
- return 'downloading';
- }
-}
diff --git a/src/pages/api/modules/media-requests/index.spec.ts b/src/pages/api/modules/media-requests/index.spec.ts
deleted file mode 100644
index d8b79a73628..00000000000
--- a/src/pages/api/modules/media-requests/index.spec.ts
+++ /dev/null
@@ -1,534 +0,0 @@
-import Consola from 'consola';
-import { createMocks } from 'node-mocks-http';
-import { describe, expect, it, vi } from 'vitest';
-import 'vitest-fetch-mock';
-
-import { ConfigType } from '../../../../types/config';
-import MediaRequestsRoute from './index';
-
-const mockedGetConfig = vi.fn();
-
-describe('media-requests api', () => {
- it('reduce when empty list of requests', async () => {
- // Arrange
- const { req, res } = createMocks();
-
- vi.mock('./../../../../tools/config/getConfig.ts', () => ({
- get getConfig() {
- return mockedGetConfig;
- },
- }));
- mockedGetConfig.mockReturnValue({
- apps: [],
- });
-
- // Act
- await MediaRequestsRoute(req, res);
-
- // Assert
- expect(res._getStatusCode()).toBe(200);
- expect(res.finished).toBe(true);
- expect(JSON.parse(res._getData())).toEqual([]);
- });
-
- it('log error when fetch was not successful', async () => {
- // Arrange
- const { req, res } = createMocks();
-
- vi.mock('./../../../../tools/config/getConfig.ts', () => ({
- get getConfig() {
- return mockedGetConfig;
- },
- }));
- mockedGetConfig.mockReturnValue({
- apps: [
- {
- integration: {
- type: 'overseerr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: 'abc',
- },
- ],
- },
- },
- ],
- } as ConfigType);
- const logSpy = vi.spyOn(Consola, 'error');
-
- // Act
- await MediaRequestsRoute(req, res);
-
- // Assert
- expect(res._getStatusCode()).toBe(200);
- expect(res.finished).toBe(true);
- expect(JSON.parse(res._getData())).toEqual([]);
-
- expect(logSpy).toHaveBeenCalledOnce();
- expect(logSpy.mock.lastCall).toEqual([
- 'Failed to request data from Overseerr: FetchError: invalid json response body at reason: Unexpected end of JSON input',
- ]);
-
- logSpy.mockRestore();
- });
-
- it('fetch and return requests in response with external url', async () => {
- // Arrange
- const { req, res } = createMocks({
- method: 'GET',
- });
-
- vi.mock('./../../../../tools/config/getConfig.ts', () => ({
- get getConfig() {
- return mockedGetConfig;
- },
- }));
- mockedGetConfig.mockReturnValue({
- apps: [
- {
- url: 'http://my-overseerr.local',
- behaviour: {
- externalUrl: 'http://my-overseerr.external',
- },
- integration: {
- type: 'overseerr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: 'abc',
- },
- ],
- },
- },
- ],
- widgets: [
- {
- id: 'hjeruijgrig',
- type: 'media-requests-list',
- properties: {
- replaceLinksWithExternalHost: true,
- },
- },
- ],
- } as unknown as ConfigType);
- const logSpy = vi.spyOn(Consola, 'error');
-
- fetchMock.mockResponse((request) => {
- if (request.url === 'http://my-overseerr.local/api/v1/request?take=25&skip=0&sort=added') {
- return JSON.stringify({
- pageInfo: { pages: 3, pageSize: 20, results: 42, page: 1 },
- results: [
- {
- id: 44,
- status: 2,
- createdAt: '2023-04-06T19:38:45.000Z',
- updatedAt: '2023-04-06T19:38:45.000Z',
- type: 'movie',
- is4k: false,
- serverId: 0,
- profileId: 4,
- tags: [],
- isAutoRequest: false,
- media: {
- downloadStatus: [],
- downloadStatus4k: [],
- id: 999,
- mediaType: 'movie',
- tmdbId: 99999999,
- tvdbId: null,
- imdbId: null,
- status: 5,
- status4k: 1,
- createdAt: '2023-02-06T19:38:45.000Z',
- updatedAt: '2023-02-06T20:00:04.000Z',
- lastSeasonChange: '2023-08-06T19:38:45.000Z',
- mediaAddedAt: '2023-05-14T06:30:34.000Z',
- serviceId: 0,
- serviceId4k: null,
- externalServiceId: 32,
- externalServiceId4k: null,
- externalServiceSlug: '000000000000',
- externalServiceSlug4k: null,
- ratingKey: null,
- ratingKey4k: null,
- jellyfinMediaId: '0000',
- jellyfinMediaId4k: null,
- mediaUrl:
- 'http://your-jellyfin.local/web/index.html#!/details?id=mn8q2j4gq038g&context=home&serverId=jf83fj34gm340g',
- serviceUrl: 'http://your-jellyfin.local/movie/0000',
- },
- seasons: [],
- modifiedBy: {
- permissions: 2,
- warnings: [],
- id: 1,
- email: 'example-user@homarr.dev',
- plexUsername: null,
- jellyfinUsername: 'example-user',
- username: null,
- recoveryLinkExpirationDate: null,
- userType: 3,
- plexId: null,
- jellyfinUserId: '00000000000000000',
- jellyfinDeviceId: '111111111111111111',
- jellyfinAuthToken: '2222222222222222222',
- plexToken: null,
- avatar: '/os_logo_square.png',
- movieQuotaLimit: null,
- movieQuotaDays: null,
- tvQuotaLimit: null,
- tvQuotaDays: null,
- createdAt: '2022-07-03T19:53:08.000Z',
- updatedAt: '2022-07-03T19:53:08.000Z',
- requestCount: 34,
- displayName: 'Example User',
- },
- requestedBy: {
- permissions: 2,
- warnings: [],
- id: 1,
- email: 'example-user@homarr.dev',
- plexUsername: null,
- jellyfinUsername: 'example-user',
- username: null,
- recoveryLinkExpirationDate: null,
- userType: 3,
- plexId: null,
- jellyfinUserId: '00000000000000000',
- jellyfinDeviceId: '111111111111111111',
- jellyfinAuthToken: '2222222222222222222',
- plexToken: null,
- avatar: '/os_logo_square.png',
- movieQuotaLimit: null,
- movieQuotaDays: null,
- tvQuotaLimit: null,
- tvQuotaDays: null,
- createdAt: '2022-07-03T19:53:08.000Z',
- updatedAt: '2022-07-03T19:53:08.000Z',
- requestCount: 34,
- displayName: 'Example User',
- },
- seasonCount: 0,
- },
- ],
- });
- }
-
- if (request.url === 'http://my-overseerr.local/api/v1/movie/99999999') {
- return JSON.stringify({
- id: 0,
- adult: false,
- budget: 0,
- genres: [
- {
- id: 18,
- name: 'Dashboards',
- },
- ],
- relatedVideos: [],
- originalLanguage: 'jp',
- originalTitle: 'Homarrrr Movie',
- popularity: 9.352,
- productionCompanies: [],
- productionCountries: [],
- releaseDate: '2023-12-08',
- releases: {
- results: [],
- },
- revenue: 0,
- spokenLanguages: [
- {
- english_name: 'Japanese',
- iso_639_1: 'jp',
- name: '日本語',
- },
- ],
- status: 'Released',
- title: 'Homarr Movie',
- video: false,
- voteAverage: 9.999,
- voteCount: 0,
- backdropPath: '/mhjq8jr0qgrjnghnh.jpg',
- homepage: '',
- imdbId: 'tt0000000',
- overview: 'A very cool movie',
- posterPath: '/hf4j0928gq543njgh8935nqh8.jpg',
- runtime: 97,
- tagline: '',
- credits: {},
- collection: null,
- externalIds: {
- facebookId: null,
- imdbId: null,
- instagramId: null,
- twitterId: null,
- },
- watchProviders: [],
- keywords: [],
- });
- }
-
- return Promise.reject(new Error(`Bad url: ${request.url}`));
- });
-
- // Act
- await MediaRequestsRoute(req, res);
-
- // Assert
- expect(res._getStatusCode()).toBe(200);
- expect(res.finished).toBe(true);
- expect(JSON.parse(res._getData())).toEqual([
- {
- airDate: '2023-12-08',
- backdropPath: 'https://image.tmdb.org/t/p/original//mhjq8jr0qgrjnghnh.jpg',
- createdAt: '2023-04-06T19:38:45.000Z',
- href: 'http://my-overseerr.external/movie/99999999',
- id: 44,
- name: 'Homarrrr Movie',
- posterPath:
- 'https://image.tmdb.org/t/p/w600_and_h900_bestv2//hf4j0928gq543njgh8935nqh8.jpg',
- status: 2,
- type: 'movie',
- userLink: 'http://my-overseerr.external/users/1',
- userName: 'Example User',
- userProfilePicture: 'http://my-overseerr.external//os_logo_square.png',
- },
- ]);
-
- expect(logSpy).not.toHaveBeenCalled();
-
- logSpy.mockRestore();
- });
-
- it('fetch and return requests in response with internal url', async () => {
- // Arrange
- const { req, res } = createMocks({
- method: 'GET',
- });
-
- vi.mock('./../../../../tools/config/getConfig.ts', () => ({
- get getConfig() {
- return mockedGetConfig;
- },
- }));
- mockedGetConfig.mockReturnValue({
- apps: [
- {
- url: 'http://my-overseerr.local',
- behaviour: {
- externalUrl: 'http://my-overseerr.external',
- },
- integration: {
- type: 'overseerr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: 'abc',
- },
- ],
- },
- },
- ],
- widgets: [
- {
- id: 'hjeruijgrig',
- type: 'media-requests-list',
- properties: {
- replaceLinksWithExternalHost: false,
- },
- },
- ],
- } as unknown as ConfigType);
- const logSpy = vi.spyOn(Consola, 'error');
-
- fetchMock.mockResponse((request) => {
- if (request.url === 'http://my-overseerr.local/api/v1/request?take=25&skip=0&sort=added') {
- return JSON.stringify({
- pageInfo: { pages: 3, pageSize: 20, results: 42, page: 1 },
- results: [
- {
- id: 44,
- status: 2,
- createdAt: '2023-04-06T19:38:45.000Z',
- updatedAt: '2023-04-06T19:38:45.000Z',
- type: 'movie',
- is4k: false,
- serverId: 0,
- profileId: 4,
- tags: [],
- isAutoRequest: false,
- media: {
- downloadStatus: [],
- downloadStatus4k: [],
- id: 999,
- mediaType: 'movie',
- tmdbId: 99999999,
- tvdbId: null,
- imdbId: null,
- status: 5,
- status4k: 1,
- createdAt: '2023-02-06T19:38:45.000Z',
- updatedAt: '2023-02-06T20:00:04.000Z',
- lastSeasonChange: '2023-08-06T19:38:45.000Z',
- mediaAddedAt: '2023-05-14T06:30:34.000Z',
- serviceId: 0,
- serviceId4k: null,
- externalServiceId: 32,
- externalServiceId4k: null,
- externalServiceSlug: '000000000000',
- externalServiceSlug4k: null,
- ratingKey: null,
- ratingKey4k: null,
- jellyfinMediaId: '0000',
- jellyfinMediaId4k: null,
- mediaUrl:
- 'http://your-jellyfin.local/web/index.html#!/details?id=mn8q2j4gq038g&context=home&serverId=jf83fj34gm340g',
- serviceUrl: 'http://your-jellyfin.local/movie/0000',
- },
- seasons: [],
- modifiedBy: {
- permissions: 2,
- warnings: [],
- id: 1,
- email: 'example-user@homarr.dev',
- plexUsername: null,
- jellyfinUsername: 'example-user',
- username: null,
- recoveryLinkExpirationDate: null,
- userType: 3,
- plexId: null,
- jellyfinUserId: '00000000000000000',
- jellyfinDeviceId: '111111111111111111',
- jellyfinAuthToken: '2222222222222222222',
- plexToken: null,
- avatar: '/os_logo_square.png',
- movieQuotaLimit: null,
- movieQuotaDays: null,
- tvQuotaLimit: null,
- tvQuotaDays: null,
- createdAt: '2022-07-03T19:53:08.000Z',
- updatedAt: '2022-07-03T19:53:08.000Z',
- requestCount: 34,
- displayName: 'Example User',
- },
- requestedBy: {
- permissions: 2,
- warnings: [],
- id: 1,
- email: 'example-user@homarr.dev',
- plexUsername: null,
- jellyfinUsername: 'example-user',
- username: null,
- recoveryLinkExpirationDate: null,
- userType: 3,
- plexId: null,
- jellyfinUserId: '00000000000000000',
- jellyfinDeviceId: '111111111111111111',
- jellyfinAuthToken: '2222222222222222222',
- plexToken: null,
- avatar: '/os_logo_square.png',
- movieQuotaLimit: null,
- movieQuotaDays: null,
- tvQuotaLimit: null,
- tvQuotaDays: null,
- createdAt: '2022-07-03T19:53:08.000Z',
- updatedAt: '2022-07-03T19:53:08.000Z',
- requestCount: 34,
- displayName: 'Example User',
- },
- seasonCount: 0,
- },
- ],
- });
- }
-
- if (request.url === 'http://my-overseerr.local/api/v1/movie/99999999') {
- return JSON.stringify({
- id: 0,
- adult: false,
- budget: 0,
- genres: [
- {
- id: 18,
- name: 'Dashboards',
- },
- ],
- relatedVideos: [],
- originalLanguage: 'jp',
- originalTitle: 'Homarrrr Movie',
- popularity: 9.352,
- productionCompanies: [],
- productionCountries: [],
- releaseDate: '2023-12-08',
- releases: {
- results: [],
- },
- revenue: 0,
- spokenLanguages: [
- {
- english_name: 'Japanese',
- iso_639_1: 'jp',
- name: '日本語',
- },
- ],
- status: 'Released',
- title: 'Homarr Movie',
- video: false,
- voteAverage: 9.999,
- voteCount: 0,
- backdropPath: '/mhjq8jr0qgrjnghnh.jpg',
- homepage: '',
- imdbId: 'tt0000000',
- overview: 'A very cool movie',
- posterPath: '/hf4j0928gq543njgh8935nqh8.jpg',
- runtime: 97,
- tagline: '',
- credits: {},
- collection: null,
- externalIds: {
- facebookId: null,
- imdbId: null,
- instagramId: null,
- twitterId: null,
- },
- watchProviders: [],
- keywords: [],
- });
- }
-
- return Promise.reject(new Error(`Bad url: ${request.url}`));
- });
-
- // Act
- await MediaRequestsRoute(req, res);
-
- // Assert
- expect(res._getStatusCode()).toBe(200);
- expect(res.finished).toBe(true);
- expect(JSON.parse(res._getData())).toEqual([
- {
- airDate: '2023-12-08',
- backdropPath: 'https://image.tmdb.org/t/p/original//mhjq8jr0qgrjnghnh.jpg',
- createdAt: '2023-04-06T19:38:45.000Z',
- href: 'http://my-overseerr.local/movie/99999999',
- id: 44,
- name: 'Homarrrr Movie',
- posterPath:
- 'https://image.tmdb.org/t/p/w600_and_h900_bestv2//hf4j0928gq543njgh8935nqh8.jpg',
- status: 2,
- type: 'movie',
- userLink: 'http://my-overseerr.local/users/1',
- userName: 'Example User',
- userProfilePicture: 'http://my-overseerr.local//os_logo_square.png',
- },
- ]);
-
- expect(logSpy).not.toHaveBeenCalled();
-
- logSpy.mockRestore();
- });
-});
diff --git a/src/pages/api/modules/media-requests/index.ts b/src/pages/api/modules/media-requests/index.ts
deleted file mode 100644
index ff39ba513b0..00000000000
--- a/src/pages/api/modules/media-requests/index.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import Consola from 'consola';
-import { getCookie } from 'cookies-next';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { checkIntegrationsType } from '~/tools/client/app-properties';
-
-import { getConfig } from '../../../../tools/config/getConfig';
-import { MediaRequestListWidget } from '../../../../widgets/media-requests/MediaRequestListTile';
-import { MediaRequest } from '../../../../widgets/media-requests/media-request-types';
-
-const Get = async (request: NextApiRequest, response: NextApiResponse) => {
- const configName = getCookie('config-name', { req: request });
- const config = getConfig(configName?.toString() ?? 'default');
-
- const apps = config.apps.filter((app) =>
- checkIntegrationsType(app.integration, ['overseerr', 'jellyseerr'])
- );
-
- Consola.log(`Retrieving media requests from ${apps.length} apps`);
-
- const promises = apps.map((app): Promise => {
- const apiKey = app.integration?.properties.find((prop) => prop.field === 'apiKey')?.value ?? '';
- const headers: HeadersInit = { 'X-Api-Key': apiKey };
- return fetch(`${app.url}/api/v1/request?take=25&skip=0&sort=added`, {
- headers,
- })
- .then(async (response) => {
- const body = (await response.json()) as OverseerrResponse;
- const mediaWidget = config.widgets.find((x) => x.type === 'media-requests-list') as
- | MediaRequestListWidget
- | undefined;
- if (!mediaWidget) {
- Consola.log('No media-requests-list found');
- return Promise.resolve([]);
- }
- const appUrl = mediaWidget.properties.replaceLinksWithExternalHost
- ? app.behaviour.externalUrl
- : app.url;
-
- const requests = await Promise.all(
- body.results.map(async (item): Promise => {
- const genericItem = await retrieveDetailsForItem(
- app.url,
- item.type,
- headers,
- item.media.tmdbId
- );
- return {
- appId: app.id,
- createdAt: item.createdAt,
- id: item.id,
- rootFolder: item.rootFolder,
- type: item.type,
- name: genericItem.name,
- userName: item.requestedBy.displayName,
- userProfilePicture: constructAvatarUrl(appUrl, item),
- userLink: `${appUrl}/users/${item.requestedBy.id}`,
- airDate: genericItem.airDate,
- status: item.status,
- backdropPath: `https://image.tmdb.org/t/p/original/${genericItem.backdropPath}`,
- posterPath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${genericItem.posterPath}`,
- href: `${appUrl}/movie/${item.media.tmdbId}`,
- };
- })
- );
-
- return Promise.resolve(requests);
- })
- .catch((err) => {
- Consola.error(`Failed to request data from Overseerr: ${err}`);
- return Promise.resolve([]);
- });
- });
-
- const mediaRequests = (await Promise.all(promises)).reduce((prev, cur) => prev.concat(cur), []);
-
- return response.status(200).json(mediaRequests);
-};
-
-const constructAvatarUrl = (appUrl: string, item: OverseerrResponseItem) => {
- const isAbsolute =
- item.requestedBy.avatar.startsWith('http://') || item.requestedBy.avatar.startsWith('https://');
-
- if (isAbsolute) {
- return item.requestedBy.avatar;
- }
-
- return `${appUrl}/${item.requestedBy.avatar}`;
-};
-
-const retrieveDetailsForItem = async (
- baseUrl: string,
- type: OverseerrResponseItem['type'],
- headers: HeadersInit,
- id: number
-): Promise => {
- if (type === 'tv') {
- const tvResponse = await fetch(`${baseUrl}/api/v1/tv/${id}`, {
- headers,
- });
-
- const series = (await tvResponse.json()) as OverseerrSeries;
-
- return {
- name: series.name,
- airDate: series.firstAirDate,
- backdropPath: series.backdropPath,
- posterPath: series.backdropPath,
- };
- }
-
- const movieResponse = await fetch(`${baseUrl}/api/v1/movie/${id}`, {
- headers,
- });
-
- const movie = (await movieResponse.json()) as OverseerrMovie;
-
- return {
- name: movie.originalTitle,
- airDate: movie.releaseDate,
- backdropPath: movie.backdropPath,
- posterPath: movie.posterPath,
- };
-};
-
-type GenericOverseerrItem = {
- name: string;
- airDate: string;
- backdropPath: string;
- posterPath: string;
-};
-
-type OverseerrMovie = {
- originalTitle: string;
- releaseDate: string;
- backdropPath: string;
- posterPath: string;
-};
-
-type OverseerrSeries = {
- name: string;
- firstAirDate: string;
- backdropPath: string;
- posterPath: string;
-};
-
-type OverseerrResponse = {
- results: OverseerrResponseItem[];
-};
-
-type OverseerrResponseItem = {
- id: number;
- status: number;
- createdAt: string;
- type: 'movie' | 'tv';
- rootFolder: string;
- requestedBy: OverseerrResponseItemUser;
- media: OverseerrResponseItemMedia;
-};
-
-type OverseerrResponseItemMedia = {
- tmdbId: number;
-};
-
-type OverseerrResponseItemUser = {
- id: number;
- displayName: string;
- avatar: string;
-};
-
-export default async (request: NextApiRequest, response: NextApiResponse) => {
- if (request.method === 'GET') {
- return Get(request, response);
- }
-
- return response.status(405);
-};
diff --git a/src/pages/api/modules/media-server/index.ts b/src/pages/api/modules/media-server/index.ts
deleted file mode 100644
index 8dcc104234d..00000000000
--- a/src/pages/api/modules/media-server/index.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-import { Jellyfin } from '@jellyfin/sdk';
-import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models';
-import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
-import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
-import Consola from 'consola';
-import { getCookie } from 'cookies-next';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
-
-import { getConfig } from '../../../../tools/config/getConfig';
-import { PlexClient } from '../../../../tools/server/sdk/plex/plexClient';
-import { GenericMediaServer } from '../../../../types/api/media-server/media-server';
-import { MediaServersResponseType } from '../../../../types/api/media-server/response';
-import {
- GenericCurrentlyPlaying,
- GenericSessionInfo,
-} from '../../../../types/api/media-server/session-info';
-import { ConfigAppType } from '../../../../types/app';
-
-const jellyfin = new Jellyfin({
- clientInfo: {
- name: 'Homarr',
- version: '0.0.1',
- },
- deviceInfo: {
- name: 'Homarr Jellyfin Widget',
- id: 'homarr-jellyfin-widget',
- },
-});
-
-const Get = async (request: NextApiRequest, response: NextApiResponse) => {
- const configName = getCookie('config-name', { req: request });
- const config = getConfig(configName?.toString() ?? 'default');
-
- const apps = config.apps.filter((app) =>
- checkIntegrationsType(app.integration, ['jellyfin', 'plex'])
- );
-
- const servers = await Promise.all(
- apps.map(async (app): Promise => {
- try {
- return await handleServer(app);
- } catch (error) {
- Consola.error(
- `failed to communicate with media server '${app.name}' (${app.id}): ${error}`
- );
- return {
- serverAddress: app.url,
- sessions: [],
- success: false,
- version: undefined,
- type: undefined,
- appId: app.id,
- };
- }
- })
- );
-
- return response.status(200).json({
- servers: servers.filter((server) => server !== undefined),
- } as MediaServersResponseType);
-};
-
-const handleServer = async (app: ConfigAppType): Promise => {
- switch (app.integration?.type) {
- case 'jellyfin': {
- const username = findAppProperty(app, 'username');
-
- if (!username) {
- return {
- appId: app.id,
- serverAddress: app.url,
- sessions: [],
- type: 'jellyfin',
- version: undefined,
- success: false,
- };
- }
-
- const password = findAppProperty(app, 'password');
-
- if (!password) {
- return {
- appId: app.id,
- serverAddress: app.url,
- sessions: [],
- type: 'jellyfin',
- version: undefined,
- success: false,
- };
- }
-
- const api = jellyfin.createApi(app.url);
- const infoApi = await getSystemApi(api).getPublicSystemInfo();
- await api.authenticateUserByName(username, password);
- const sessionApi = await getSessionApi(api);
- const sessions = await sessionApi.getSessions();
- return {
- type: 'jellyfin',
- appId: app.id,
- serverAddress: app.url,
- version: infoApi.data.Version ?? undefined,
- sessions: sessions.data.map(
- (session): GenericSessionInfo => ({
- id: session.Id ?? '?',
- username: session.UserName ?? undefined,
- sessionName: `${session.Client} (${session.DeviceName})`,
- supportsMediaControl: session.SupportsMediaControl ?? false,
- currentlyPlaying: session.NowPlayingItem
- ? {
- name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
- seasonName: session.NowPlayingItem.SeasonName as string,
- episodeName: session.NowPlayingItem.Name as string,
- albumName: session.NowPlayingItem.Album as string,
- episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
- metadata: {
- video:
- session.NowPlayingItem &&
- session.NowPlayingItem.Width &&
- session.NowPlayingItem.Height
- ? {
- videoCodec: undefined,
- width: session.NowPlayingItem.Width ?? undefined,
- height: session.NowPlayingItem.Height ?? undefined,
- bitrate: undefined,
- videoFrameRate: session.TranscodingInfo?.Framerate
- ? String(session.TranscodingInfo?.Framerate)
- : undefined,
- }
- : undefined,
- audio: session.TranscodingInfo
- ? {
- audioChannels: session.TranscodingInfo.AudioChannels ?? undefined,
- audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
- }
- : undefined,
- transcoding: session.TranscodingInfo
- ? {
- audioChannels: session.TranscodingInfo.AudioChannels ?? -1,
- audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
- container: session.TranscodingInfo.Container ?? undefined,
- width: session.TranscodingInfo.Width ?? undefined,
- height: session.TranscodingInfo.Height ?? undefined,
- videoCodec: session.TranscodingInfo?.VideoCodec ?? undefined,
- audioDecision: undefined,
- context: undefined,
- duration: undefined,
- error: undefined,
- sourceAudioCodec: undefined,
- sourceVideoCodec: undefined,
- timeStamp: undefined,
- transcodeHwRequested: undefined,
- videoDecision: undefined,
- }
- : undefined,
- },
- type: convertJellyfinType(session.NowPlayingItem.Type),
- }
- : undefined,
- userProfilePicture: undefined,
- })
- ),
- success: true,
- };
- }
- case 'plex': {
- const apiKey = findAppProperty(app, 'apiKey');
-
- if (!apiKey) {
- return {
- serverAddress: app.url,
- sessions: [],
- type: 'plex',
- appId: app.id,
- version: undefined,
- success: false,
- };
- }
-
- const plexClient = new PlexClient(app.url, apiKey);
- const sessions = await plexClient.getSessions();
- return {
- serverAddress: app.url,
- sessions,
- type: 'plex',
- version: undefined,
- appId: app.id,
- success: true,
- };
- }
- default: {
- Consola.warn(
- `media-server api entered a fallback case. This should normally not happen and must be reported. Cause: '${app.name}' (${app.id})`
- );
- return undefined;
- }
- }
-};
-
-const convertJellyfinType = (kind: BaseItemKind | undefined): GenericCurrentlyPlaying['type'] => {
- switch (kind) {
- case BaseItemKind.Audio:
- case BaseItemKind.MusicVideo:
- return 'audio';
- case BaseItemKind.Episode:
- case BaseItemKind.Video:
- return 'video';
- case BaseItemKind.Movie:
- return 'movie';
- case BaseItemKind.TvChannel:
- case BaseItemKind.TvProgram:
- case BaseItemKind.LiveTvChannel:
- case BaseItemKind.LiveTvProgram:
- return 'tv';
- default:
- return undefined;
- }
-};
-
-export default async (request: NextApiRequest, response: NextApiResponse) => {
- if (request.method === 'GET') {
- return Get(request, response);
- }
-
- return response.status(405);
-};
diff --git a/src/pages/api/modules/overseerr/[id].tsx b/src/pages/api/modules/overseerr/[id].tsx
deleted file mode 100644
index 2fc9a31c47e..00000000000
--- a/src/pages/api/modules/overseerr/[id].tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import axios from 'axios';
-import Consola from 'consola';
-import { getCookie } from 'cookies-next';
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import type { MediaType } from '../../../../modules/overseerr/SearchResult';
-import { getConfig } from '../../../../tools/config/getConfig';
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- // Get the slug of the request
- const { id, type } = req.query as { id: string; type: string };
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const app = config.apps.find(
- (app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
- );
- if (!id) {
- return res.status(400).json({ error: 'No id provided' });
- }
- if (!type) {
- return res.status(400).json({ error: 'No type provided' });
- }
- const apiKey = app?.integration?.properties.find((x) => x.field === 'apiKey')?.value;
- if (!apiKey) {
- return res.status(400).json({ error: 'No apps found' });
- }
-
- const appUrl = new URL(app.url);
- switch (type) {
- case 'movie':
- return axios
- .get(`${appUrl.origin}/api/v1/movie/${id}`, {
- headers: {
- // Set X-Api-Key to the value of the API key
- 'X-Api-Key': apiKey,
- },
- })
- .then((axiosres) => res.status(200).json(axiosres.data))
-
- .catch((err) => {
- Consola.error(err);
- return res.status(500).json({
- message: 'Something went wrong',
- });
- });
- case 'tv':
- // Make request to the tv api
- return axios
- .get(`${appUrl.origin}/api/v1/tv/${id}`, {
- headers: {
- // Set X-Api-Key to the value of the API key
- 'X-Api-Key': apiKey,
- },
- })
- .then((axiosres) => res.status(200).json(axiosres.data))
- .catch((err) => {
- Consola.error(err);
- return res.status(500).json({
- message: 'Something went wrong',
- });
- });
-
- default:
- return res.status(400).json({
- message: 'Wrong request, type should be movie or tv',
- });
- }
-}
-
-async function Post(req: NextApiRequest, res: NextApiResponse) {
- // Get the slug of the request
- const { id } = req.query as { id: string };
- const { seasons, type } = req.body as { seasons?: number[]; type: MediaType };
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const app = config.apps.find(
- (app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
- );
- if (!id) {
- return res.status(400).json({ error: 'No id provided' });
- }
- if (!type) {
- return res.status(400).json({ error: 'No type provided' });
- }
-
- const apiKey = app?.integration?.properties.find((x) => x.field === 'apiKey')?.value;
- if (!apiKey) {
- return res.status(400).json({ error: 'No app found' });
- }
- if (type === 'movie' && !seasons) {
- return res.status(400).json({ error: 'No seasons provided' });
- }
- const appUrl = new URL(app.url);
- Consola.info('Got an Overseerr request with these arguments', {
- mediaType: type,
- mediaId: id,
- seasons,
- });
- return axios
- .post(
- `${appUrl.origin}/api/v1/request`,
- {
- mediaType: type,
- mediaId: Number(id),
- seasons,
- },
- {
- headers: {
- // Set X-Api-Key to the value of the API key
- 'X-Api-Key': apiKey,
- },
- }
- )
- .then((axiosres) => res.status(200).json(axiosres.data))
- .catch((err) =>
- res.status(500).json({
- message: err.message,
- })
- );
-}
-
-async function Put(req: NextApiRequest, res: NextApiResponse) {
- // Get the slug of the request
- const { id, action } = req.query as { id: string; action: string };
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- Consola.log('Got a request to approve or decline a request', id, action);
- const app = config.apps.find(
- (app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
- );
- if (!id) {
- return res.status(400).json({ error: 'No id provided' });
- }
- if (action !== 'approve' && action !== 'decline') {
- return res.status(400).json({ error: 'Action type undefined' });
- }
-
- const apiKey = app?.integration?.properties.find((x) => x.field === 'apiKey')?.value;
- if (!apiKey) {
- return res.status(400).json({ error: 'No app found' });
- }
- const appUrl = new URL(app.url);
- return axios
- .post(
- `${appUrl.origin}/api/v1/request/${id}/${action}`,
- {},
- {
- headers: {
- 'X-Api-Key': apiKey,
- },
- }
- )
- .then((axiosres) => res.status(200).json(axiosres.data))
- .catch((err) =>
- res.status(500).json({
- message: err.message,
- })
- );
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- if (req.method === 'POST') {
- return Post(req, res);
- }
- if (req.method === 'GET') {
- return Get(req, res);
- }
- if (req.method === 'PUT') {
- return Put(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/overseerr/index.ts b/src/pages/api/modules/overseerr/index.ts
deleted file mode 100644
index 80d91a76e57..00000000000
--- a/src/pages/api/modules/overseerr/index.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import axios from 'axios';
-import { getCookie } from 'cookies-next';
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import { getConfig } from '../../../../tools/config/getConfig';
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const { query } = req.query;
- const app = config.apps.find(
- (app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
- );
- // If query is an empty string, return an empty array
- if (query === '' || query === undefined) {
- return res.status(200).json([]);
- }
-
- const apiKey = app?.integration?.properties.find((x) => x.field === 'apiKey')?.value;
- if (!app || !query || !apiKey) {
- return res.status(400).json({
- error: 'Wrong request',
- });
- }
- const appUrl = new URL(app.url);
- const data = await axios
- .get(`${appUrl.origin}/api/v1/search?query=${query}`, {
- headers: {
- // Set X-Api-Key to the value of the API key
- 'X-Api-Key': apiKey,
- },
- })
- .then((res) => res.data);
- // Get login, password and url from the body
- res.status(200).json(data);
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/rss/index.ts b/src/pages/api/modules/rss/index.ts
deleted file mode 100644
index 6167f470f12..00000000000
--- a/src/pages/api/modules/rss/index.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import Consola from 'consola';
-import { getCookie } from 'cookies-next';
-import { decode, encode } from 'html-entities';
-import { NextApiRequest, NextApiResponse } from 'next';
-import Parser from 'rss-parser';
-import xss from 'xss';
-import { z } from 'zod';
-
-import { getConfig } from '../../../../tools/config/getConfig';
-import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool';
-import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile';
-
-type CustomItem = {
- 'media:content': string;
- enclosure: {
- url: string;
- };
-};
-
-const parser: Parser = new Parser({
- customFields: {
- item: ['media:content', 'enclosure'],
- },
-});
-
-const getQuerySchema = z.object({
- widgetId: z.string().uuid(),
- feedUrl: z.string(),
-});
-
-export const Get = async (request: NextApiRequest, response: NextApiResponse) => {
- const configName = getCookie('config-name', { req: request });
- const config = getConfig(configName?.toString() ?? 'default');
-
- const parseResult = getQuerySchema.safeParse(request.query);
-
- if (!parseResult.success) {
- response.status(400).json({ message: 'invalid query parameters, please specify the widgetId' });
- return;
- }
-
- const rssWidget = config.widgets.find(
- (x) => x.type === 'rss' && x.id === parseResult.data.widgetId
- ) as IRssWidget | undefined;
- if (
- !rssWidget ||
- !rssWidget.properties.rssFeedUrl ||
- rssWidget.properties.rssFeedUrl.length < 1
- ) {
- response.status(400).json({ message: 'required widget does not exist' });
- return;
- }
-
- Consola.info(`Requesting RSS feed at url ${parseResult.data.feedUrl}`);
- const stopWatch = new Stopwatch();
- const feed = await parser.parseURL(parseResult.data.feedUrl);
- Consola.info(`Retrieved RSS feed after ${stopWatch.getEllapsedMilliseconds()} milliseconds`);
-
- const orderedFeed = {
- ...feed,
- items: feed.items
- .map((item: { title: string; content: string; 'content:encoded': string }) => ({
- ...item,
- title: item.title ? decode(item.title) : undefined,
- content: processItemContent(
- item['content:encoded'] ?? item.content,
- rssWidget.properties.dangerousAllowSanitizedItemContent
- ),
- enclosure: createEnclosure(item),
- link: createLink(item),
- }))
- .sort((a: { pubDate: number }, b: { pubDate: number }) => {
- if (!a.pubDate || !b.pubDate) {
- return 0;
- }
-
- return a.pubDate - b.pubDate;
- })
- .slice(0, 20),
- };
-
- response.status(200).json({
- feed: orderedFeed,
- success: orderedFeed?.items !== undefined,
- });
-};
-
-const processItemContent = (content: string, dangerousAllowSanitizedItemContent: boolean) => {
- if (dangerousAllowSanitizedItemContent) {
- return xss(content, {
- allowList: {
- p: [],
- h1: [],
- h2: [],
- h3: [],
- h4: [],
- h5: [],
- h6: [],
- a: ['href'],
- b: [],
- strong: [],
- i: [],
- em: [],
- img: ['src', 'width', 'height'],
- br: [],
- small: [],
- ul: [],
- li: [],
- ol: [],
- figure: [],
- svg: [],
- code: [],
- mark: [],
- blockquote: [],
- },
- });
- }
-
- return encode(content);
-};
-
-const createLink = (item: any) => {
- if (item.link) {
- return item.link;
- }
-
- return item.guid;
-};
-
-const createEnclosure = (item: any) => {
- if (item.enclosure) {
- return item.enclosure;
- }
-
- if (item['media:content']) {
- return {
- url: item['media:content'].$.url,
- };
- }
-
- return undefined;
-};
-
-export default async (request: NextApiRequest, response: NextApiResponse) => {
- if (request.method === 'GET') {
- return Get(request, response);
- }
-
- return response.status(405);
-};
diff --git a/src/pages/api/modules/usenet/history.ts b/src/pages/api/modules/usenet/history.ts
deleted file mode 100644
index 61ca1ebbb41..00000000000
--- a/src/pages/api/modules/usenet/history.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { getCookie } from 'cookies-next';
-import dayjs from 'dayjs';
-import duration from 'dayjs/plugin/duration';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { Client } from 'sabnzbd-api';
-import { findAppProperty } from '~/tools/client/app-properties';
-
-import { NzbgetClient } from '../../../../server/api/routers/usenet/nzbget/nzbget-client';
-import { NzbgetHistoryItem } from '../../../../server/api/routers/usenet/nzbget/types';
-import { getConfig } from '../../../../tools/config/getConfig';
-import { UsenetHistoryItem } from '../../../../widgets/useNet/types';
-
-dayjs.extend(duration);
-
-export interface UsenetHistoryRequestParams {
- appId: string;
- offset: number;
- limit: number;
-}
-
-export interface UsenetHistoryResponse {
- items: UsenetHistoryItem[];
- total: number;
-}
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- try {
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const { limit, offset, appId } = req.query as any as UsenetHistoryRequestParams;
-
- const app = config.apps.find((x) => x.id === appId);
-
- if (!app) {
- throw new Error(`App with ID "${req.query.appId}" could not be found.`);
- }
-
- let response: UsenetHistoryResponse;
- switch (app.integration?.type) {
- case 'nzbGet': {
- const url = new URL(app.url);
- const options = {
- host: url.hostname,
- port: url.port || (url.protocol === 'https:' ? '443' : '80'),
- login: findAppProperty(app, 'username'),
- hash: findAppProperty(app, 'password'),
- };
-
- const nzbGet = NzbgetClient(options);
-
- const nzbgetHistory: NzbgetHistoryItem[] = await new Promise((resolve, reject) => {
- nzbGet.history(false, (err: any, result: NzbgetHistoryItem[]) => {
- if (!err) {
- resolve(result);
- } else {
- reject(err);
- }
- });
- });
-
- if (!nzbgetHistory) {
- throw new Error('Error while getting NZBGet history');
- }
-
- const nzbgetItems: UsenetHistoryItem[] = nzbgetHistory.map((item: NzbgetHistoryItem) => ({
- id: item.NZBID.toString(),
- name: item.Name,
- // Convert from MB to bytes
- size: item.DownloadedSizeMB * 1000000,
- time: item.DownloadTimeSec,
- }));
-
- response = {
- items: nzbgetItems,
- total: nzbgetItems.length,
- };
- break;
- }
- case 'sabnzbd': {
- const { origin } = new URL(app.url);
-
- const apiKey = findAppProperty(app, 'apiKey');
- if (!apiKey) {
- throw new Error(`API Key for app "${app.name}" is missing`);
- }
-
- const history = await new Client(origin, apiKey).history(offset, limit);
-
- const items: UsenetHistoryItem[] = history.slots.map((slot) => ({
- id: slot.nzo_id,
- name: slot.name,
- size: slot.bytes,
- time: slot.download_time,
- }));
-
- response = {
- items,
- total: history.noofslots,
- };
- break;
- }
- default:
- throw new Error(`App type "${app.integration?.type}" unrecognized.`);
- }
-
- return res.status(200).json(response);
- } catch (err) {
- return res.status(500).send((err as any).message);
- }
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/usenet/index.ts b/src/pages/api/modules/usenet/index.ts
deleted file mode 100644
index 78ca460b4eb..00000000000
--- a/src/pages/api/modules/usenet/index.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { getCookie } from 'cookies-next';
-import dayjs from 'dayjs';
-import duration from 'dayjs/plugin/duration';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { Client } from 'sabnzbd-api';
-import { findAppProperty } from '~/tools/client/app-properties';
-
-import { NzbgetClient } from '../../../../server/api/routers/usenet/nzbget/nzbget-client';
-import { NzbgetStatus } from '../../../../server/api/routers/usenet/nzbget/types';
-import { getConfig } from '../../../../tools/config/getConfig';
-
-dayjs.extend(duration);
-
-export interface UsenetInfoRequestParams {
- appId: string;
-}
-
-export interface UsenetInfoResponse {
- paused: boolean;
- sizeLeft: number;
- speed: number;
- eta: number;
-}
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- try {
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const { appId } = req.query as any as UsenetInfoRequestParams;
-
- const app = config.apps.find((x) => x.id === appId);
-
- if (!app) {
- throw new Error(`App with ID "${req.query.appId}" could not be found.`);
- }
-
- let response: UsenetInfoResponse;
- switch (app.integration?.type) {
- case 'nzbGet': {
- const url = new URL(app.url);
- const options = {
- host: url.hostname,
- port: url.port || (url.protocol === 'https:' ? '443' : '80'),
- login: findAppProperty(app, 'username'),
- hash: findAppProperty(app, 'password'),
- };
-
- const nzbGet = NzbgetClient(options);
-
- const nzbgetStatus: NzbgetStatus = await new Promise((resolve, reject) => {
- nzbGet.status((err: any, result: NzbgetStatus) => {
- if (!err) {
- resolve(result);
- } else {
- reject(err);
- }
- });
- });
-
- if (!nzbgetStatus) {
- throw new Error('Error while getting NZBGet status');
- }
-
- const bytesRemaining = nzbgetStatus.RemainingSizeMB * 1000000;
- const eta = bytesRemaining / nzbgetStatus.DownloadRate;
- response = {
- paused: nzbgetStatus.DownloadPaused,
- sizeLeft: bytesRemaining,
- speed: nzbgetStatus.DownloadRate,
- eta,
- };
- break;
- }
- case 'sabnzbd': {
- const apiKey = findAppProperty(app, 'apiKey');
- if (!apiKey) {
- throw new Error(`API Key for app "${app.name}" is missing`);
- }
-
- const { origin } = new URL(app.url);
-
- const queue = await new Client(origin, apiKey).queue(0, -1);
-
- const [hours, minutes, seconds] = queue.timeleft.split(':');
- const eta = dayjs.duration({
- hour: parseInt(hours, 10),
- minutes: parseInt(minutes, 10),
- seconds: parseInt(seconds, 10),
- } as any);
-
- response = {
- paused: queue.paused,
- sizeLeft: parseFloat(queue.mbleft) * 1024 * 1024,
- speed: parseFloat(queue.kbpersec) * 1000,
- eta: eta.asSeconds(),
- };
- break;
- }
- default:
- throw new Error(`App type "${app.integration?.type}" unrecognized.`);
- }
-
- return res.status(200).json(response);
- } catch (err) {
- return res.status(500).send((err as any).message);
- }
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/usenet/pause.ts b/src/pages/api/modules/usenet/pause.ts
deleted file mode 100644
index 65fbf04d4dc..00000000000
--- a/src/pages/api/modules/usenet/pause.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { getCookie } from 'cookies-next';
-import dayjs from 'dayjs';
-import duration from 'dayjs/plugin/duration';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { Client } from 'sabnzbd-api';
-import { findAppProperty } from '~/tools/client/app-properties';
-
-import { NzbgetClient } from '../../../../server/api/routers/usenet/nzbget/nzbget-client';
-import { getConfig } from '../../../../tools/config/getConfig';
-
-dayjs.extend(duration);
-
-export interface UsenetPauseRequestParams {
- appId: string;
-}
-
-async function Post(req: NextApiRequest, res: NextApiResponse) {
- try {
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const { appId } = req.query as any as UsenetPauseRequestParams;
-
- const app = config.apps.find((x) => x.id === appId);
-
- if (!app) {
- throw new Error(`App with ID "${req.query.appId}" could not be found.`);
- }
-
- let result;
- switch (app.integration?.type) {
- case 'nzbGet': {
- const url = new URL(app.url);
- const options = {
- host: url.hostname,
- port: url.port || (url.protocol === 'https:' ? '443' : '80'),
- login: findAppProperty(app, 'username'),
- hash: findAppProperty(app, 'password'),
- };
-
- const nzbGet = NzbgetClient(options);
-
- result = await new Promise((resolve, reject) => {
- nzbGet.pauseDownload(false, (err: any, result: any) => {
- if (!err) {
- resolve(result);
- } else {
- reject(err);
- }
- });
- });
- break;
- }
- case 'sabnzbd': {
- const apiKey = findAppProperty(app, 'apiKey');
- if (!apiKey) {
- throw new Error(`API Key for app "${app.name}" is missing`);
- }
-
- const { origin } = new URL(app.url);
-
- result = await new Client(origin, apiKey).queuePause();
- break;
- }
- default:
- throw new Error(`App type "${app.integration?.type}" unrecognized.`);
- }
-
- return res.status(200).json(result);
- } catch (err) {
- return res.status(500).send((err as any).message);
- }
-}
-
-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);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/usenet/queue.ts b/src/pages/api/modules/usenet/queue.ts
deleted file mode 100644
index b5cc95f8570..00000000000
--- a/src/pages/api/modules/usenet/queue.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { getCookie } from 'cookies-next';
-import dayjs from 'dayjs';
-import duration from 'dayjs/plugin/duration';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { Client } from 'sabnzbd-api';
-import { findAppProperty } from '~/tools/client/app-properties';
-
-import { NzbgetClient } from '../../../../server/api/routers/usenet/nzbget/nzbget-client';
-import { NzbgetQueueItem, NzbgetStatus } from '../../../../server/api/routers/usenet/nzbget/types';
-import { getConfig } from '../../../../tools/config/getConfig';
-import { UsenetQueueItem } from '../../../../widgets/useNet/types';
-
-dayjs.extend(duration);
-
-export interface UsenetQueueRequestParams {
- appId: string;
- offset: number;
- limit: number;
-}
-
-export interface UsenetQueueResponse {
- items: UsenetQueueItem[];
- total: number;
-}
-
-async function Get(req: NextApiRequest, res: NextApiResponse) {
- try {
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const { limit, offset, appId } = req.query as any as UsenetQueueRequestParams;
-
- const app = config.apps.find((x) => x.id === appId);
-
- if (!app) {
- throw new Error(`App with ID "${req.query.appId}" could not be found.`);
- }
-
- let response: UsenetQueueResponse;
- switch (app.integration?.type) {
- case 'nzbGet': {
- const url = new URL(app.url);
- const options = {
- host: url.hostname,
- port: url.port || (url.protocol === 'https:' ? '443' : '80'),
- login: findAppProperty(app, 'username'),
- hash: findAppProperty(app, 'password'),
- };
-
- const nzbGet = NzbgetClient(options);
-
- const nzbgetQueue: NzbgetQueueItem[] = await new Promise((resolve, reject) => {
- nzbGet.listGroups((err: any, result: NzbgetQueueItem[]) => {
- if (!err) {
- resolve(result);
- } else {
- reject(err);
- }
- });
- });
-
- if (!nzbgetQueue) {
- throw new Error('Error while getting NZBGet queue');
- }
-
- const nzbgetStatus: NzbgetStatus = await new Promise((resolve, reject) => {
- nzbGet.status((err: any, result: NzbgetStatus) => {
- if (!err) {
- resolve(result);
- } else {
- reject(err);
- }
- });
- });
-
- if (!nzbgetStatus) {
- throw new Error('Error while getting NZBGet status');
- }
-
- const nzbgetItems: UsenetQueueItem[] = nzbgetQueue.map((item: NzbgetQueueItem) => ({
- id: item.NZBID.toString(),
- name: item.NZBName,
- progress: (item.DownloadedSizeMB / item.FileSizeMB) * 100,
- eta: (item.RemainingSizeMB * 1000000) / nzbgetStatus.DownloadRate,
- // Multiple MB to get bytes
- size: item.FileSizeMB * 1000 * 1000,
- state: getNzbgetState(item.Status),
- }));
-
- response = {
- items: nzbgetItems,
- total: nzbgetItems.length,
- };
- break;
- }
- case 'sabnzbd': {
- const apiKey = findAppProperty(app, 'apiKey');
- if (!apiKey) {
- throw new Error(`API Key for app "${app.name}" is missing`);
- }
-
- const { origin } = new URL(app.url);
- const queue = await new Client(origin, apiKey).queue(offset, limit);
-
- const items: UsenetQueueItem[] = queue.slots.map((slot) => {
- const [hours, minutes, seconds] = slot.timeleft.split(':');
- const eta = dayjs.duration({
- hour: parseInt(hours, 10),
- minutes: parseInt(minutes, 10),
- seconds: parseInt(seconds, 10),
- } as any);
-
- return {
- id: slot.nzo_id,
- eta: eta.asSeconds(),
- name: slot.filename,
- progress: parseFloat(slot.percentage),
- size: parseFloat(slot.mb) * 1000 * 1000,
- state: slot.status.toLowerCase() as any,
- };
- });
-
- response = {
- items,
- total: queue.noofslots,
- };
- break;
- }
- default:
- throw new Error(`App type "${app.integration?.type}" unrecognized.`);
- }
-
- return res.status(200).json(response);
- } catch (err) {
- return res.status(500).send((err as any).message);
- }
-}
-
-function getNzbgetState(status: string) {
- switch (status) {
- case 'QUEUED':
- return 'queued';
- case 'PAUSED ':
- return 'paused';
- default:
- return 'downloading';
- }
-}
-
-export default async (req: NextApiRequest, res: NextApiResponse) => {
- // Filter out if the reuqest is a POST or a GET
- if (req.method === 'GET') {
- return Get(req, res);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/api/modules/usenet/resume.ts b/src/pages/api/modules/usenet/resume.ts
deleted file mode 100644
index 245fb0ac8a1..00000000000
--- a/src/pages/api/modules/usenet/resume.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { getCookie } from 'cookies-next';
-import dayjs from 'dayjs';
-import duration from 'dayjs/plugin/duration';
-import { NextApiRequest, NextApiResponse } from 'next';
-import { Client } from 'sabnzbd-api';
-import { findAppProperty } from '~/tools/client/app-properties';
-
-import { NzbgetClient } from '../../../../server/api/routers/usenet/nzbget/nzbget-client';
-import { getConfig } from '../../../../tools/config/getConfig';
-
-dayjs.extend(duration);
-
-export interface UsenetResumeRequestParams {
- appId: string;
- nzbId?: string;
-}
-
-async function Post(req: NextApiRequest, res: NextApiResponse) {
- try {
- const configName = getCookie('config-name', { req });
- const config = getConfig(configName?.toString() ?? 'default');
- const { appId } = req.query as any as UsenetResumeRequestParams;
-
- const app = config.apps.find((x) => x.id === appId);
-
- if (!app) {
- throw new Error(`App with ID "${req.query.appId}" could not be found.`);
- }
-
- let result;
- switch (app.integration?.type) {
- case 'nzbGet': {
- const url = new URL(app.url);
- const options = {
- host: url.hostname,
- port: url.port || (url.protocol === 'https:' ? '443' : '80'),
- login: findAppProperty(app, 'username'),
- hash: findAppProperty(app, 'password'),
- };
-
- const nzbGet = NzbgetClient(options);
-
- result = await new Promise((resolve, reject) => {
- nzbGet.resumeDownload(false, (err: any, result: any) => {
- if (!err) {
- resolve(result);
- } else {
- reject(err);
- }
- });
- });
- break;
- }
- case 'sabnzbd': {
- const apiKey = findAppProperty(app, 'apiKey');
- if (!apiKey) {
- throw new Error(`API Key for app "${app.name}" is missing`);
- }
-
- const { origin } = new URL(app.url);
-
- result = await new Client(origin, apiKey).queueResume();
- break;
- }
- default:
- throw new Error(`App type "${app.integration?.type}" unrecognized.`);
- }
-
- return res.status(200).json(result);
- } catch (err) {
- return res.status(500).send((err as any).message);
- }
-}
-
-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);
- }
- return res.status(405).json({
- statusCode: 405,
- message: 'Method not allowed',
- });
-};
diff --git a/src/pages/migrate.tsx b/src/pages/migrate.tsx
deleted file mode 100644
index 9b13a79b8d5..00000000000
--- a/src/pages/migrate.tsx
+++ /dev/null
@@ -1,358 +0,0 @@
-import {
- Alert,
- Anchor,
- AppShell,
- Badge,
- Box,
- Button,
- Container,
- Group,
- Header,
- List,
- Loader,
- Paper,
- Progress,
- Space,
- Stack,
- Stepper,
- Switch,
- Text,
- ThemeIcon,
- Title,
- createStyles,
- useMantineColorScheme,
- useMantineTheme,
-} from '@mantine/core';
-import {
- IconAlertCircle,
- IconBrandDiscord,
- IconCheck,
- IconCircleCheck,
- IconMoonStars,
- IconSun,
-} from '@tabler/icons-react';
-import axios from 'axios';
-import { motion } from 'framer-motion';
-import fs from 'fs';
-import { GetServerSidePropsContext } from 'next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import React, { useEffect, useState } from 'react';
-
-import { Logo } from '../components/layout/Logo';
-import { usePrimaryGradient } from '../components/layout/useGradient';
-
-const useStyles = createStyles((theme) => ({
- root: {
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- height: '100%',
- backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).background,
- },
-
- label: {
- textAlign: 'center',
- color: theme.colors[theme.primaryColor][8],
- fontWeight: 900,
- fontSize: 110,
- lineHeight: 1,
- marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
-
- [theme.fn.smallerThan('sm')]: {
- fontSize: 60,
- },
- },
-
- title: {
- fontFamily: `Greycliff CF, ${theme.fontFamily}`,
- textAlign: 'center',
- fontWeight: 900,
- fontSize: 38,
-
- [theme.fn.smallerThan('sm')]: {
- fontSize: 32,
- },
- },
-
- card: {
- position: 'relative',
- overflow: 'visible',
- padding: theme.spacing.xl,
- },
-
- icon: {
- position: 'absolute',
- top: -ICON_SIZE / 3,
- left: `calc(50% - ${ICON_SIZE / 2}px)`,
- },
-
- description: {
- maxWidth: 700,
- margin: 'auto',
- marginTop: theme.spacing.xl,
- marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
- },
-}));
-
-export default function ServerError({ configs }: { configs: any }) {
- const { classes } = useStyles();
- const [active, setActive] = React.useState(0);
- const gradient = usePrimaryGradient();
- const [progress, setProgress] = React.useState(0);
- const [isUpgrading, setIsUpgrading] = React.useState(false);
- const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
- const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
-
- return (
- ({
- main: {
- backgroundColor:
- theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0],
- },
- })}
- >
-
-
-
-
-
-
- {/* Header content */}
-
- }
- styles={(theme) => ({
- main: {
- backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor })
- .background,
- },
- })}
- >
-
-
-
-
-
- Homarr v0.11
-
-
-
-
- {active === 0 && "Good to see you back! Let's get started"}
- {active === 1 && progress !== 100 && 'Migrating your configs'}
- {active === 1 && progress === 100 && 'Migration complete!'}
-
-
-
-
-
- A few things have changed since the last time you used Homarr. We'll
- help you migrate your old configuration to the new format. This process is automatic
- and should take less than a minute. Then, you'll be able to use the new
- features of Homarr!
-
- }
- title="Please make a backup of your configs!"
- color="red"
- radius="md"
- variant="outline"
- >
- Please make sure to have a backup of your configs in case something goes wrong.{' '}
- Not all settings can be migrated , so you'll have to re-do some
- configuration yourself.
-
-
- }
- label="Step 2"
- description="Migrating your configs"
- >
-
-
-
-
- Homarr v0.11 brings a lot of new features, if you are interested in learning
- about them, please check out the{' '}
-
- documentation page
-
-
-
-
-
- That's it ! We hope you enjoy the new flexibility v0.11 brings. If you spot any
- bugs make sure to report them as a{' '}
-
- github issue
- {' '}
- or directly on the
-
-
-
- discord !
-
-
-
-
-
-
- }
- onClick={active === 3 ? () => window.location.reload() : nextStep}
- variant="filled"
- disabled={active === 1 && progress < 100}
- >
- {active === 3 ? 'Finish' : 'Next'}
-
-
-
-
-
- );
-}
-
-function SwitchToggle() {
- const { colorScheme, toggleColorScheme } = useMantineColorScheme();
- const theme = useMantineTheme();
-
- return (
- toggleColorScheme()}
- size="lg"
- onLabel={ }
- offLabel={ }
- />
- );
-}
-
-export async function getServerSideProps({ req, res, locale }: GetServerSidePropsContext) {
- // Get all the configs in the /data/configs folder
- // All the files that end in ".json"
- const configs = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
-
- if (configs.length === 0) {
- res.writeHead(302, {
- Location: '/',
- });
- res.end();
- return { props: {} };
- }
- // If all the configs are migrated (contains a schemaVersion), redirect to the index
- if (
- configs.every(
- (config) => JSON.parse(fs.readFileSync(`./data/configs/${config}`, 'utf8')).schemaVersion
- )
- ) {
- res.writeHead(302, {
- Location: '/',
- });
- res.end();
- return {
- processed: true,
- };
- }
- return {
- props: {
- configs: configs.map(
- // Get all the file names in ./data/configs
- (config) => config.replace('.json', '')
- ),
- ...(await serverSideTranslations(locale!, [])),
- // Will be passed to the page component as props
- },
- };
-}
-
-const ICON_SIZE = 60;
-
-export function StatsCard({
- configs,
- progress,
- setProgress,
-}: {
- configs: string[];
- progress: number;
- setProgress: (progress: number) => void;
-}) {
- const { classes } = useStyles();
- const numberOfConfigs = configs.length;
- // Update the progress every 100ms
- const [treatedConfigs, setTreatedConfigs] = useState([]);
- // Stop the progress at 100%
- useEffect(() => {
- const data = axios.post('/api/migrate').then((response) => {
- setProgress(100);
- });
-
- const interval = setInterval(() => {
- if (configs.length === 0) {
- clearInterval(interval);
- setProgress(100);
- return;
- }
- // Add last element of configs to the treatedConfigs array
- setTreatedConfigs((treatedConfigs) => [...treatedConfigs, configs[configs.length - 1]]);
- // Remove last element of configs
- configs.pop();
- }, 500);
- return () => clearInterval(interval);
- }, [configs]);
-
- return (
-
-
-
- Progress
-
-
- {(100 / (numberOfConfigs + 1)).toFixed(1)}%
-
-
-
-
-
-
-
- }
- >
- {configs.map((config, index) => (
- }>
- {config ?? 'Unknown'}
-
- ))}
- {treatedConfigs.map((config, index) => (
- {config ?? 'Unknown'}
- ))}
-
-
-
-
-
- {configs.length} configs left
-
-
- );
-}
diff --git a/src/tools/config/backendMigrateConfig.ts b/src/tools/config/backendMigrateConfig.ts
deleted file mode 100644
index cd35cd9d663..00000000000
--- a/src/tools/config/backendMigrateConfig.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import fs from 'fs';
-
-import { BackendConfigType } from '../../types/config';
-import { Config } from '../types';
-import { migrateConfig } from './migrateConfig';
-
-export function backendMigrateConfig(config: Config, name: string): BackendConfigType {
- const migratedConfig = migrateConfig(config);
-
- // Make a backup of the old file ./data/configs/${name}.json
- // New name is ./data/configs/${name}.bak
- fs.copyFileSync(`./data/configs/${name}.json`, `./data/configs/${name}.json.bak`);
-
- // Overrite the file ./data/configs/${name}.json
- // with the new config format
- fs.writeFileSync(`./data/configs/${name}.json`, JSON.stringify(migratedConfig, null, 2));
-
- return migratedConfig;
-}
diff --git a/src/tools/config/getConfig.ts b/src/tools/config/getConfig.ts
index 080f20e655c..6bee0db4a28 100644
--- a/src/tools/config/getConfig.ts
+++ b/src/tools/config/getConfig.ts
@@ -2,7 +2,6 @@ import Consola from 'consola';
import { v4 as uuidv4 } from 'uuid';
import { BackendConfigType, ConfigType } from '../../types/config';
-import { backendMigrateConfig } from './backendMigrateConfig';
import { configExists } from './configExists';
import { getFallbackConfig } from './getFallbackConfig';
import { readConfig } from './readConfig';
@@ -16,10 +15,6 @@ export const getConfig = (name: string): BackendConfigType => {
// then it is an old config file and we should try to migrate it
// to the new format.
const config = readConfig(name);
- if (config.schemaVersion === undefined) {
- Consola.log('Migrating config file...', config.name);
- return backendMigrateConfig(config, name);
- }
let backendConfig = config as BackendConfigType;
diff --git a/src/tools/config/migrateConfig.ts b/src/tools/config/migrateConfig.ts
deleted file mode 100644
index 12f4b343546..00000000000
--- a/src/tools/config/migrateConfig.ts
+++ /dev/null
@@ -1,490 +0,0 @@
-import Consola from 'consola';
-import { v4 as uuidv4 } from 'uuid';
-
-import { ConfigAppIntegrationType, ConfigAppType, IntegrationType } from '../../types/app';
-import { AreaType } from '../../types/area';
-import { CategoryType } from '../../types/category';
-import { BackendConfigType } from '../../types/config';
-import { SearchEngineCommonSettingsType } from '../../types/settings';
-import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
-import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
-import { IDateWidget } from '../../widgets/date/DateTile';
-import { ITorrentNetworkTraffic } from '../../widgets/download-speed/TorrentNetworkTrafficTile';
-import { ITorrent } from '../../widgets/torrent/TorrentTile';
-import { IUsenetWidget } from '../../widgets/useNet/UseNetTile';
-import { IWeatherWidget } from '../../widgets/weather/WeatherTile';
-import { IWidget } from '../../widgets/widgets';
-import { Config, serviceItem } from '../types';
-
-export function migrateConfig(config: Config): BackendConfigType {
- const newConfig: BackendConfigType = {
- schemaVersion: 1,
- configProperties: {
- name: config.name ?? 'default',
- },
- categories: [],
- widgets: migrateModules(config).filter((widget) => widget !== null),
- apps: [],
- settings: {
- common: {
- searchEngine: migrateSearchEngine(config),
- defaultConfig: 'default',
- },
- customization: {
- colors: {
- primary: config.settings.primaryColor ?? 'red',
- secondary: config.settings.secondaryColor ?? 'orange',
- shade: config.settings.primaryShade ?? 7,
- },
- layout: {
- enabledDocker: config.modules.docker?.enabled ?? false,
- enabledLeftSidebar: false,
- enabledPing: config.modules.ping?.enabled ?? false,
- enabledRightSidebar: false,
- enabledSearchbar: config.modules.search?.enabled ?? true,
- },
- accessibility: {
- disablePingPulse: false,
- replacePingDotsWithIcons: false,
- },
- },
- },
- wrappers: [
- {
- id: 'default',
- position: 0,
- },
- ],
- };
-
- config.services.forEach((service) => {
- const { category: categoryName } = service;
-
- if (!categoryName) {
- newConfig.apps.push(
- migrateService(service, {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- })
- );
- return;
- }
-
- const category = getConfigAndCreateIfNotExsists(newConfig, categoryName);
-
- if (!category) {
- return;
- }
-
- newConfig.apps.push(
- migrateService(service, { type: 'category', properties: { id: category.id } })
- );
- });
-
- Consola.info('Migrator converted a configuration with the old schema to the new schema');
-
- return newConfig;
-}
-
-const migrateSearchEngine = (config: Config): SearchEngineCommonSettingsType => {
- switch (config.settings.searchUrl) {
- case 'https://bing.com/search?q=':
- return {
- type: 'bing',
- properties: {
- enabled: true,
- openInNewTab: true,
- },
- };
- case 'https://google.com/search?q=':
- return {
- type: 'google',
- properties: {
- enabled: true,
- openInNewTab: true,
- },
- };
- case 'https://duckduckgo.com/?q=':
- return {
- type: 'duckDuckGo',
- properties: {
- enabled: true,
- openInNewTab: true,
- },
- };
- default:
- return {
- type: 'custom',
- properties: {
- enabled: true,
- openInNewTab: true,
- template: config.settings.searchUrl,
- },
- };
- }
-};
-
-const getConfigAndCreateIfNotExsists = (
- config: BackendConfigType,
- categoryName: string
-): CategoryType | null => {
- const foundCategory = config.categories.find((c) => c.name === categoryName);
- if (foundCategory) {
- return foundCategory;
- }
-
- const category: CategoryType = {
- id: uuidv4(),
- name: categoryName,
- position: config.categories.length + 1, // sync up with index of categories
- };
-
- config.categories.push(category);
-
- // sync up with categories
- if (config.wrappers.length < config.categories.length) {
- config.wrappers.push({
- id: uuidv4(),
- position: config.wrappers.length + 1, // sync up with index of categories
- });
- }
-
- return category;
-};
-
-const migrateService = (oldService: serviceItem, areaType: AreaType): ConfigAppType => ({
- id: uuidv4(),
- name: oldService.name,
- url: oldService.url,
- behaviour: {
- isOpeningNewTab: oldService.newTab ?? true,
- externalUrl: oldService.openedUrl ?? '',
- },
- network: {
- enabledStatusChecker: oldService.ping ?? true,
- statusCodes: oldService.status ?? ['200'],
- },
- appearance: {
- iconUrl: migrateIcon(oldService.icon),
- },
- integration: migrateIntegration(oldService),
- area: areaType,
- shape: {},
-});
-
-const migrateModules = (config: Config): IWidget[] => {
- const moduleKeys = Object.keys(config.modules);
- return moduleKeys
- .map((moduleKey): IWidget | null => {
- const oldModule = config.modules[moduleKey];
-
- if (!oldModule.enabled) {
- return null;
- }
-
- switch (moduleKey.toLowerCase()) {
- case 'torrent-status':
- case 'Torrent':
- return {
- id: uuidv4(),
- type: 'torrents-status',
- properties: {
- refreshInterval: 10,
- displayCompletedTorrents: oldModule.options?.hideComplete?.value ?? false,
- displayStaleTorrents: true,
- labelFilter: [],
- labelFilterIsWhitelist: true,
- },
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {},
- } as ITorrent;
- case 'weather':
- return {
- id: uuidv4(),
- type: 'weather',
- properties: {
- displayInFahrenheit: oldModule.options?.freedomunit?.value ?? false,
- location: {
- name: oldModule.options?.location?.value ?? '',
- latitude: 0,
- longitude: 0,
- },
- },
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {},
- } as IWeatherWidget;
- case 'dashdot':
- case 'Dash.': {
- const oldDashDotService = config.services.find((service) => service.type === 'Dash.');
- return {
- id: uuidv4(),
- type: 'dashdot',
- properties: {
- url: oldModule.options?.url?.value ?? oldDashDotService?.url ?? '',
- cpuMultiView: oldModule.options?.cpuMultiView?.value ?? false,
- storageMultiView: oldModule.options?.storageMultiView?.value ?? false,
- useCompactView: oldModule.options?.useCompactView?.value ?? false,
- graphs: oldModule.options?.graphs?.value ?? ['cpu', 'ram'],
- },
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {},
- } as unknown as IDashDotTile;
- }
- case 'date':
- return {
- id: uuidv4(),
- type: 'date',
- properties: {
- display24HourFormat: oldModule.options?.full?.value ?? true,
- },
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {},
- } as IDateWidget;
- case 'Download Speed' || 'dlspeed':
- return {
- id: uuidv4(),
- type: 'dlspeed',
- properties: {},
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {},
- } as ITorrentNetworkTraffic;
- case 'calendar':
- return {
- id: uuidv4(),
- type: 'calendar',
- properties: {},
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {},
- } as ICalendarWidget;
- case 'usenet':
- return {
- id: uuidv4(),
- type: 'usenet',
- properties: {},
- area: {
- type: 'wrapper',
- properties: {
- id: 'default',
- },
- },
- shape: {},
- } as IUsenetWidget;
- default:
- Consola.error(`Failed to map unknown module type ${moduleKey} to new type definitions.`);
- return null;
- }
- })
- .filter((x) => x !== null) as IWidget[];
-};
-
-const migrateIcon = (iconUrl: string) => {
- if (iconUrl.startsWith('https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/')) {
- const icon = iconUrl.split('/').at(-1);
- Consola.warn(
- `Detected legacy icon repository. Upgrading to replacement repository: ${iconUrl} -> ${icon}`
- );
- return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`;
- }
-
- return iconUrl;
-};
-
-const migrateIntegration = (oldService: serviceItem): ConfigAppIntegrationType => {
- const logInformation = (newType: IntegrationType) => {
- Consola.info(`Migrated integration ${oldService.type} to the new type ${newType}`);
- };
- switch (oldService.type) {
- case 'Deluge':
- logInformation('deluge');
- return {
- type: 'deluge',
- properties: [
- {
- field: 'password',
- type: 'private',
- value: oldService.password,
- },
- ],
- };
- case 'Jellyseerr':
- logInformation('jellyseerr');
- return {
- type: 'jellyseerr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: oldService.apiKey,
- },
- ],
- };
- case 'Overseerr':
- logInformation('overseerr');
- return {
- type: 'overseerr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: oldService.apiKey,
- },
- ],
- };
- case 'Lidarr':
- logInformation('lidarr');
- return {
- type: 'lidarr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: oldService.apiKey,
- },
- ],
- };
- case 'Radarr':
- logInformation('radarr');
- return {
- type: 'radarr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: oldService.apiKey,
- },
- ],
- };
- case 'Readarr':
- logInformation('readarr');
- return {
- type: 'readarr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: oldService.apiKey,
- },
- ],
- };
- case 'Sabnzbd':
- logInformation('sabnzbd');
- return {
- type: 'sabnzbd',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: oldService.apiKey,
- },
- ],
- };
- case 'Sonarr':
- logInformation('sonarr');
- return {
- type: 'sonarr',
- properties: [
- {
- field: 'apiKey',
- type: 'private',
- value: oldService.apiKey,
- },
- ],
- };
- case 'NZBGet':
- logInformation('nzbGet');
- return {
- type: 'nzbGet',
- properties: [
- {
- field: 'username',
- type: 'private',
- value: oldService.username,
- },
- {
- field: 'password',
- type: 'private',
- value: oldService.password,
- },
- ],
- };
- case 'qBittorrent':
- logInformation('qBittorrent');
- return {
- type: 'qBittorrent',
- properties: [
- {
- field: 'username',
- type: 'private',
- value: oldService.username,
- },
- {
- field: 'password',
- type: 'private',
- value: oldService.password,
- },
- ],
- };
- case 'Transmission':
- logInformation('transmission');
- return {
- type: 'transmission',
- properties: [
- {
- field: 'username',
- type: 'private',
- value: oldService.username,
- },
- {
- field: 'password',
- type: 'private',
- value: oldService.password,
- },
- ],
- };
- case 'Other':
- return {
- type: null,
- properties: [],
- };
- default:
- Consola.warn(
- `Integration type of service ${oldService.name} could not be mapped to new integration type definition`
- );
- return {
- type: null,
- properties: [],
- };
- }
-};
diff --git a/src/tools/getConfig.ts b/src/tools/getConfig.ts
deleted file mode 100644
index c594e9b01a2..00000000000
--- a/src/tools/getConfig.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import fs from 'fs';
-import path from 'path';
-
-import { ConfigType } from '../types/config';
-import { getFallbackConfig } from './config/getFallbackConfig';
-
-export function getConfig(name: string, props: any = undefined) {
- // Check if the config file exists
- const configPath = path.join(process.cwd(), 'data/configs', `${name}.json`);
- if (!fs.existsSync(configPath)) {
- return getFallbackConfig() as unknown as ConfigType;
- }
- const config = fs.readFileSync(configPath, 'utf8');
- // Print loaded config
- return {
- props: {
- configName: name,
- config: JSON.parse(config),
- ...props,
- },
- };
-}
diff --git a/src/tools/types.ts b/src/tools/types.ts
deleted file mode 100644
index 71046a43397..00000000000
--- a/src/tools/types.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-import { MantineTheme } from '@mantine/core';
-
-import { OptionValues } from '../modules/ModuleTypes';
-
-export interface Settings {
- searchUrl: string;
- searchNewTab?: boolean;
- title?: string;
- logo?: string;
- favicon?: string;
- primaryColor?: MantineTheme['primaryColor'];
- secondaryColor?: MantineTheme['primaryColor'];
- primaryShade?: MantineTheme['primaryShade'];
- background?: string;
- customCSS?: string;
- appOpacity?: number;
- widgetPosition?: string;
- grow?: boolean;
- appCardWidth?: number;
-}
-
-export interface Config {
- name: string;
- services: serviceItem[];
- settings: Settings;
- modules: {
- [key: string]: ConfigModule;
- };
-}
-
-interface ConfigModule {
- title: string;
- enabled: boolean;
- options: {
- [key: string]: OptionValues;
- };
-}
-
-export const Targets = [
- { value: '_blank', label: 'New Tab' },
- { value: '_top', label: 'Same Window' },
-];
-
-export const ServiceTypeList = [
- 'Other',
- 'Dash.',
- 'Deluge',
- 'Emby',
- 'Lidarr',
- 'Plex',
- 'qBittorrent',
- 'Radarr',
- 'Readarr',
- 'Sonarr',
- 'Transmission',
- 'Overseerr',
- 'Jellyseerr',
- 'Sabnzbd',
- 'NZBGet',
-];
-export type ServiceType =
- | 'Other'
- | 'Dash.'
- | 'Deluge'
- | 'Emby'
- | 'Lidarr'
- | 'Plex'
- | 'qBittorrent'
- | 'Radarr'
- | 'Readarr'
- | 'Sonarr'
- | 'Overseerr'
- | 'Jellyseerr'
- | 'Transmission'
- | 'Sabnzbd'
- | 'NZBGet';
-
-/**
- * @deprecated
- * @param name the name to match
- * @param form the form
- * @returns the port from the map
- */
-export function tryMatchPort(name: string | undefined, form?: any) {
- if (!name) {
- return undefined;
- }
- // Match name with portmap key
- const port = portmap.find((p) => p.name === name.toLowerCase());
- if (form && port) {
- form.setFieldValue('url', `http://localhost:${port.value}`);
- }
- return port;
-}
-
-export const portmap = [
- { name: 'qbittorrent', value: '8080' },
- { name: 'sonarr', value: '8989' },
- { name: 'radarr', value: '7878' },
- { name: 'lidarr', value: '8686' },
- { name: 'readarr', value: '8787' },
- { name: 'deluge', value: '8112' },
- { name: 'transmission', value: '9091' },
- { name: 'plex', value: '32400' },
- { name: 'emby', value: '8096' },
- { name: 'overseerr', value: '5055' },
- { name: 'dash.', value: '3001' },
- { name: 'sabnzbd', value: '8080' },
- { name: 'nzbget', value: '6789' },
-];
-
-//TODO: Fix this to be used in the docker add to homarr button
-export const MatchingImages: {
- image: string;
- type: ServiceType;
-}[] = [
- //Official images
- { image: 'mauricenino/dashdot', type: 'Dash.' },
- { image: 'emby/embyserver', type: 'Emby' },
- { image: 'plexinc/pms-docker', type: 'Plex' },
- //Lidarr images
- { image: 'hotio/lidarr', type: 'Lidarr' },
- { image: 'ghcr.io/hotio/lidarr', type: 'Lidarr' },
- { image: 'cr.hotio.dev/hotio/lidarr', type: 'Lidarr' },
- // Plex
- { image: 'hotio/plex', type: 'Plex' },
- { image: 'ghcr.io/hotio/plex', type: 'Plex' },
- { image: 'cr.hotio.dev/hotio/plex', type: 'Plex' },
- // qbittorrent
- { image: 'hotio/qbittorrent', type: 'qBittorrent' },
- { image: 'ghcr.io/hotio/qbittorrent', type: 'qBittorrent' },
- { image: 'cr.hotio.dev/hotio/qbittorrent', type: 'qBittorrent' },
- // Radarr
- { image: 'hotio/radarr', type: 'Radarr' },
- { image: 'ghcr.io/hotio/radarr', type: 'Radarr' },
- { image: 'cr.hotio.dev/hotio/radarr', type: 'Radarr' },
- // Readarr
- { image: 'hotio/readarr', type: 'Readarr' },
- { image: 'ghcr.io/hotio/readarr', type: 'Readarr' },
- { image: 'cr.hotio.dev/hotio/readarr', type: 'Readarr' },
- // Sonarr
- { image: 'hotio/sonarr', type: 'Sonarr' },
- { image: 'ghcr.io/hotio/sonarr', type: 'Sonarr' },
- { image: 'cr.hotio.dev/hotio/sonarr', type: 'Sonarr' },
- //LinuxServer images
- { image: 'lscr.io/linuxserver/deluge', type: 'Deluge' },
- { image: 'lscr.io/linuxserver/emby', type: 'Emby' },
- { image: 'lscr.io/linuxserver/lidarr', type: 'Lidarr' },
- { image: 'lscr.io/linuxserver/plex', type: 'Plex' },
- { image: 'lscr.io/linuxserver/qbittorrent', type: 'qBittorrent' },
- { image: 'lscr.io/linuxserver/radarr', type: 'Radarr' },
- { image: 'lscr.io/linuxserver/readarr', type: 'Readarr' },
- { image: 'lscr.io/linuxserver/sonarr', type: 'Sonarr' },
- { image: 'lscr.io/linuxserver/transmission', type: 'Transmission' },
- // LinuxServer but on Docker Hub
- { image: 'linuxserver/deluge', type: 'Deluge' },
- { image: 'linuxserver/emby', type: 'Emby' },
- { image: 'linuxserver/lidarr', type: 'Lidarr' },
- { image: 'linuxserver/plex', type: 'Plex' },
- { image: 'linuxserver/qbittorrent', type: 'qBittorrent' },
- { image: 'linuxserver/radarr', type: 'Radarr' },
- { image: 'linuxserver/readarr', type: 'Readarr' },
- { image: 'linuxserver/sonarr', type: 'Sonarr' },
- { image: 'linuxserver/transmission', type: 'Transmission' },
- //High usage
- { image: 'markusmcnugen/qbittorrentvpn', type: 'qBittorrent' },
- { image: 'haugene/transmission-openvpn', type: 'Transmission' },
-];
-
-export interface serviceItem {
- id: string;
- name: string;
- type: ServiceType;
- url: string;
- icon: string;
- category?: string;
- apiKey?: string;
- password?: string;
- username?: string;
- openedUrl?: string;
- newTab?: boolean;
- ping?: boolean;
- status?: string[];
-}
diff --git a/src/types/app.ts b/src/types/app.ts
index ce5f6f5b8c3..6438968b451 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -23,6 +23,9 @@ interface AppBehaviourType {
interface AppNetworkType {
enabledStatusChecker: boolean;
+ /**
+ * @deprecated replaced by statusCodes
+ */
okStatus?: number[];
statusCodes: string[];
}
diff --git a/src/hooks/widgets/dashDot/api.ts b/src/widgets/dashDot/api.ts
similarity index 83%
rename from src/hooks/widgets/dashDot/api.ts
rename to src/widgets/dashDot/api.ts
index 6ede6e2b493..aac79be9bf0 100644
--- a/src/hooks/widgets/dashDot/api.ts
+++ b/src/widgets/dashDot/api.ts
@@ -1,11 +1,6 @@
import { useConfigContext } from '~/config/provider';
import { RouterInputs, api } from '~/utils/api';
-
-import { UsenetInfoRequestParams } from '../../../pages/api/modules/usenet';
-import type { UsenetHistoryRequestParams } from '../../../pages/api/modules/usenet/history';
-import { UsenetPauseRequestParams } from '../../../pages/api/modules/usenet/pause';
-import type { UsenetQueueRequestParams } from '../../../pages/api/modules/usenet/queue';
-import { UsenetResumeRequestParams } from '../../../pages/api/modules/usenet/resume';
+import { UsenetHistoryRequestParams, UsenetInfoRequestParams, UsenetPauseRequestParams, UsenetQueueRequestParams, UsenetResumeRequestParams } from '../useNet/types';
const POLLING_INTERVAL = 2000;
diff --git a/src/widgets/download-speed/Tile.tsx b/src/widgets/download-speed/Tile.tsx
index 71e9ffd02b2..83d568e18dd 100644
--- a/src/widgets/download-speed/Tile.tsx
+++ b/src/widgets/download-speed/Tile.tsx
@@ -18,7 +18,7 @@ import { useEffect } from 'react';
import { AppAvatar } from '../../components/AppAvatar';
import { useConfigContext } from '../../config/provider';
-import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
+import { useGetDownloadClientsQueue } from './useGetNetworkSpeed';
import { useColorTheme } from '../../tools/color';
import { humanFileSize } from '../../tools/humanFileSize';
import {
diff --git a/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx b/src/widgets/download-speed/useGetNetworkSpeed.tsx
similarity index 100%
rename from src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx
rename to src/widgets/download-speed/useGetNetworkSpeed.tsx
diff --git a/src/widgets/media-server/MediaServerTile.tsx b/src/widgets/media-server/MediaServerTile.tsx
index a5451834235..c66d2f2e788 100644
--- a/src/widgets/media-server/MediaServerTile.tsx
+++ b/src/widgets/media-server/MediaServerTile.tsx
@@ -15,7 +15,7 @@ import { useTranslation } from 'next-i18next';
import { AppAvatar } from '../../components/AppAvatar';
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
import { useConfigContext } from '../../config/provider';
-import { useGetMediaServers } from '../../hooks/widgets/media-servers/useGetMediaServers';
+import { useGetMediaServers } from './useGetMediaServers';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { TableRow } from './TableRow';
diff --git a/src/hooks/widgets/media-servers/useGetMediaServers.tsx b/src/widgets/media-server/useGetMediaServers.tsx
similarity index 100%
rename from src/hooks/widgets/media-servers/useGetMediaServers.tsx
rename to src/widgets/media-server/useGetMediaServers.tsx
diff --git a/src/widgets/torrent/TorrentTile.tsx b/src/widgets/torrent/TorrentTile.tsx
index c34f6668e24..27b3ab02645 100644
--- a/src/widgets/torrent/TorrentTile.tsx
+++ b/src/widgets/torrent/TorrentTile.tsx
@@ -19,7 +19,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import { useTranslation } from 'next-i18next';
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
-import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
+import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
import { AppIntegrationType } from '../../types/app';
import { defineWidget } from '../helper';
diff --git a/src/widgets/useNet/UseNetTile.tsx b/src/widgets/useNet/UseNetTile.tsx
index bc28de4d935..fdc04f54199 100644
--- a/src/widgets/useNet/UseNetTile.tsx
+++ b/src/widgets/useNet/UseNetTile.tsx
@@ -12,7 +12,7 @@ import {
useGetUsenetInfo,
usePauseUsenetQueueMutation,
useResumeUsenetQueueMutation,
-} from '../../hooks/widgets/dashDot/api';
+} from '../dashDot/api';
import { humanFileSize } from '../../tools/humanFileSize';
import { AppIntegrationType } from '../../types/app';
import { defineWidget } from '../helper';
diff --git a/src/widgets/useNet/UsenetHistoryList.tsx b/src/widgets/useNet/UsenetHistoryList.tsx
index 7123a9a8f73..2db65f03338 100644
--- a/src/widgets/useNet/UsenetHistoryList.tsx
+++ b/src/widgets/useNet/UsenetHistoryList.tsx
@@ -18,7 +18,7 @@ import duration from 'dayjs/plugin/duration';
import { useTranslation } from 'next-i18next';
import { FunctionComponent, useState } from 'react';
-import { useGetUsenetHistory } from '../../hooks/widgets/dashDot/api';
+import { useGetUsenetHistory } from '../dashDot/api';
import { parseDuration } from '../../tools/client/parseDuration';
import { humanFileSize } from '../../tools/humanFileSize';
diff --git a/src/widgets/useNet/UsenetQueueList.tsx b/src/widgets/useNet/UsenetQueueList.tsx
index b3e39e61bde..f034d818be5 100644
--- a/src/widgets/useNet/UsenetQueueList.tsx
+++ b/src/widgets/useNet/UsenetQueueList.tsx
@@ -21,7 +21,7 @@ import duration from 'dayjs/plugin/duration';
import { useTranslation } from 'next-i18next';
import { FunctionComponent, useState } from 'react';
-import { useGetUsenetDownloads } from '../../hooks/widgets/dashDot/api';
+import { useGetUsenetDownloads } from '../dashDot/api';
import { humanFileSize } from '../../tools/humanFileSize';
dayjs.extend(duration);
diff --git a/src/widgets/useNet/types.ts b/src/widgets/useNet/types.ts
index a4dd8c5a072..cafc97058da 100644
--- a/src/widgets/useNet/types.ts
+++ b/src/widgets/useNet/types.ts
@@ -18,3 +18,45 @@ export interface UsenetHistoryItem {
id: string;
time: number;
}
+
+export interface UsenetHistoryRequestParams {
+ appId: string;
+ offset: number;
+ limit: number;
+}
+
+export interface UsenetHistoryResponse {
+ items: UsenetHistoryItem[];
+ total: number;
+}
+
+export interface UsenetInfoRequestParams {
+ appId: string;
+}
+
+export interface UsenetInfoResponse {
+ paused: boolean;
+ sizeLeft: number;
+ speed: number;
+ eta: number;
+}
+
+export interface UsenetPauseRequestParams {
+ appId: string;
+}
+
+export interface UsenetQueueRequestParams {
+ appId: string;
+ offset: number;
+ limit: number;
+}
+
+export interface UsenetQueueResponse {
+ items: UsenetQueueItem[];
+ total: number;
+}
+
+export interface UsenetResumeRequestParams {
+ appId: string;
+ nzbId?: string;
+}