mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
♻️ Refactor torrent network traffic widget #616
This commit is contained in:
11
src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx
Normal file
11
src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { NormalizedDownloadQueueResponse } from '../../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
|
||||||
|
export const useGetDownloadClientsQueue = () => useQuery({
|
||||||
|
queryKey: ['network-speed'],
|
||||||
|
queryFn: async (): Promise<NormalizedDownloadQueueResponse> => {
|
||||||
|
const response = await fetch('/api/modules/downloads');
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
refetchInterval: 3000,
|
||||||
|
});
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Query, useQuery } from '@tanstack/react-query';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { NormalizedTorrentListResponse } from '../../../types/api/NormalizedTorrentListResponse';
|
|
||||||
|
|
||||||
interface TorrentsDataRequestParams {
|
|
||||||
appId: string;
|
|
||||||
refreshInterval: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGetTorrentData = (params: TorrentsDataRequestParams) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: ['torrentsData', params.appId],
|
|
||||||
queryFn: fetchData,
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
refetchInterval(_: any, query: Query) {
|
|
||||||
if (query.state.fetchFailureCount < 3) {
|
|
||||||
return params.refreshInterval;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
enabled: !!params.appId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchData = async (): Promise<NormalizedTorrentListResponse> => {
|
|
||||||
const response = await axios.post('/api/modules/torrents');
|
|
||||||
return response.data as NormalizedTorrentListResponse;
|
|
||||||
};
|
|
||||||
191
src/pages/api/modules/downloads/index.ts
Normal file
191
src/pages/api/modules/downloads/index.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { Deluge } from '@ctrl/deluge';
|
||||||
|
import { AllClientData } from '@ctrl/shared-torrent';
|
||||||
|
import { getCookie } from 'cookies-next';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { Client } from 'sabnzbd-api';
|
||||||
|
import { getConfig } from '../../../../tools/config/getConfig';
|
||||||
|
import {
|
||||||
|
NormalizedDownloadAppStat,
|
||||||
|
NormalizedDownloadQueueResponse,
|
||||||
|
} from '../../../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
import { ConfigAppType, IntegrationField } from '../../../../types/app';
|
||||||
|
import { UsenetQueueItem } from '../../../../widgets/useNet/types';
|
||||||
|
import { NzbgetClient } from '../usenet/nzbget/nzbget-client';
|
||||||
|
import { NzbgetQueueItem, NzbgetStatus } from '../usenet/nzbget/types';
|
||||||
|
|
||||||
|
const Get = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||||
|
const configName = getCookie('config-name', { req: request });
|
||||||
|
const config = getConfig(configName?.toString() ?? 'default');
|
||||||
|
|
||||||
|
const clientData: Promise<NormalizedDownloadAppStat | undefined>[] = config.apps.map((app) =>
|
||||||
|
GetDataFromClient(app)
|
||||||
|
);
|
||||||
|
|
||||||
|
const settledPromises = await Promise.allSettled(clientData);
|
||||||
|
|
||||||
|
const data: NormalizedDownloadAppStat[] = settledPromises
|
||||||
|
.filter((x) => x.status === 'fulfilled')
|
||||||
|
.map((promise) => (promise as PromiseFulfilledResult<NormalizedDownloadAppStat>).value)
|
||||||
|
.filter((x) => x !== undefined);
|
||||||
|
|
||||||
|
const responseBody = { apps: data } as NormalizedDownloadQueueResponse;
|
||||||
|
|
||||||
|
return response.status(200).json(responseBody);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GetDataFromClient = async (
|
||||||
|
app: ConfigAppType
|
||||||
|
): Promise<NormalizedDownloadAppStat | undefined> => {
|
||||||
|
const reduceTorrent = (data: AllClientData): NormalizedDownloadAppStat => ({
|
||||||
|
type: 'torrent',
|
||||||
|
appId: app.id,
|
||||||
|
success: true,
|
||||||
|
torrents: data.torrents,
|
||||||
|
totalDownload: data.torrents
|
||||||
|
.map((torrent) => torrent.downloadSpeed)
|
||||||
|
.reduce((acc, torrent) => acc + torrent),
|
||||||
|
totalUpload: data.torrents
|
||||||
|
.map((torrent) => torrent.uploadSpeed)
|
||||||
|
.reduce((acc, torrent) => acc + torrent),
|
||||||
|
});
|
||||||
|
|
||||||
|
const findField = (app: ConfigAppType, field: IntegrationField) =>
|
||||||
|
app.integration?.properties.find((x) => x.field === field)?.value ?? undefined;
|
||||||
|
|
||||||
|
switch (app.integration?.type) {
|
||||||
|
case 'deluge': {
|
||||||
|
return reduceTorrent(
|
||||||
|
await new Deluge({
|
||||||
|
baseUrl: app.url,
|
||||||
|
password: findField(app, 'password'),
|
||||||
|
}).getAllData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'transmission': {
|
||||||
|
return reduceTorrent(
|
||||||
|
await new Deluge({
|
||||||
|
baseUrl: app.url,
|
||||||
|
username: findField(app, 'username'),
|
||||||
|
password: findField(app, 'password'),
|
||||||
|
}).getAllData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'qBittorrent': {
|
||||||
|
return reduceTorrent(
|
||||||
|
await new Deluge({
|
||||||
|
baseUrl: app.url,
|
||||||
|
username: findField(app, 'username'),
|
||||||
|
password: findField(app, 'password'),
|
||||||
|
}).getAllData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'sabnzbd': {
|
||||||
|
const { origin } = new URL(app.url);
|
||||||
|
const client = new Client(origin, findField(app, 'apiKey') ?? '');
|
||||||
|
const queue = await client.queue();
|
||||||
|
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
||||||
|
const [hours, minutes, seconds] = slot.timeleft.split(':');
|
||||||
|
const eta = dayjs.duration({
|
||||||
|
hour: parseInt(hours, 10),
|
||||||
|
minutes: parseInt(minutes, 10),
|
||||||
|
seconds: parseInt(seconds, 10),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: slot.nzo_id,
|
||||||
|
eta: eta.asSeconds(),
|
||||||
|
name: slot.filename,
|
||||||
|
progress: parseFloat(slot.percentage),
|
||||||
|
size: parseFloat(slot.mb) * 1000 * 1000,
|
||||||
|
state: slot.status.toLowerCase() as any,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const killobitsPerSecond = Number(queue.kbpersec);
|
||||||
|
const bytesPerSecond = killobitsPerSecond * 1024; // convert killobytes to bytes
|
||||||
|
return {
|
||||||
|
type: 'usenet',
|
||||||
|
appId: app.id,
|
||||||
|
totalDownload: bytesPerSecond,
|
||||||
|
nzbs: items,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'nzbGet': {
|
||||||
|
const url = new URL(app.url);
|
||||||
|
const options = {
|
||||||
|
host: url.hostname,
|
||||||
|
port: url.port,
|
||||||
|
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
||||||
|
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nzbGet = NzbgetClient(options);
|
||||||
|
const nzbgetQueue: NzbgetQueueItem[] = await new Promise((resolve, reject) => {
|
||||||
|
nzbGet.listGroups((err: any, result: NzbgetQueueItem[]) => {
|
||||||
|
if (!err) {
|
||||||
|
resolve(result);
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!nzbgetQueue) {
|
||||||
|
throw new Error('Error while getting NZBGet queue');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nzbgetStatus: NzbgetStatus = await new Promise((resolve, reject) => {
|
||||||
|
nzbGet.status((err: any, result: NzbgetStatus) => {
|
||||||
|
if (!err) {
|
||||||
|
resolve(result);
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!nzbgetStatus) {
|
||||||
|
throw new Error('Error while getting NZBGet status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nzbgetItems: UsenetQueueItem[] = nzbgetQueue.map((item: NzbgetQueueItem) => ({
|
||||||
|
id: item.NZBID.toString(),
|
||||||
|
name: item.NZBName,
|
||||||
|
progress: (item.DownloadedSizeMB / item.FileSizeMB) * 100,
|
||||||
|
eta: (item.RemainingSizeMB * 1000000) / nzbgetStatus.DownloadRate,
|
||||||
|
// Multiple MB to get bytes
|
||||||
|
size: item.FileSizeMB * 1000 * 1000,
|
||||||
|
state: getNzbgetState(item.Status),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'usenet',
|
||||||
|
appId: app.id,
|
||||||
|
nzbs: nzbgetItems,
|
||||||
|
success: true,
|
||||||
|
totalDownload: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (request: NextApiRequest, response: NextApiResponse) => {
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
return Get(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.status(405);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNzbgetState(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case 'QUEUED':
|
||||||
|
return 'queued';
|
||||||
|
case 'PAUSED ':
|
||||||
|
return 'paused';
|
||||||
|
default:
|
||||||
|
return 'downloading';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import { Deluge } from '@ctrl/deluge';
|
|
||||||
import { QBittorrent } from '@ctrl/qbittorrent';
|
|
||||||
import { AllClientData } from '@ctrl/shared-torrent';
|
|
||||||
import { Transmission } from '@ctrl/transmission';
|
|
||||||
import Consola from 'consola';
|
|
||||||
import { getCookie } from 'cookies-next';
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { getConfig } from '../../../tools/config/getConfig';
|
|
||||||
import { NormalizedTorrentListResponse } from '../../../types/api/NormalizedTorrentListResponse';
|
|
||||||
import { ConfigAppType, IntegrationType } from '../../../types/app';
|
|
||||||
|
|
||||||
const supportedTypes: IntegrationType[] = ['deluge', 'qBittorrent', 'transmission'];
|
|
||||||
|
|
||||||
async function Post(req: NextApiRequest, res: NextApiResponse<NormalizedTorrentListResponse>) {
|
|
||||||
// Get the type of app from the request url
|
|
||||||
const configName = getCookie('config-name', { req });
|
|
||||||
const config = getConfig(configName?.toString() ?? 'default');
|
|
||||||
|
|
||||||
const clientApps = config.apps.filter(
|
|
||||||
(app) =>
|
|
||||||
app.integration && app.integration.type && supportedTypes.includes(app.integration.type)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (clientApps.length < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
allSuccess: false,
|
|
||||||
missingDownloadClients: true,
|
|
||||||
labels: [],
|
|
||||||
torrents: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const promiseList: Promise<ConcatenatedClientData>[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < clientApps.length; i += 1) {
|
|
||||||
const app = clientApps[i];
|
|
||||||
const getAllData = getAllDataForClient(app);
|
|
||||||
if (!getAllData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const concatenatedPromise = async (): Promise<ConcatenatedClientData> => ({
|
|
||||||
clientData: await getAllData,
|
|
||||||
appId: app.id,
|
|
||||||
});
|
|
||||||
promiseList.push(concatenatedPromise());
|
|
||||||
}
|
|
||||||
|
|
||||||
const settledPromises = await Promise.allSettled(promiseList);
|
|
||||||
const fulfilledPromises = settledPromises.filter(
|
|
||||||
(settledPromise) => settledPromise.status === 'fulfilled'
|
|
||||||
);
|
|
||||||
|
|
||||||
const fulfilledClientData: ConcatenatedClientData[] = fulfilledPromises.map(
|
|
||||||
(fulfilledPromise) => (fulfilledPromise as PromiseFulfilledResult<ConcatenatedClientData>).value
|
|
||||||
);
|
|
||||||
|
|
||||||
const notFulfilledClientData = settledPromises
|
|
||||||
.filter((x) => x.status === 'rejected')
|
|
||||||
.map(
|
|
||||||
(fulfilledPromise) =>
|
|
||||||
(fulfilledPromise as PromiseRejectedResult)
|
|
||||||
);
|
|
||||||
|
|
||||||
notFulfilledClientData.forEach((result) => {
|
|
||||||
Consola.error(`Error while communicating with torrent download client: ${result.reason}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
Consola.info(
|
|
||||||
`Successfully fetched data from ${fulfilledPromises.length} torrent download clients`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
labels: fulfilledClientData.flatMap((clientData) => clientData.clientData.labels),
|
|
||||||
torrents: fulfilledClientData.map((clientData) => ({
|
|
||||||
appId: clientData.appId,
|
|
||||||
torrents: clientData.clientData.torrents,
|
|
||||||
})),
|
|
||||||
allSuccess: settledPromises.length === fulfilledPromises.length,
|
|
||||||
missingDownloadClients: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllDataForClient = (app: ConfigAppType) => {
|
|
||||||
switch (app.integration?.type) {
|
|
||||||
case 'deluge': {
|
|
||||||
const password =
|
|
||||||
app.integration?.properties.find((x) => x.field === 'password')?.value ?? undefined;
|
|
||||||
return new Deluge({
|
|
||||||
baseUrl: app.url,
|
|
||||||
password,
|
|
||||||
}).getAllData();
|
|
||||||
}
|
|
||||||
case 'transmission': {
|
|
||||||
return new Transmission({
|
|
||||||
baseUrl: app.url,
|
|
||||||
username:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
|
||||||
password:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
|
||||||
}).getAllData();
|
|
||||||
}
|
|
||||||
case 'qBittorrent': {
|
|
||||||
return new QBittorrent({
|
|
||||||
baseUrl: app.url,
|
|
||||||
username:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'username')?.value ?? undefined,
|
|
||||||
password:
|
|
||||||
app.integration!.properties.find((x) => x.field === 'password')?.value ?? undefined,
|
|
||||||
}).getAllData();
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
Consola.error(`unable to find torrent client of type '${app.integration?.type}'`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConcatenatedClientData = {
|
|
||||||
appId: string;
|
|
||||||
clientData: AllClientData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
// Filter out if the reuqest is a POST or a GET
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
return Post(req, res);
|
|
||||||
}
|
|
||||||
return res.status(405).json({
|
|
||||||
statusCode: 405,
|
|
||||||
message: 'Method not allowed',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -9,7 +9,7 @@ import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
|
|||||||
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
||||||
import { IDateWidget } from '../../widgets/date/DateTile';
|
import { IDateWidget } from '../../widgets/date/DateTile';
|
||||||
import { ITorrent } from '../../widgets/torrent/TorrentTile';
|
import { ITorrent } from '../../widgets/torrent/TorrentTile';
|
||||||
import { ITorrentNetworkTraffic } from '../../widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile';
|
import { ITorrentNetworkTraffic } from '../../widgets/download-speed/TorrentNetworkTrafficTile';
|
||||||
import { IUsenetWidget } from '../../widgets/useNet/UseNetTile';
|
import { IUsenetWidget } from '../../widgets/useNet/UseNetTile';
|
||||||
import { IWeatherWidget } from '../../widgets/weather/WeatherTile';
|
import { IWeatherWidget } from '../../widgets/weather/WeatherTile';
|
||||||
import { IWidget } from '../../widgets/widgets';
|
import { IWidget } from '../../widgets/widgets';
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Label, NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
||||||
|
|
||||||
export type NormalizedTorrentListResponse = {
|
|
||||||
/**
|
|
||||||
* Available labels on all torrent clients
|
|
||||||
*/
|
|
||||||
labels: Label[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feteched and normalized torrents of all download clients
|
|
||||||
*/
|
|
||||||
torrents: ConcatenatedTorrentList[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicated wether all requests were a success
|
|
||||||
*/
|
|
||||||
allSuccess: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Missing download clients
|
|
||||||
*/
|
|
||||||
missingDownloadClients: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConcatenatedTorrentList = {
|
|
||||||
appId: string;
|
|
||||||
torrents: NormalizedTorrent[];
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
||||||
|
import { UsenetQueueItem } from '../../../../widgets/useNet/types';
|
||||||
|
|
||||||
|
export type NormalizedDownloadQueueResponse = {
|
||||||
|
apps: NormalizedDownloadAppStat[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NormalizedDownloadAppStat = {
|
||||||
|
success: boolean;
|
||||||
|
appId: string;
|
||||||
|
totalDownload: number;
|
||||||
|
} & (TorrentTotalDownload | UsenetTotalDownloas);
|
||||||
|
|
||||||
|
export type TorrentTotalDownload = {
|
||||||
|
type: 'torrent';
|
||||||
|
torrents: NormalizedTorrent[];
|
||||||
|
totalUpload: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UsenetTotalDownloas = {
|
||||||
|
type: 'usenet';
|
||||||
|
nzbs: UsenetQueueItem[];
|
||||||
|
};
|
||||||
294
src/widgets/download-speed/TorrentNetworkTrafficTile.tsx
Normal file
294
src/widgets/download-speed/TorrentNetworkTrafficTile.tsx
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Indicator,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useElementSize, useListState } from '@mantine/hooks';
|
||||||
|
import { linearGradientDef } from '@nivo/core';
|
||||||
|
import { Datum, ResponsiveLine, Serie } from '@nivo/line';
|
||||||
|
import { IconArrowsUpDown, IconDownload, IconUpload } from '@tabler/icons';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useConfigContext } from '../../config/provider';
|
||||||
|
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
||||||
|
import { useColorTheme } from '../../tools/color';
|
||||||
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
|
import {
|
||||||
|
NormalizedDownloadQueueResponse,
|
||||||
|
TorrentTotalDownload,
|
||||||
|
} from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
import { defineWidget } from '../helper';
|
||||||
|
import { IWidget } from '../widgets';
|
||||||
|
|
||||||
|
const definition = defineWidget({
|
||||||
|
id: 'dlspeed',
|
||||||
|
icon: IconArrowsUpDown,
|
||||||
|
options: {},
|
||||||
|
|
||||||
|
gridstack: {
|
||||||
|
minWidth: 2,
|
||||||
|
minHeight: 2,
|
||||||
|
maxWidth: 12,
|
||||||
|
maxHeight: 6,
|
||||||
|
},
|
||||||
|
component: TorrentNetworkTrafficTile,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ITorrentNetworkTraffic = IWidget<typeof definition['id'], typeof definition>;
|
||||||
|
|
||||||
|
interface TorrentNetworkTrafficTileProps {
|
||||||
|
widget: ITorrentNetworkTraffic;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) {
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
const { ref: refRoot, height: heightRoot } = useElementSize();
|
||||||
|
const { ref: refTitle, height: heightTitle } = useElementSize();
|
||||||
|
const { ref: refFooter, height: heightFooter } = useElementSize();
|
||||||
|
const { primaryColor, secondaryColor } = useColorTheme();
|
||||||
|
const { t } = useTranslation(`modules/${definition.id}`);
|
||||||
|
|
||||||
|
const [clientDataHistory, setClientDataHistory] = useListState<NormalizedDownloadQueueResponse>();
|
||||||
|
|
||||||
|
const { data, dataUpdatedAt } = useGetDownloadClientsQueue();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setClientDataHistory.append(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientDataHistory.length < 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setClientDataHistory.remove(0);
|
||||||
|
}, [dataUpdatedAt]);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 lineChartData: Serie[] = uniqueRecordedAppsOverTime.flatMap((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||||
|
|
||||||
|
const series: Serie[] = [
|
||||||
|
{
|
||||||
|
id: `download_${appId}`,
|
||||||
|
data: records.map((record, index) => ({
|
||||||
|
x: index,
|
||||||
|
y: record.totalDownload,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (records.some((x) => x.type === 'torrent')) {
|
||||||
|
const torrentRecords = records.map((record, index): Datum | null => {
|
||||||
|
if (record.type !== 'torrent') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: index,
|
||||||
|
y: record.totalUpload,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const filteredRecords = torrentRecords.filter((x) => x !== null) as Datum[];
|
||||||
|
series.push({
|
||||||
|
id: `upload_${appId}`,
|
||||||
|
data: filteredRecords,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalDownload = uniqueRecordedAppsOverTime
|
||||||
|
.map((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||||
|
const lastRecord = records.at(-1);
|
||||||
|
return lastRecord?.totalDownload ?? 0;
|
||||||
|
})
|
||||||
|
.reduce((acc, n) => acc + n, 0);
|
||||||
|
|
||||||
|
const totalUpload = uniqueRecordedAppsOverTime
|
||||||
|
.map((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId && x.type === 'torrent');
|
||||||
|
const lastRecord = records.at(-1) as TorrentTotalDownload;
|
||||||
|
return lastRecord?.totalUpload ?? 0;
|
||||||
|
})
|
||||||
|
.reduce((acc, n) => acc + n, 0);
|
||||||
|
|
||||||
|
const graphHeight = heightRoot - heightFooter - heightTitle;
|
||||||
|
|
||||||
|
const { colors } = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack ref={refRoot} style={{ height: '100%' }}>
|
||||||
|
<Group ref={refTitle}>
|
||||||
|
<IconDownload />
|
||||||
|
<Title order={4}>{t('card.lineChart.title')}</Title>
|
||||||
|
</Group>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
height: graphHeight,
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box style={{ height: '100%', width: '100%', position: 'absolute' }}>
|
||||||
|
<ResponsiveLine
|
||||||
|
isInteractive
|
||||||
|
enableSlices="x"
|
||||||
|
sliceTooltip={({ slice }) => {
|
||||||
|
const { points } = slice;
|
||||||
|
|
||||||
|
const recordsFromPoints = uniqueRecordedAppsOverTime.map((appId) => {
|
||||||
|
const records = recoredAppsOverTime.filter((x) => x.appId === appId);
|
||||||
|
const point = points.find((x) => x.id.includes(appId));
|
||||||
|
const pointIndex = Number(point?.data.x) ?? 0;
|
||||||
|
const color = point?.serieColor;
|
||||||
|
return {
|
||||||
|
record: records[pointIndex],
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card p="xs" radius="md" withBorder>
|
||||||
|
<Card.Section p="xs">
|
||||||
|
<Stack spacing="xs">
|
||||||
|
{recordsFromPoints.map((entry, index) => {
|
||||||
|
const app = config?.apps.find((x) => x.id === entry.record.appId);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group key={`download-client-tooltip-${index}`}>
|
||||||
|
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
||||||
|
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Text size="sm">{app.name}</Text>
|
||||||
|
<Group>
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconDownload opacity={0.6} size={14} />
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{humanFileSize(entry.record.totalDownload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{entry.record.type === 'torrent' && (
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconUpload opacity={0.6} size={14} />
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{humanFileSize(entry.record.totalUpload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Card.Section>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
data={lineChartData}
|
||||||
|
curve="monotoneX"
|
||||||
|
yFormat=" >-.2f"
|
||||||
|
axisLeft={null}
|
||||||
|
axisBottom={null}
|
||||||
|
axisRight={null}
|
||||||
|
enablePoints={false}
|
||||||
|
enableGridX={false}
|
||||||
|
enableGridY={false}
|
||||||
|
enableArea
|
||||||
|
defs={[
|
||||||
|
linearGradientDef('gradientA', [
|
||||||
|
{ offset: 0, color: 'inherit' },
|
||||||
|
{ offset: 100, color: 'inherit', opacity: 0 },
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
colors={lineChartData.flatMap((data) =>
|
||||||
|
data.id.toString().startsWith('upload_')
|
||||||
|
? colors[secondaryColor][5]
|
||||||
|
: colors[primaryColor][5]
|
||||||
|
)}
|
||||||
|
fill={[{ match: '*', id: 'gradientA' }]}
|
||||||
|
margin={{ bottom: 5 }}
|
||||||
|
animate={false}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group position="apart" ref={refFooter}>
|
||||||
|
<Group>
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconDownload color={colors[primaryColor][5]} opacity={0.6} size={18} />
|
||||||
|
<Text color="dimmed" size="sm">
|
||||||
|
{humanFileSize(totalDownload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group spacing="xs">
|
||||||
|
<IconUpload color={colors[secondaryColor][5]} opacity={0.6} size={18} />
|
||||||
|
<Text color="dimmed" size="sm">
|
||||||
|
{humanFileSize(totalUpload, false)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Avatar.Group>
|
||||||
|
{uniqueRecordedAppsOverTime.map((appId, index) => {
|
||||||
|
const app = config?.apps.find((x) => x.id === appId);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={app.name}
|
||||||
|
key={`download-client-app-tooltip-${index}`}
|
||||||
|
withArrow
|
||||||
|
withinPortal
|
||||||
|
>
|
||||||
|
<AppAvatar iconUrl={app.appearance.iconUrl} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Avatar.Group>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppAvatar = ({ iconUrl }: { iconUrl: string }) => {
|
||||||
|
const { colors, colorScheme } = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
src={iconUrl}
|
||||||
|
bg={colorScheme === 'dark' ? colors.gray[8] : colors.gray[2]}
|
||||||
|
size="sm"
|
||||||
|
radius="xl"
|
||||||
|
p={4}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default definition;
|
||||||
@@ -4,7 +4,7 @@ import dashdot from './dashDot/DashDotTile';
|
|||||||
import usenet from './useNet/UseNetTile';
|
import usenet from './useNet/UseNetTile';
|
||||||
import weather from './weather/WeatherTile';
|
import weather from './weather/WeatherTile';
|
||||||
import torrent from './torrent/TorrentTile';
|
import torrent from './torrent/TorrentTile';
|
||||||
import torrentNetworkTraffic from './torrentNetworkTraffic/TorrentNetworkTrafficTile';
|
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
calendar,
|
calendar,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Center,
|
Center,
|
||||||
@@ -18,10 +17,8 @@ import dayjs from 'dayjs';
|
|||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useEffect, useState } from 'react';
|
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
import { useGetTorrentData } from '../../hooks/widgets/torrents/useGetTorrentData';
|
|
||||||
import { NormalizedTorrentListResponse } from '../../types/api/NormalizedTorrentListResponse';
|
|
||||||
import { AppIntegrationType } from '../../types/app';
|
import { AppIntegrationType } from '../../types/app';
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
@@ -44,13 +41,6 @@ const definition = defineWidget({
|
|||||||
type: 'switch',
|
type: 'switch',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
refreshInterval: {
|
|
||||||
type: 'slider',
|
|
||||||
defaultValue: 10,
|
|
||||||
min: 1,
|
|
||||||
max: 60,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
gridstack: {
|
gridstack: {
|
||||||
minWidth: 2,
|
minWidth: 2,
|
||||||
@@ -72,43 +62,17 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
||||||
const { width } = useElementSize();
|
const { width } = useElementSize();
|
||||||
|
|
||||||
const { config } = useConfigContext();
|
|
||||||
const downloadApps =
|
|
||||||
config?.apps.filter((x) => x.integration && downloadAppTypes.includes(x.integration.type)) ??
|
|
||||||
[];
|
|
||||||
|
|
||||||
const [selectedAppId, setSelectedApp] = useState<string | null>(downloadApps[0]?.id);
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isError,
|
isError,
|
||||||
isInitialLoading,
|
isInitialLoading,
|
||||||
dataUpdatedAt,
|
dataUpdatedAt,
|
||||||
}: {
|
}: {
|
||||||
data: NormalizedTorrentListResponse | undefined;
|
data: NormalizedDownloadQueueResponse | undefined;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
isInitialLoading: boolean;
|
isInitialLoading: boolean;
|
||||||
dataUpdatedAt: number;
|
dataUpdatedAt: number;
|
||||||
} = useGetTorrentData({
|
} = useGetDownloadClientsQueue();
|
||||||
appId: selectedAppId!,
|
|
||||||
refreshInterval: widget.properties.refreshInterval * 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedAppId && downloadApps.length) {
|
|
||||||
setSelectedApp(downloadApps[0].id);
|
|
||||||
}
|
|
||||||
}, [downloadApps, selectedAppId]);
|
|
||||||
|
|
||||||
if (downloadApps.length === 0) {
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
|
||||||
<Group>
|
|
||||||
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
@@ -121,7 +85,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInitialLoading) {
|
if (isInitialLoading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
align="center"
|
align="center"
|
||||||
@@ -139,7 +103,18 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || Object.values(data.torrents).length < 1) {
|
if (data.apps.length === 0) {
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
|
||||||
|
<Group>
|
||||||
|
<Text>{t('card.errors.noDownloadClients.text')}</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || Object.values(data.apps).length < 1) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Title order={3}>{t('card.table.body.nothingFound')}</Title>
|
<Title order={3}>{t('card.table.body.nothingFound')}</Title>
|
||||||
@@ -147,17 +122,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = (torrent: NormalizedTorrent) => {
|
const torrents = data.apps.flatMap((app) => (app.type === 'torrent' ? app.torrents : []));
|
||||||
if (!widget.properties.displayCompletedTorrents && torrent.isCompleted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!widget.properties.displayStaleTorrents && !torrent.isCompleted && torrent.eta <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const difference = new Date().getTime() - dataUpdatedAt;
|
const difference = new Date().getTime() - dataUpdatedAt;
|
||||||
const duration = dayjs.duration(difference, 'ms');
|
const duration = dayjs.duration(difference, 'ms');
|
||||||
@@ -178,19 +143,14 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.torrents.map((concatenatedTorrentList) => {
|
{torrents.map((torrent, index) => (
|
||||||
const app = config?.apps.find((x) => x.id === concatenatedTorrentList.appId);
|
<BitTorrrentQueueItem key={index} torrent={torrent} app={undefined} />
|
||||||
return concatenatedTorrentList.torrents
|
))}
|
||||||
.filter(filter)
|
|
||||||
.map((item: NormalizedTorrent, index: number) => (
|
|
||||||
<BitTorrrentQueueItem key={index} torrent={item} app={app} />
|
|
||||||
));
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
{!data.allSuccess && (
|
{data.apps.some((x) => !x.success) && (
|
||||||
<Badge variant="dot" color="red">
|
<Badge variant="dot" color="red">
|
||||||
{t('card.footer.error')}
|
{t('card.footer.error')}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
||||||
import { Text, Title, Group, useMantineTheme, Box, Card, ColorSwatch, Stack } from '@mantine/core';
|
|
||||||
import { useListState } from '@mantine/hooks';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { linearGradientDef } from '@nivo/core';
|
|
||||||
import { Datum, ResponsiveLine } from '@nivo/line';
|
|
||||||
import { IconArrowsUpDown } from '@tabler/icons';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
|
||||||
import { useSetSafeInterval } from '../../hooks/useSetSafeInterval';
|
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
|
||||||
import { NormalizedTorrentListResponse } from '../../types/api/NormalizedTorrentListResponse';
|
|
||||||
import { defineWidget } from '../helper';
|
|
||||||
import { IWidget } from '../widgets';
|
|
||||||
|
|
||||||
const definition = defineWidget({
|
|
||||||
id: 'dlspeed',
|
|
||||||
icon: IconArrowsUpDown,
|
|
||||||
options: {},
|
|
||||||
|
|
||||||
gridstack: {
|
|
||||||
minWidth: 2,
|
|
||||||
minHeight: 2,
|
|
||||||
maxWidth: 12,
|
|
||||||
maxHeight: 6,
|
|
||||||
},
|
|
||||||
component: TorrentNetworkTrafficTile,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ITorrentNetworkTraffic = IWidget<typeof definition['id'], typeof definition>;
|
|
||||||
|
|
||||||
interface TorrentNetworkTrafficTileProps {
|
|
||||||
widget: ITorrentNetworkTraffic;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) {
|
|
||||||
const { t } = useTranslation(`modules/${definition.id}`);
|
|
||||||
const { colors } = useMantineTheme();
|
|
||||||
const setSafeInterval = useSetSafeInterval();
|
|
||||||
const { configVersion, config } = useConfigContext();
|
|
||||||
|
|
||||||
const [torrentHistory, torrentHistoryHandlers] = useListState<TorrentHistory>([]);
|
|
||||||
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
|
|
||||||
|
|
||||||
const downloadServices =
|
|
||||||
config?.apps.filter(
|
|
||||||
(app) =>
|
|
||||||
app.integration.type === 'qBittorrent' ||
|
|
||||||
app.integration.type === 'transmission' ||
|
|
||||||
app.integration.type === 'deluge'
|
|
||||||
) ?? [];
|
|
||||||
const totalDownloadSpeed = torrents.reduce((acc, torrent) => acc + torrent.downloadSpeed, 0);
|
|
||||||
const totalUploadSpeed = torrents.reduce((acc, torrent) => acc + torrent.uploadSpeed, 0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (downloadServices.length === 0) return;
|
|
||||||
const interval = setSafeInterval(() => {
|
|
||||||
// Send one request with each download service inside
|
|
||||||
axios
|
|
||||||
.post('/api/modules/torrents')
|
|
||||||
.then((response) => {
|
|
||||||
const responseData: NormalizedTorrentListResponse = response.data;
|
|
||||||
setTorrents(responseData.torrents.flatMap((x) => x.torrents));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (error.status === 401) return;
|
|
||||||
setTorrents([]);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Error while fetching torrents', error.response.data);
|
|
||||||
showNotification({
|
|
||||||
title: 'Torrent speed module failed to fetch torrents',
|
|
||||||
autoClose: 1000,
|
|
||||||
disallowClose: true,
|
|
||||||
id: 'fail-torrent-speed-module',
|
|
||||||
color: 'red',
|
|
||||||
message:
|
|
||||||
'Error fetching torrents, please check your config for any potential errors, check the console for more info',
|
|
||||||
});
|
|
||||||
clearInterval(interval);
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}, [configVersion]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
torrentHistoryHandlers.append({
|
|
||||||
x: Date.now(),
|
|
||||||
down: totalDownloadSpeed,
|
|
||||||
up: totalUploadSpeed,
|
|
||||||
});
|
|
||||||
}, [totalDownloadSpeed, totalUploadSpeed]);
|
|
||||||
|
|
||||||
const history = torrentHistory.slice(-10);
|
|
||||||
const chartDataUp = history.map((load, i) => ({
|
|
||||||
x: load.x,
|
|
||||||
y: load.up,
|
|
||||||
})) as Datum[];
|
|
||||||
const chartDataDown = history.map((load, i) => ({
|
|
||||||
x: load.x,
|
|
||||||
y: load.down,
|
|
||||||
})) as Datum[];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Title order={4}>{t('card.lineChart.title')}</Title>
|
|
||||||
<Stack>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={12} color={colors.green[5]} />
|
|
||||||
<Text>
|
|
||||||
{t('card.lineChart.totalDownload', { download: humanFileSize(totalDownloadSpeed) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={12} color={colors.blue[5]} />
|
|
||||||
<Text>
|
|
||||||
{t('card.lineChart.totalUpload', { upload: humanFileSize(totalUploadSpeed) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
height: 200,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ResponsiveLine
|
|
||||||
isInteractive
|
|
||||||
enableSlices="x"
|
|
||||||
sliceTooltip={({ slice }) => {
|
|
||||||
const Download = slice.points[0].data.y as number;
|
|
||||||
const Upload = slice.points[1].data.y as number;
|
|
||||||
// Get the number of seconds since the last update.
|
|
||||||
const seconds = (Date.now() - (slice.points[0].data.x as number)) / 1000;
|
|
||||||
// Round to the nearest second.
|
|
||||||
const roundedSeconds = Math.round(seconds);
|
|
||||||
return (
|
|
||||||
<Card p="sm" radius="md" withBorder>
|
|
||||||
<Text size="md">{t('card.lineChart.timeSpan', { seconds: roundedSeconds })}</Text>
|
|
||||||
<Card.Section p="sm">
|
|
||||||
<Stack>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={10} color={colors.green[5]} />
|
|
||||||
<Text size="md">
|
|
||||||
{t('card.lineChart.download', { download: humanFileSize(Download) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Group>
|
|
||||||
<ColorSwatch size={10} color={colors.blue[5]} />
|
|
||||||
<Text size="md">
|
|
||||||
{t('card.lineChart.upload', { upload: humanFileSize(Upload) })}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card.Section>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
id: 'downloads',
|
|
||||||
data: chartDataUp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'uploads',
|
|
||||||
data: chartDataDown,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
curve="monotoneX"
|
|
||||||
yFormat=" >-.2f"
|
|
||||||
axisTop={null}
|
|
||||||
axisRight={null}
|
|
||||||
enablePoints={false}
|
|
||||||
animate={false}
|
|
||||||
enableGridX={false}
|
|
||||||
enableGridY={false}
|
|
||||||
enableArea
|
|
||||||
defs={[
|
|
||||||
linearGradientDef('gradientA', [
|
|
||||||
{ offset: 0, color: 'inherit' },
|
|
||||||
{ offset: 100, color: 'inherit', opacity: 0 },
|
|
||||||
]),
|
|
||||||
]}
|
|
||||||
fill={[{ match: '*', id: 'gradientA' }]}
|
|
||||||
colors={[colors.blue[5], colors.green[5]]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definition;
|
|
||||||
|
|
||||||
interface TorrentHistory {
|
|
||||||
x: number;
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
|
||||||
Center,
|
Center,
|
||||||
Code,
|
Code,
|
||||||
Group,
|
Group,
|
||||||
Pagination,
|
Pagination,
|
||||||
Progress,
|
Progress,
|
||||||
ScrollArea,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
|
|||||||
Reference in New Issue
Block a user