♻️ Migrate torrent widget to board database

This commit is contained in:
Meier Lukas
2023-11-17 18:11:59 +01:00
parent 9883643789
commit 8d24e55a2e
4 changed files with 100 additions and 71 deletions

View File

@@ -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,

View File

@@ -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>
);
})}

View File

@@ -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,

View File

@@ -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 (