mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-20 14:32:18 +01:00
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',
|
||||
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[]>;
|
||||
|
||||
@@ -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
|
||||
|
||||
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/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'];
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
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