diff --git a/src/components/modules/downloads/TotalDownloadsModule.tsx b/src/components/modules/downloads/TotalDownloadsModule.tsx new file mode 100644 index 000000000..4c40cc5ab --- /dev/null +++ b/src/components/modules/downloads/TotalDownloadsModule.tsx @@ -0,0 +1,212 @@ +import { Text, Title, Group, useMantineTheme, Box, Card, ColorSwatch } from '@mantine/core'; +import { Download } from 'tabler-icons-react'; +import { useEffect, useState } from 'react'; +import axios from 'axios'; +import { NormalizedTorrent } from '@ctrl/shared-torrent'; +import { linearGradientDef } from '@nivo/core'; +import { Datum, ResponsiveLine } from '@nivo/line'; +import { useListState } from '@mantine/hooks'; +import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem'; +import { useConfig } from '../../../tools/state'; +import { IModule } from '../modules'; + +/** + * Format bytes as human-readable text. + * + * @param bytes Number of bytes. + * @param si True to use metric (SI) units, aka powers of 1000. False to use + * binary (IEC), aka powers of 1024. + * @param dp Number of decimal places to display. + * + * @return Formatted string. + */ +function humanFileSize(initialBytes: number, si = true, dp = 1) { + const thresh = si ? 1000 : 1024; + let bytes = initialBytes; + + if (Math.abs(bytes) < thresh) { + return `${bytes} B`; + } + + const units = si + ? ['kb', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + const r = 10 ** dp; + + do { + bytes /= thresh; + u += 1; + } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); + + return `${bytes.toFixed(dp)} ${units[u]}`; +} + +export const TotalDownloadsModule: IModule = { + title: 'Download speed', + description: 'Show the current download speed of supported services', + icon: Download, + component: TotalDownloadsComponent, +}; + +interface torrentHistory { + x: number; + up: number; + down: number; +} + +export default function TotalDownloadsComponent() { + const { config } = useConfig(); + const qBittorrentService = config.services + .filter((service) => service.type === 'qBittorrent') + .at(0); + const delugeService = config.services.filter((service) => service.type === 'Deluge').at(0); + + const [delugeTorrents, setDelugeTorrents] = useState([]); + const [torrentHistory, torrentHistoryHandlers] = useListState([]); + const [qBittorrentTorrents, setqBittorrentTorrents] = useState([]); + + const torrents: NormalizedTorrent[] = []; + delugeTorrents.forEach((delugeTorrent) => + torrents.push({ ...delugeTorrent, progress: delugeTorrent.progress / 100 }) + ); + qBittorrentTorrents.forEach((torrent) => torrents.push(torrent)); + + const totalDownloadSpeed = torrents.reduce((acc, torrent) => acc + torrent.downloadSpeed, 0); + const totalUploadSpeed = torrents.reduce((acc, torrent) => acc + torrent.uploadSpeed, 0); + + useEffect(() => { + const interval = setInterval(() => { + // Get the current download speed of qBittorrent. + if (qBittorrentService) { + axios + .post('/api/modules/downloads?dlclient=qbit', { ...qBittorrentService }) + .then((res) => { + setqBittorrentTorrents(res.data.torrents); + }); + if (delugeService) { + axios.post('/api/modules/downloads?dlclient=deluge', { ...delugeService }).then((res) => { + setDelugeTorrents(res.data.torrents); + }); + } + } + }, 1000); + }, [config.modules]); + + useEffect(() => { + torrentHistoryHandlers.append({ + x: Date.now(), + down: totalDownloadSpeed, + up: totalUploadSpeed, + }); + }, [totalDownloadSpeed, totalUploadSpeed]); + + if (!qBittorrentService && !delugeService) { + return ( + + Critical: No qBittorrent/Deluge instance found in services. + + Add a qBittorrent/Deluge service to view current downloads + + + + ); + } + + const theme = useMantineTheme(); + // Load the last 10 values from the history + const history = torrentHistory.slice(-10); + console.log(history); + 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 ( + + Current download speed + + + + Download: {humanFileSize(totalDownloadSpeed)}/s + + + + Upload: {humanFileSize(totalUploadSpeed)}/s + + + + { + 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 ( + + {roundedSeconds} seconds ago + + + + + Download: {humanFileSize(Download)} + + + + Upload: {humanFileSize(Upload)} + + + + + ); + }} + 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={[ + // Blue + theme.colors.blue[5], + // Green + theme.colors.green[5], + ]} + /> + + + ); +}