Indexer manager (#1807)
* indexer manager widget Co-authored-by: Tagaishi <Tagaishi@hotmail.ch>
This commit is contained in:
19
public/locales/en/modules/indexer-manager.json
Normal file
19
public/locales/en/modules/indexer-manager.json
Normal 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)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,6 +163,11 @@ export const availableIntegrations = [
|
|||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
||||||
label: 'Readarr',
|
label: 'Readarr',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'prowlarr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prowlarr.png',
|
||||||
|
label: 'Prowlarr',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'jellyfin',
|
value: 'jellyfin',
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
||||||
@@ -186,6 +191,6 @@ export const availableIntegrations = [
|
|||||||
{
|
{
|
||||||
value: 'homeAssistant',
|
value: 'homeAssistant',
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
|
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[]>;
|
] as const satisfies Readonly<SelectItem[]>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createTRPCRouter } from '~/server/api/trpc';
|
import { createTRPCRouter } from '~/server/api/trpc';
|
||||||
|
|
||||||
import { appRouter } from './routers/app';
|
import { appRouter } from './routers/app';
|
||||||
import { boardRouter } from './routers/board';
|
import { boardRouter } from './routers/board';
|
||||||
import { calendarRouter } from './routers/calendar';
|
import { calendarRouter } from './routers/calendar';
|
||||||
@@ -8,6 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router';
|
|||||||
import { dockerRouter } from './routers/docker/router';
|
import { dockerRouter } from './routers/docker/router';
|
||||||
import { downloadRouter } from './routers/download';
|
import { downloadRouter } from './routers/download';
|
||||||
import { iconRouter } from './routers/icon';
|
import { iconRouter } from './routers/icon';
|
||||||
|
import { indexerManagerRouter } from './routers/indexer-manager';
|
||||||
import { inviteRouter } from './routers/invite/invite-router';
|
import { inviteRouter } from './routers/invite/invite-router';
|
||||||
import { mediaRequestsRouter } from './routers/media-request';
|
import { mediaRequestsRouter } from './routers/media-request';
|
||||||
import { mediaServerRouter } from './routers/media-server';
|
import { mediaServerRouter } from './routers/media-server';
|
||||||
@@ -30,6 +32,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
rss: rssRouter,
|
rss: rssRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
calendar: calendarRouter,
|
calendar: calendarRouter,
|
||||||
|
indexerManager: indexerManagerRouter,
|
||||||
config: configRouter,
|
config: configRouter,
|
||||||
dashDot: dashDotRouter,
|
dashDot: dashDotRouter,
|
||||||
dnsHole: dnsHoleRouter,
|
dnsHole: dnsHoleRouter,
|
||||||
@@ -45,7 +48,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
boards: boardRouter,
|
boards: boardRouter,
|
||||||
password: passwordRouter,
|
password: passwordRouter,
|
||||||
notebook: notebookRouter,
|
notebook: notebookRouter,
|
||||||
smartHomeEntityState: smartHomeEntityStateRouter
|
smartHomeEntityState: smartHomeEntityStateRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
102
src/server/api/routers/indexer-manager.ts
Normal file
102
src/server/api/routers/indexer-manager.ts
Normal 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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -22,6 +22,7 @@ export const boardNamespaces = [
|
|||||||
'modules/dashdot',
|
'modules/dashdot',
|
||||||
'modules/overseerr',
|
'modules/overseerr',
|
||||||
'modules/media-server',
|
'modules/media-server',
|
||||||
|
'modules/indexer-manager',
|
||||||
'modules/common-media-cards',
|
'modules/common-media-cards',
|
||||||
'modules/video-stream',
|
'modules/video-stream',
|
||||||
'modules/media-requests-list',
|
'modules/media-requests-list',
|
||||||
@@ -44,7 +45,7 @@ export const manageNamespaces = [
|
|||||||
'manage/users',
|
'manage/users',
|
||||||
'manage/users/invites',
|
'manage/users/invites',
|
||||||
'manage/users/create',
|
'manage/users/create',
|
||||||
'manage/users/edit'
|
'manage/users/edit',
|
||||||
];
|
];
|
||||||
export const loginNamespaces = ['authentication/login'];
|
export const loginNamespaces = ['authentication/login'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react';
|
import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react';
|
||||||
|
|
||||||
import { Property } from 'csstype';
|
import { Property } from 'csstype';
|
||||||
|
|
||||||
import { TileBaseType } from './tile';
|
import { TileBaseType } from './tile';
|
||||||
@@ -46,6 +45,7 @@ export type IntegrationType =
|
|||||||
| 'radarr'
|
| 'radarr'
|
||||||
| 'sonarr'
|
| 'sonarr'
|
||||||
| 'lidarr'
|
| 'lidarr'
|
||||||
|
| 'prowlarr'
|
||||||
| 'sabnzbd'
|
| 'sabnzbd'
|
||||||
| 'jellyseerr'
|
| 'jellyseerr'
|
||||||
| 'overseerr'
|
| 'overseerr'
|
||||||
@@ -87,6 +87,7 @@ export const integrationFieldProperties: {
|
|||||||
lidarr: ['apiKey'],
|
lidarr: ['apiKey'],
|
||||||
radarr: ['apiKey'],
|
radarr: ['apiKey'],
|
||||||
sonarr: ['apiKey'],
|
sonarr: ['apiKey'],
|
||||||
|
prowlarr: ['apiKey'],
|
||||||
sabnzbd: ['apiKey'],
|
sabnzbd: ['apiKey'],
|
||||||
readarr: ['apiKey'],
|
readarr: ['apiKey'],
|
||||||
overseerr: ['apiKey'],
|
overseerr: ['apiKey'],
|
||||||
@@ -99,7 +100,7 @@ export const integrationFieldProperties: {
|
|||||||
plex: ['apiKey'],
|
plex: ['apiKey'],
|
||||||
pihole: ['apiKey'],
|
pihole: ['apiKey'],
|
||||||
adGuardHome: ['username', 'password'],
|
adGuardHome: ['username', 'password'],
|
||||||
homeAssistant: ['apiKey']
|
homeAssistant: ['apiKey'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IntegrationFieldDefinitionType = {
|
export type IntegrationFieldDefinitionType = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import dnsHoleControls from './dnshole/DnsHoleControls';
|
|||||||
import dnsHoleSummary from './dnshole/DnsHoleSummary';
|
import dnsHoleSummary from './dnshole/DnsHoleSummary';
|
||||||
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
||||||
import iframe from './iframe/IFrameTile';
|
import iframe from './iframe/IFrameTile';
|
||||||
|
import indexerManager from './indexer-manager/IndexerManagerTile';
|
||||||
import mediaRequestsList from './media-requests/MediaRequestListTile';
|
import mediaRequestsList from './media-requests/MediaRequestListTile';
|
||||||
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
|
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
|
||||||
import mediaServer from './media-server/MediaServerTile';
|
import mediaServer from './media-server/MediaServerTile';
|
||||||
@@ -20,6 +21,7 @@ import weather from './weather/WeatherTile';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
calendar,
|
calendar,
|
||||||
|
'indexer-manager': indexerManager,
|
||||||
dashdot,
|
dashdot,
|
||||||
usenet,
|
usenet,
|
||||||
weather,
|
weather,
|
||||||
|
|||||||
98
src/widgets/indexer-manager/IndexerManagerTile.tsx
Normal file
98
src/widgets/indexer-manager/IndexerManagerTile.tsx
Normal 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;
|
||||||
Reference in New Issue
Block a user