Indexer manager (#1807)

* indexer manager widget

Co-authored-by: Tagaishi <Tagaishi@hotmail.ch>
This commit is contained in:
Yossi Hillali
2024-02-09 23:35:56 +02:00
committed by GitHub
parent d45ae5fab9
commit 5d113ea280
8 changed files with 237 additions and 6 deletions

View File

@@ -0,0 +1,19 @@
{
"descriptor": {
"name": "Indexer manager status",
"description": "Status about your indexers",
"settings": {
"title": "Indexer manager status"
}
},
"indexersStatus": {
"title": "Indexer manager",
"testAllButton": "Test all"
},
"errors": {
"general": {
"title": "Unable to find a indexer manager",
"text": "There was a problem connecting to your indexer manager. Please verify your configuration/integration(s)."
}
}
}

View File

@@ -163,6 +163,11 @@ export const availableIntegrations = [
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
label: 'Readarr',
},
{
value: 'prowlarr',
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prowlarr.png',
label: 'Prowlarr',
},
{
value: 'jellyfin',
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
@@ -186,6 +191,6 @@ export const availableIntegrations = [
{
value: 'homeAssistant',
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
label: 'Home Assistant'
}
label: 'Home Assistant',
},
] as const satisfies Readonly<SelectItem[]>;

View File

@@ -1,4 +1,5 @@
import { createTRPCRouter } from '~/server/api/trpc';
import { appRouter } from './routers/app';
import { boardRouter } from './routers/board';
import { calendarRouter } from './routers/calendar';
@@ -8,6 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router';
import { dockerRouter } from './routers/docker/router';
import { downloadRouter } from './routers/download';
import { iconRouter } from './routers/icon';
import { indexerManagerRouter } from './routers/indexer-manager';
import { inviteRouter } from './routers/invite/invite-router';
import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server';
@@ -30,6 +32,7 @@ export const rootRouter = createTRPCRouter({
rss: rssRouter,
user: userRouter,
calendar: calendarRouter,
indexerManager: indexerManagerRouter,
config: configRouter,
dashDot: dashDotRouter,
dnsHole: dnsHoleRouter,
@@ -45,7 +48,7 @@ export const rootRouter = createTRPCRouter({
boards: boardRouter,
password: passwordRouter,
notebook: notebookRouter,
smartHomeEntityState: smartHomeEntityStateRouter
smartHomeEntityState: smartHomeEntityStateRouter,
});
// export type definition of API

View File

@@ -0,0 +1,102 @@
import axios from 'axios';
import Consola from 'consola';
import { z } from 'zod';
import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';
import { IntegrationType } from '~/types/app';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
export const indexerManagerRouter = createTRPCRouter({
indexers: publicProcedure
.input(
z.object({
configName: z.string(),
})
)
.query(async ({ input }) => {
const config = getConfig(input.configName);
const indexerAppIntegrationTypes = ['prowlarr'] as const satisfies readonly IntegrationType[];
const app = config.apps.find((app) =>
checkIntegrationsType(app.integration, indexerAppIntegrationTypes)
)!;
const apiKey = findAppProperty(app, 'apiKey');
if (!app || !apiKey) {
Consola.error(
`Failed to process request to indexer app (${app.id}): API key not found. Please check the configuration.`
);
}
const appUrl = new URL(app.url);
const data = await axios
.get(`${appUrl.origin}/api/v1/indexer`, {
headers: {
'X-Api-Key': apiKey,
},
})
.then((res) => res.data);
return data;
}),
statuses: publicProcedure
.input(
z.object({
configName: z.string(),
})
)
.query(async ({ input }) => {
const config = getConfig(input.configName);
const indexerAppIntegrationTypes = ['prowlarr'] as const satisfies readonly IntegrationType[];
const app = config.apps.find((app) =>
checkIntegrationsType(app.integration, indexerAppIntegrationTypes)
)!;
const apiKey = findAppProperty(app, 'apiKey');
if (!app || !apiKey) {
Consola.error(
`Failed to process request to indexer app (${app.id}): API key not found. Please check the configuration.`
);
}
const appUrl = new URL(app.url);
const data = await axios
.get(`${appUrl.origin}/api/v1/indexerstatus`, {
headers: {
'X-Api-Key': apiKey,
},
})
.then((res) => res.data);
return data;
}),
testAllIndexers: protectedProcedure
.input(
z.object({
configName: z.string(),
})
)
.mutation(async ({ input }) => {
const config = getConfig(input.configName);
const indexerAppIntegrationTypes = ['prowlarr'] as const satisfies readonly IntegrationType[];
const app = config.apps.find((app) =>
checkIntegrationsType(app.integration, indexerAppIntegrationTypes)
)!;
const apiKey = findAppProperty(app, 'apiKey');
if (!app || !apiKey) {
Consola.error(
`failed to process request to app '${app?.integration}' (${app?.id}). Please check api key`
);
}
const appUrl = new URL(app.url);
const result = await axios
.post(`${appUrl.origin}/api/v1/indexer/testall`, null, {
headers: {
'X-Api-Key': apiKey,
},
})
.then((res) => res.data)
.catch((err: any) => err.response.data);
return result;
}),
});

View File

@@ -22,6 +22,7 @@ export const boardNamespaces = [
'modules/dashdot',
'modules/overseerr',
'modules/media-server',
'modules/indexer-manager',
'modules/common-media-cards',
'modules/video-stream',
'modules/media-requests-list',
@@ -44,7 +45,7 @@ export const manageNamespaces = [
'manage/users',
'manage/users/invites',
'manage/users/create',
'manage/users/edit'
'manage/users/edit',
];
export const loginNamespaces = ['authentication/login'];

View File

@@ -1,5 +1,4 @@
import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react';
import { Property } from 'csstype';
import { TileBaseType } from './tile';
@@ -46,6 +45,7 @@ export type IntegrationType =
| 'radarr'
| 'sonarr'
| 'lidarr'
| 'prowlarr'
| 'sabnzbd'
| 'jellyseerr'
| 'overseerr'
@@ -87,6 +87,7 @@ export const integrationFieldProperties: {
lidarr: ['apiKey'],
radarr: ['apiKey'],
sonarr: ['apiKey'],
prowlarr: ['apiKey'],
sabnzbd: ['apiKey'],
readarr: ['apiKey'],
overseerr: ['apiKey'],
@@ -99,7 +100,7 @@ export const integrationFieldProperties: {
plex: ['apiKey'],
pihole: ['apiKey'],
adGuardHome: ['username', 'password'],
homeAssistant: ['apiKey']
homeAssistant: ['apiKey'],
};
export type IntegrationFieldDefinitionType = {

View File

@@ -6,6 +6,7 @@ import dnsHoleControls from './dnshole/DnsHoleControls';
import dnsHoleSummary from './dnshole/DnsHoleSummary';
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
import iframe from './iframe/IFrameTile';
import indexerManager from './indexer-manager/IndexerManagerTile';
import mediaRequestsList from './media-requests/MediaRequestListTile';
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
import mediaServer from './media-server/MediaServerTile';
@@ -20,6 +21,7 @@ import weather from './weather/WeatherTile';
export default {
calendar,
'indexer-manager': indexerManager,
dashdot,
usenet,
weather,

View File

@@ -0,0 +1,98 @@
import { Button, Card, Flex, Group, ScrollArea, Text } from '@mantine/core';
import { IconCircleCheck, IconCircleX, IconReportSearch, IconTestPipe } from '@tabler/icons-react';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
const definition = defineWidget({
id: 'indexer-manager',
icon: IconReportSearch,
options: {},
gridstack: {
minWidth: 1,
minHeight: 1,
maxWidth: 3,
maxHeight: 3,
},
component: IndexerManagerWidgetTile,
});
export type IIndexerManagerWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface IndexerManagerWidgetProps {
widget: IIndexerManagerWidget;
}
function IndexerManagerWidgetTile({ widget }: IndexerManagerWidgetProps) {
const { t } = useTranslation('modules/indexer-manager');
const { data: sessionData } = useSession();
const { name: configName } = useConfigContext();
const utils = api.useUtils();
const { isLoading: testAllLoading, mutateAsync: testAllAsync } =
api.indexerManager.testAllIndexers.useMutation({
onSuccess: async () => {
await utils.indexerManager.invalidate();
},
});
const { isInitialLoading: indexersLoading, data: indexersData } =
api.indexerManager.indexers.useQuery({
configName: configName!,
});
const { isInitialLoading: statusesLoading, data: statusesData } =
api.indexerManager.statuses.useQuery(
{
configName: configName!,
},
{
staleTime: 1000 * 60 * 2,
}
);
if (indexersLoading || !indexersData || statusesLoading) {
return <WidgetLoading />;
}
return (
<Flex h="100%" gap={0} direction="column">
<Text mt={2}>{t('indexersStatus.title')}</Text>
<Card py={5} px={10} radius="md" withBorder style={{ flex: '1' }}>
<ScrollArea h="100%">
{indexersData.map((indexer: any) => (
<Group key={indexer.id} position="apart">
<Text color="dimmed" align="center" size="xs">
{indexer.name}
</Text>
{!statusesData.find((status: any) => indexer.id === status.indexerId) &&
indexer.enable ? (
<IconCircleCheck color="#2ecc71" />
) : (
<IconCircleX color="#d9534f" />
)}
</Group>
))}
</ScrollArea>
</Card>
{sessionData && (
<Button
mt={5}
radius="md"
variant="light"
onClick={() => {
testAllAsync({ configName: configName! });
}}
loading={testAllLoading}
loaderPosition="right"
rightIcon={<IconTestPipe size={20} />}
>
{t('indexersStatus.testAllButton')}
</Button>
)}
</Flex>
);
}
export default definition;