mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-15 03:52:13 +01:00
♻️ Migrate torrent widget to board database
This commit is contained in:
@@ -2,19 +2,19 @@ import { Deluge } from '@ctrl/deluge';
|
||||
import { QBittorrent } from '@ctrl/qbittorrent';
|
||||
import { AllClientData } from '@ctrl/shared-torrent';
|
||||
import { Transmission } from '@ctrl/transmission';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import Consola from 'consola';
|
||||
import dayjs from 'dayjs';
|
||||
import { Client } from 'sabnzbd-api';
|
||||
import { z } from 'zod';
|
||||
import { NzbgetClient } from '~/server/api/routers/usenet/nzbget/nzbget-client';
|
||||
import { NzbgetQueueItem, NzbgetStatus } from '~/server/api/routers/usenet/nzbget/types';
|
||||
import { findAppProperty } from '~/tools/client/app-properties';
|
||||
import { getConfig } from '~/tools/config/getConfig';
|
||||
import { getSecret } from '~/server/db/queries/integrations';
|
||||
import { WidgetIntegration, getWidgetAsync } from '~/server/db/queries/widget';
|
||||
import {
|
||||
NormalizedDownloadAppStat,
|
||||
NormalizedDownloadQueueResponse,
|
||||
} from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||
import { ConfigAppType, IntegrationField } from '~/types/app';
|
||||
import { UsenetQueueItem } from '~/widgets/useNet/types';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
@@ -23,35 +23,51 @@ export const downloadRouter = createTRPCRouter({
|
||||
get: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
configName: z.string(),
|
||||
boardId: z.string(),
|
||||
widgetId: z.string(),
|
||||
sort: z.enum(['torrents-status', 'dlspeed']),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const config = getConfig(input.configName);
|
||||
.query(async ({ input, ctx }) => {
|
||||
const widget = await getWidgetAsync(
|
||||
input.boardId,
|
||||
input.widgetId,
|
||||
ctx.session?.user,
|
||||
input.sort
|
||||
);
|
||||
|
||||
if (!widget) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Widget not found',
|
||||
});
|
||||
}
|
||||
|
||||
const failedClients: string[] = [];
|
||||
|
||||
const clientData: Promise<NormalizedDownloadAppStat>[] = config.apps.map(async (app) => {
|
||||
try {
|
||||
const response = await GetDataFromClient(app);
|
||||
const clientData: Promise<NormalizedDownloadAppStat>[] = widget.integrations.map(
|
||||
async (integration) => {
|
||||
try {
|
||||
const response = await GetDataFromClient(integration);
|
||||
|
||||
if (!response) {
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
} as NormalizedDownloadAppStat;
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
Consola.error(
|
||||
`Error communicating with your download client '${integration.name}' (${integration.id}): ${err}`
|
||||
);
|
||||
failedClients.push(integration.id);
|
||||
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);
|
||||
|
||||
@@ -76,11 +92,11 @@ export const downloadRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
const GetDataFromClient = async (
|
||||
app: ConfigAppType
|
||||
integration: WidgetIntegration
|
||||
): Promise<NormalizedDownloadAppStat | undefined> => {
|
||||
const reduceTorrent = (data: AllClientData): NormalizedDownloadAppStat => ({
|
||||
type: 'torrent',
|
||||
appId: app.id,
|
||||
integrationId: integration.id,
|
||||
success: true,
|
||||
torrents: data.torrents,
|
||||
totalDownload: data.torrents
|
||||
@@ -91,39 +107,36 @@ const GetDataFromClient = async (
|
||||
.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) {
|
||||
switch (integration.sort) {
|
||||
case 'deluge': {
|
||||
return reduceTorrent(
|
||||
await new Deluge({
|
||||
baseUrl: app.url,
|
||||
password: findField(app, 'password'),
|
||||
baseUrl: integration.url,
|
||||
password: getSecret(integration, 'password'),
|
||||
}).getAllData()
|
||||
);
|
||||
}
|
||||
case 'transmission': {
|
||||
return reduceTorrent(
|
||||
await new Transmission({
|
||||
baseUrl: app.url,
|
||||
username: findField(app, 'username'),
|
||||
password: findField(app, 'password'),
|
||||
baseUrl: integration.url,
|
||||
username: getSecret(integration, 'username'),
|
||||
password: getSecret(integration, 'password'),
|
||||
}).getAllData()
|
||||
);
|
||||
}
|
||||
case 'qBittorrent': {
|
||||
return reduceTorrent(
|
||||
await new QBittorrent({
|
||||
baseUrl: app.url,
|
||||
username: findField(app, 'username'),
|
||||
password: findField(app, 'password'),
|
||||
baseUrl: integration.url,
|
||||
username: getSecret(integration, 'username'),
|
||||
password: getSecret(integration, 'password'),
|
||||
}).getAllData()
|
||||
);
|
||||
}
|
||||
case 'sabnzbd': {
|
||||
const { origin } = new URL(app.url);
|
||||
const client = new Client(origin, findField(app, 'apiKey') ?? '');
|
||||
const { origin } = new URL(integration.url);
|
||||
const client = new Client(origin, getSecret(integration, 'apiKey') ?? '');
|
||||
const queue = await client.queue();
|
||||
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
||||
const [hours, minutes, seconds] = slot.timeleft.split(':');
|
||||
@@ -146,19 +159,19 @@ const GetDataFromClient = async (
|
||||
const bytesPerSecond = killobitsPerSecond * 1024; // convert killobytes to bytes
|
||||
return {
|
||||
type: 'usenet',
|
||||
appId: app.id,
|
||||
integrationId: integration.id,
|
||||
totalDownload: bytesPerSecond,
|
||||
nzbs: items,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
case 'nzbGet': {
|
||||
const url = new URL(app.url);
|
||||
const url = new URL(integration.url);
|
||||
const options = {
|
||||
host: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
|
||||
login: findAppProperty(app, 'username'),
|
||||
hash: findAppProperty(app, 'password'),
|
||||
login: getSecret(integration, 'username'),
|
||||
hash: getSecret(integration, 'password'),
|
||||
};
|
||||
|
||||
const nzbGet = NzbgetClient(options);
|
||||
@@ -203,7 +216,7 @@ const GetDataFromClient = async (
|
||||
|
||||
return {
|
||||
type: 'usenet',
|
||||
appId: app.id,
|
||||
integrationId: integration.id,
|
||||
nzbs: nzbgetItems,
|
||||
success: true,
|
||||
totalDownload: nzbgetStatus.DownloadRate,
|
||||
|
||||
@@ -16,7 +16,8 @@ import { IconDownload, IconUpload } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { AppAvatar } from '~/components/AppAvatar';
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { useRequiredBoard } from '~/components/Board/context';
|
||||
import { integrationTypes } from '~/server/db/items';
|
||||
import { useColorTheme } from '~/tools/color';
|
||||
import { humanFileSize } from '~/tools/humanFileSize';
|
||||
import {
|
||||
@@ -32,7 +33,7 @@ interface TorrentNetworkTrafficTileProps {
|
||||
}
|
||||
|
||||
export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) {
|
||||
const { config } = useConfigContext();
|
||||
const { id: boardId } = useRequiredBoard();
|
||||
const { ref: refRoot, height: heightRoot } = useElementSize();
|
||||
const { ref: refTitle, height: heightTitle } = useElementSize();
|
||||
const { ref: refFooter, height: heightFooter } = useElementSize();
|
||||
@@ -41,7 +42,11 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
|
||||
const [clientDataHistory, setClientDataHistory] = useListState<NormalizedDownloadQueueResponse>();
|
||||
|
||||
const { data, dataUpdatedAt } = useGetDownloadClientsQueue();
|
||||
const { data, dataUpdatedAt } = useGetDownloadClientsQueue({
|
||||
boardId,
|
||||
widgetId: widget.id,
|
||||
sort: 'dlspeed',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -60,17 +65,14 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
|
||||
const recoredAppsOverTime = clientDataHistory.flatMap((x) => x.apps.map((app) => app));
|
||||
|
||||
// removing duplicates the "naive" way: https://stackoverflow.com/a/9229821/15257712
|
||||
const uniqueRecordedAppsOverTime = recoredAppsOverTime
|
||||
.map((x) => x.appId)
|
||||
.filter((item, position) => recoredAppsOverTime.map((y) => y.appId).indexOf(item) === position);
|
||||
const uniqueRecordedAppsOverTime = [...new Set(recoredAppsOverTime.map((x) => x.integrationId))];
|
||||
|
||||
const lineChartData: Serie[] = uniqueRecordedAppsOverTime.flatMap((appId) => {
|
||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||
const lineChartData: Serie[] = uniqueRecordedAppsOverTime.flatMap((integrationId) => {
|
||||
const records = recoredAppsOverTime.filter((x) => x.integrationId === integrationId);
|
||||
|
||||
const series: Serie[] = [
|
||||
{
|
||||
id: `download_${appId}`,
|
||||
id: `download_${integrationId}`,
|
||||
data: records.map((record, index) => ({
|
||||
x: index,
|
||||
y: record.totalDownload,
|
||||
@@ -91,7 +93,7 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
});
|
||||
const filteredRecords = torrentRecords.filter((x) => x !== null) as Datum[];
|
||||
series.push({
|
||||
id: `upload_${appId}`,
|
||||
id: `upload_${integrationId}`,
|
||||
data: filteredRecords,
|
||||
});
|
||||
}
|
||||
@@ -101,7 +103,7 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
|
||||
const totalDownload = uniqueRecordedAppsOverTime
|
||||
.map((appId) => {
|
||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||
const records = recoredAppsOverTime.filter((x) => x.integrationId === appId);
|
||||
const lastRecord = records.at(-1);
|
||||
return lastRecord?.totalDownload ?? 0;
|
||||
})
|
||||
@@ -109,7 +111,9 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
|
||||
const totalUpload = uniqueRecordedAppsOverTime
|
||||
.map((appId) => {
|
||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId && x.type === 'torrent');
|
||||
const records = recoredAppsOverTime.filter(
|
||||
(x) => x.integrationId === appId && x.type === 'torrent'
|
||||
);
|
||||
const lastRecord = records.at(-1) as TorrentTotalDownload;
|
||||
return lastRecord?.totalUpload ?? 0;
|
||||
})
|
||||
@@ -140,7 +144,7 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
const { points } = slice;
|
||||
|
||||
const recordsFromPoints = uniqueRecordedAppsOverTime.map((appId) => {
|
||||
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||
const records = recoredAppsOverTime.filter((x) => x.integrationId === appId);
|
||||
const point = points.find((x) => x.id.includes(appId));
|
||||
const pointIndex = Number(point?.data.x) ?? 0;
|
||||
const color = point?.serieColor;
|
||||
@@ -155,18 +159,20 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
<Card.Section p="xs">
|
||||
<Stack spacing="xs">
|
||||
{recordsFromPoints.map((entry, index) => {
|
||||
const app = config?.apps.find((x) => x.id === entry.record.appId);
|
||||
const current = widget.integrations.find(
|
||||
(i) => i.id === entry.record.integrationId
|
||||
);
|
||||
|
||||
if (!app) {
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group key={`download-client-tooltip-${index}`}>
|
||||
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
||||
<AppAvatar iconUrl={integrationTypes[current.sort].iconUrl} />
|
||||
|
||||
<Stack spacing={0}>
|
||||
<Text size="sm">{app.name}</Text>
|
||||
<Text size="sm">{current.name}</Text>
|
||||
<Group>
|
||||
<Group spacing="xs">
|
||||
<IconDownload opacity={0.6} size={14} />
|
||||
@@ -237,21 +243,21 @@ export default function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTraf
|
||||
</Group>
|
||||
</Group>
|
||||
<Avatar.Group>
|
||||
{uniqueRecordedAppsOverTime.map((appId, index) => {
|
||||
const app = config?.apps.find((x) => x.id === appId);
|
||||
{uniqueRecordedAppsOverTime.map((integrationId, index) => {
|
||||
const current = widget.integrations.find((i) => i.id === integrationId);
|
||||
|
||||
if (!app) {
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={app.name}
|
||||
label={current.name}
|
||||
key={`download-client-app-tooltip-${index}`}
|
||||
withArrow
|
||||
withinPortal
|
||||
>
|
||||
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
||||
<AppAvatar iconUrl={integrationTypes[current.sort].iconUrl} />
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { api } from '~/utils/api';
|
||||
import { RouterInputs, api } from '~/utils/api';
|
||||
|
||||
export const useGetDownloadClientsQueue = () => {
|
||||
const { name: configName } = useConfigContext();
|
||||
export const useGetDownloadClientsQueue = ({
|
||||
boardId,
|
||||
widgetId,
|
||||
sort,
|
||||
}: RouterInputs['download']['get']) => {
|
||||
return api.download.get.useQuery(
|
||||
{
|
||||
configName: configName!,
|
||||
boardId,
|
||||
widgetId,
|
||||
sort,
|
||||
},
|
||||
{
|
||||
refetchInterval: 3000,
|
||||
|
||||
@@ -17,6 +17,7 @@ import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequiredBoard } from '~/components/Board/context';
|
||||
import { useCardStyles } from '~/components/layout/Common/useCardStyles';
|
||||
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
|
||||
import { NormalizedDownloadQueueResponse } from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||
@@ -83,6 +84,7 @@ interface TorrentTileProps {
|
||||
}
|
||||
|
||||
function TorrentTile({ widget }: TorrentTileProps) {
|
||||
const { id: boardId } = useRequiredBoard();
|
||||
const { t } = useTranslation('modules/torrents-status');
|
||||
const { width, ref } = useElementSize();
|
||||
const { classes } = useCardStyles(true);
|
||||
@@ -97,7 +99,11 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
isError: boolean;
|
||||
isInitialLoading: boolean;
|
||||
dataUpdatedAt: number;
|
||||
} = useGetDownloadClientsQueue();
|
||||
} = useGetDownloadClientsQueue({
|
||||
boardId,
|
||||
widgetId: widget.id,
|
||||
sort: 'torrents-status',
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user