From f0976081f3ea6cba1445ba26c0fc96dff4ea89d8 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Thu, 25 Aug 2022 18:10:23 +0200 Subject: [PATCH 01/17] Initial setup of Sabnzbd integration --- package.json | 1 + src/components/AppShelf/AddAppShelfItem.tsx | 3 +- src/components/AppShelf/AppShelf.tsx | 13 +- src/modules/index.ts | 3 +- src/modules/nzb/NzbModule.tsx | 137 ++++++++++++++++++ src/modules/nzb/index.ts | 1 + .../TorrentsModule.tsx} | 47 +++--- .../TotalDownloadsModule.tsx | 75 +++++----- src/modules/{downloads => torrents}/index.ts | 2 +- src/pages/api/modules/nzbs.ts | 60 ++++++++ .../api/modules/{downloads.ts => torrents.ts} | 0 src/tools/types.ts | 14 +- yarn.lock | 71 ++++++++- 13 files changed, 346 insertions(+), 81 deletions(-) create mode 100644 src/modules/nzb/NzbModule.tsx create mode 100644 src/modules/nzb/index.ts rename src/modules/{downloads/DownloadsModule.tsx => torrents/TorrentsModule.tsx} (83%) rename src/modules/{downloads => torrents}/TotalDownloadsModule.tsx (84%) rename src/modules/{downloads => torrents}/index.ts (54%) create mode 100644 src/pages/api/modules/nzbs.ts rename src/pages/api/modules/{downloads.ts => torrents.ts} (100%) diff --git a/package.json b/package.json index e9750de18..46b61bcb0 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "prism-react-renderer": "^1.3.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "sabnzbd-api": "^1.5.0", "sharp": "^0.30.7", "systeminformation": "^5.12.1", "uuid": "^8.3.2", diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index efeba042a..fc5d3960e 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -269,7 +269,8 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & form.values.type === 'Lidarr' || form.values.type === 'Overseerr' || form.values.type === 'Jellyseerr' || - form.values.type === 'Readarr') && ( + form.values.type === 'Readarr' || + form.values.type === 'Sabnzbd') && ( <> { const { config, setConfig } = useConfig(); @@ -150,7 +150,7 @@ const AppShelf = (props: any) => { {/* Return the item for all services without category */} {noCategory && noCategory.length > 0 ? ( - {t('accordions.others.text')} + Other {getItems()} ) : null} @@ -170,8 +170,8 @@ const AppShelf = (props: any) => { ${(config.settings.appOpacity || 100) / 100}`, }} > - - + + @@ -183,7 +183,8 @@ const AppShelf = (props: any) => { return ( {getItems()} - + + ); }; diff --git a/src/modules/index.ts b/src/modules/index.ts index 88cb1ad02..c903fdbad 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -1,9 +1,10 @@ export * from './calendar'; export * from './dashdot'; export * from './date'; -export * from './downloads'; +export * from './torrents'; export * from './ping'; export * from './search'; export * from './weather'; export * from './docker'; export * from './overseerr'; +export * from './nzb'; diff --git a/src/modules/nzb/NzbModule.tsx b/src/modules/nzb/NzbModule.tsx new file mode 100644 index 000000000..31374c550 --- /dev/null +++ b/src/modules/nzb/NzbModule.tsx @@ -0,0 +1,137 @@ +import { Center, Progress, ScrollArea, Skeleton, Table, Text, Title, Tooltip } from '@mantine/core'; +import { showNotification } from '@mantine/notifications'; +import { IconDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons'; +import axios from 'axios'; +import dayjs from 'dayjs'; +import { FunctionComponent, useEffect, useState } from 'react'; +import duration from 'dayjs/plugin/duration'; +import { humanFileSize } from '../../tools/humanFileSize'; +import { DownloadItem } from '../../tools/types'; +import { IModule } from '../ModuleTypes'; + +dayjs.extend(duration); + +export const NzbComponent: FunctionComponent = () => { + const [nzbs, setNzbs] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + + const getData = async () => { + try { + const response = await axios.get('/api/modules/nzbs'); + setNzbs(response.data); + } catch (error) { + setNzbs([]); + showNotification({ + title: 'Error fetching torrents', + autoClose: 1000, + disallowClose: true, + id: 'fail-torrent-downloads-module', + color: 'red', + message: + 'Please check your config for any potential errors, check the console for more info', + }); + } finally { + setIsLoading(false); + } + }; + + const interval = setInterval(getData, 10000); + getData(); + + () => { + clearInterval(interval); + }; + }, []); + + const ths = ( + + + Name + Size + ETA + Progress + + ); + + const rows = nzbs.map((nzb) => ( + + + {nzb.state === 'paused' ? ( + + ) : ( + + )} + + + + + {nzb.name} + + + + + {humanFileSize(nzb.size * 1000 * 1000)} + + + {nzb.eta <= 0 ? ( + + Paused + + ) : ( + {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} + )} + + + {nzb.progress.toFixed(1)}% + + + + )); + + if (isLoading) { + return ( + <> + + + + + ); + } + + return ( + + {rows.length > 0 ? ( + + {ths} + {rows} +
+ ) : ( +
+ Queue is empty +
+ )} +
+ ); +}; + +export const NzbModule: IModule = { + id: 'usenet', + title: 'Usenet', + icon: IconDownload, + component: NzbComponent, +}; + +export default NzbComponent; diff --git a/src/modules/nzb/index.ts b/src/modules/nzb/index.ts new file mode 100644 index 000000000..cd8cec9fc --- /dev/null +++ b/src/modules/nzb/index.ts @@ -0,0 +1 @@ +export { NzbModule } from './NzbModule'; diff --git a/src/modules/downloads/DownloadsModule.tsx b/src/modules/torrents/TorrentsModule.tsx similarity index 83% rename from src/modules/downloads/DownloadsModule.tsx rename to src/modules/torrents/TorrentsModule.tsx index 51afc5301..04a9b6b3b 100644 --- a/src/modules/downloads/DownloadsModule.tsx +++ b/src/modules/torrents/TorrentsModule.tsx @@ -8,35 +8,33 @@ import { Skeleton, ScrollArea, Center, - Stack, } from '@mantine/core'; import { IconDownload as Download } from '@tabler/icons'; import { useEffect, useState } from 'react'; import axios from 'axios'; -import { NormalizedTorrent } from '@ctrl/shared-torrent'; import { useViewportSize } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; -import { useTranslation } from 'next-i18next'; +import { NormalizedTorrent } from '@ctrl/shared-torrent'; import { IModule } from '../ModuleTypes'; import { useConfig } from '../../tools/state'; import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem'; import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval'; import { humanFileSize } from '../../tools/humanFileSize'; -export const DownloadsModule: IModule = { +export const TorrentsModule: IModule = { + id: 'torrent', title: 'Torrent', icon: Download, - component: DownloadComponent, + component: TorrentsComponent, options: { hidecomplete: { - name: 'descriptor.settings.hideComplete', + name: 'Hide completed torrents', value: false, }, }, - id: 'torrents-status', }; -export default function DownloadComponent() { +export default function TorrentsComponent() { const { config } = useConfig(); const { height, width } = useViewportSize(); const downloadServices = @@ -44,23 +42,22 @@ export default function DownloadComponent() { (service) => service.type === 'qBittorrent' || service.type === 'Transmission' || - service.type === 'Deluge' + service.type === 'Deluge' || + service.type === 'Sabnzbd' ) ?? []; + const hideComplete: boolean = - (config?.modules?.[DownloadsModule.id]?.options?.hidecomplete?.value as boolean) ?? false; + (config?.modules?.[TorrentsModule.title]?.options?.hidecomplete?.value as boolean) ?? false; const [torrents, setTorrents] = useState([]); const setSafeInterval = useSetSafeInterval(); const [isLoading, setIsLoading] = useState(true); - - const { t } = useTranslation(`modules/${DownloadsModule.id}`); - useEffect(() => { setIsLoading(true); if (downloadServices.length === 0) return; const interval = setInterval(() => { // Send one request with each download service inside axios - .post('/api/modules/downloads') + .post('/api/modules/torrents') .then((response) => { setTorrents(response.data); setIsLoading(false); @@ -86,13 +83,13 @@ export default function DownloadComponent() { if (downloadServices.length === 0) { return ( - - {t('card.errors.noDownloadClients.title')} + + No supported download clients found! - {t('card.errors.noDownloadClients.text')} + Add a download service to view your current downloads - + ); } @@ -110,12 +107,12 @@ export default function DownloadComponent() { const DEVICE_WIDTH = 576; const ths = ( - {t('card.table.header.name')} - {t('card.table.header.size')} - {width > 576 ? {t('card.table.header.download')} : ''} - {width > 576 ? {t('card.table.header.upload')} : ''} - {t('card.table.header.estimatedTimeOfArrival')} - {t('card.table.header.progress')} + Name + Size + {width > 576 ? Down : ''} + {width > 576 ? Up : ''} + ETA + Progress ); // Convert Seconds to readable format. @@ -200,7 +197,7 @@ export default function DownloadComponent() { ) : (
- {t('card.table.body.nothingFound')} + No torrents found
)} diff --git a/src/modules/downloads/TotalDownloadsModule.tsx b/src/modules/torrents/TotalDownloadsModule.tsx similarity index 84% rename from src/modules/downloads/TotalDownloadsModule.tsx rename to src/modules/torrents/TotalDownloadsModule.tsx index 0addfd712..ade7bb870 100644 --- a/src/modules/downloads/TotalDownloadsModule.tsx +++ b/src/modules/torrents/TotalDownloadsModule.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react'; import axios from 'axios'; import { NormalizedTorrent } from '@ctrl/shared-torrent'; import { linearGradientDef } from '@nivo/core'; -import { useTranslation } from 'next-i18next'; import { Datum, ResponsiveLine } from '@nivo/line'; import { useListState } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; @@ -15,10 +14,10 @@ import { IModule } from '../ModuleTypes'; import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval'; export const TotalDownloadsModule: IModule = { + id: 'totalDownload', title: 'Download Speed', icon: Download, component: TotalDownloadsComponent, - id: 'dlspeed', }; interface torrentHistory { @@ -35,9 +34,9 @@ export default function TotalDownloadsComponent() { (service) => service.type === 'qBittorrent' || service.type === 'Transmission' || - service.type === 'Deluge' + service.type === 'Deluge' || + 'Sabnzbd' ) ?? []; - const { t } = useTranslation(`modules/${TotalDownloadsModule.id}`); const [torrentHistory, torrentHistoryHandlers] = useListState([]); const [torrents, setTorrents] = useState([]); @@ -71,30 +70,6 @@ export default function TotalDownloadsComponent() { }, 1000); }, [config.services]); - useEffect(() => { - torrentHistoryHandlers.append({ - x: Date.now(), - down: totalDownloadSpeed, - up: totalUploadSpeed, - }); - }, [totalDownloadSpeed, totalUploadSpeed]); - - if (downloadServices.length === 0) { - return ( - - {t('card.errors.noDownloadClients.title')} -
- - {t('card.errors.noDownloadClients.text')} -
-
- ); - } - const theme = useMantineTheme(); // Load the last 10 values from the history const history = torrentHistory.slice(-10); @@ -107,21 +82,41 @@ export default function TotalDownloadsComponent() { y: load.down, })) as Datum[]; + useEffect(() => { + torrentHistoryHandlers.append({ + x: Date.now(), + down: totalDownloadSpeed, + up: totalUploadSpeed, + }); + }, [totalDownloadSpeed, totalUploadSpeed]); + + if (downloadServices.length === 0) { + return ( + + No supported download clients found! +
+ + Add a download service to view your current downloads +
+
+ ); + } + return ( - {t('card.lineChart.title')} + Current download speed - - {t('card.lineChart.totalDownload', { download: humanFileSize(totalDownloadSpeed) })} - + Download: {humanFileSize(totalDownloadSpeed)}/s - - {t('card.lineChart.totalUpload', { upload: humanFileSize(totalUploadSpeed) })} - + Upload: {humanFileSize(totalUploadSpeed)}/s - {t('card.lineChart.timeSpan', { seconds: roundedSeconds })} + {roundedSeconds} seconds ago - - {t('card.lineChart.download', { download: humanFileSize(Download) })} - + Download: {humanFileSize(Download)} - - {t('card.lineChart.upload', { upload: humanFileSize(Upload) })} - + Upload: {humanFileSize(Upload)} diff --git a/src/modules/downloads/index.ts b/src/modules/torrents/index.ts similarity index 54% rename from src/modules/downloads/index.ts rename to src/modules/torrents/index.ts index f2d2c9beb..c5337e5fb 100644 --- a/src/modules/downloads/index.ts +++ b/src/modules/torrents/index.ts @@ -1,2 +1,2 @@ -export { DownloadsModule } from './DownloadsModule'; +export { TorrentsModule } from './TorrentsModule'; export { TotalDownloadsModule } from './TotalDownloadsModule'; diff --git a/src/pages/api/modules/nzbs.ts b/src/pages/api/modules/nzbs.ts new file mode 100644 index 000000000..4eb098277 --- /dev/null +++ b/src/pages/api/modules/nzbs.ts @@ -0,0 +1,60 @@ +import { getCookie } from 'cookies-next'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { Client } from 'sabnzbd-api'; +import { getConfig } from '../../../tools/getConfig'; +import { Config, DownloadItem } from '../../../tools/types'; + +dayjs.extend(duration); + +async function Get(req: NextApiRequest, res: NextApiResponse) { + try { + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const nzbServices = config.services.filter((service) => service.type === 'Sabnzbd'); + + const downloads: DownloadItem[] = []; + + await Promise.all( + nzbServices.map(async (service) => { + if (!service.apiKey) { + throw new Error(`API Key for service "${service.name}" is missing`); + } + const queue = await new Client(service.url, service.apiKey).queue(); + + queue.slots.forEach((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); + downloads.push({ + id: slot.nzo_id, + eta: eta.asSeconds(), + name: slot.filename, + progress: parseFloat(slot.percentage), + size: parseFloat(slot.mb), + state: slot.status.toLowerCase() as any, + }); + }); + }) + ); + + return res.status(200).json(downloads); + } catch (err) { + return res.status(401).json(err); + } +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Filter out if the reuqest is a POST or a GET + if (req.method === 'GET') { + return Get(req, res); + } + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); +}; diff --git a/src/pages/api/modules/downloads.ts b/src/pages/api/modules/torrents.ts similarity index 100% rename from src/pages/api/modules/downloads.ts rename to src/pages/api/modules/torrents.ts diff --git a/src/tools/types.ts b/src/tools/types.ts index e08b491e4..7eab83639 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -72,6 +72,7 @@ export const ServiceTypeList = [ 'Transmission', 'Overseerr', 'Jellyseerr', + 'Sabnzbd', ]; export type ServiceType = | 'Other' @@ -86,7 +87,8 @@ export type ServiceType = | 'Sonarr' | 'Overseerr' | 'Jellyseerr' - | 'Transmission'; + | 'Transmission' + | 'Sabnzbd'; export function tryMatchPort(name: string | undefined, form?: any) { if (!name) { @@ -112,6 +114,7 @@ export const portmap = [ { name: 'emby', value: '8096' }, { name: 'overseerr', value: '5055' }, { name: 'dash.', value: '3001' }, + { name: 'sabnzbd', value: '8080' }, ]; export const MatchingImages: { @@ -185,3 +188,12 @@ export interface serviceItem { newTab?: boolean; status?: string[]; } + +export interface DownloadItem { + name: string; + progress: number; + size: number; + id: string; + state: 'paused' | 'downloading' | 'queued'; + eta: number; +} diff --git a/yarn.lock b/yarn.lock index 1488fe927..905d3b641 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1860,7 +1860,7 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/is@npm:^4.6.0": +"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.6.0": version: 4.6.0 resolution: "@sindresorhus/is@npm:4.6.0" checksum: 83839f13da2c29d55c97abc3bc2c55b250d33a0447554997a85c539e058e57b8da092da396e252b11ec24a0279a0bed1f537fa26302209327060643e327f81d2 @@ -1885,6 +1885,15 @@ __metadata: languageName: node linkType: hard +"@szmarczak/http-timer@npm:^4.0.5": + version: 4.0.6 + resolution: "@szmarczak/http-timer@npm:4.0.6" + dependencies: + defer-to-connect: ^2.0.0 + checksum: c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^5.0.1": version: 5.0.1 resolution: "@szmarczak/http-timer@npm:5.0.1" @@ -1957,7 +1966,7 @@ __metadata: languageName: node linkType: hard -"@types/cacheable-request@npm:^6.0.2": +"@types/cacheable-request@npm:^6.0.1, @types/cacheable-request@npm:^6.0.2": version: 6.0.2 resolution: "@types/cacheable-request@npm:6.0.2" dependencies: @@ -2805,6 +2814,13 @@ __metadata: languageName: node linkType: hard +"cacheable-lookup@npm:^5.0.3": + version: 5.0.4 + resolution: "cacheable-lookup@npm:5.0.4" + checksum: 763e02cf9196bc9afccacd8c418d942fc2677f22261969a4c2c2e760fa44a2351a81557bd908291c3921fe9beb10b976ba8fa50c5ca837c5a0dd945f16468f2d + languageName: node + linkType: hard + "cacheable-lookup@npm:^6.0.4": version: 6.0.4 resolution: "cacheable-lookup@npm:6.0.4" @@ -3397,7 +3413,7 @@ __metadata: languageName: node linkType: hard -"defer-to-connect@npm:^2.0.1": +"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" checksum: 8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b @@ -4597,6 +4613,25 @@ __metadata: languageName: node linkType: hard +"got@npm:^11.8.2": + version: 11.8.5 + resolution: "got@npm:11.8.5" + dependencies: + "@sindresorhus/is": ^4.0.0 + "@szmarczak/http-timer": ^4.0.5 + "@types/cacheable-request": ^6.0.1 + "@types/responselike": ^1.0.0 + cacheable-lookup: ^5.0.3 + cacheable-request: ^7.0.2 + decompress-response: ^6.0.0 + http2-wrapper: ^1.0.0-beta.5.2 + lowercase-keys: ^2.0.0 + p-cancelable: ^2.0.0 + responselike: ^2.0.0 + checksum: 2de8a1bbda4e9b6b2b72b2d2100bc055a59adc1740529e631f61feb44a8b9a1f9f8590941ed9da9df0090b6d6d0ed8ffee94cd9ac086ec3409b392b33440f7d2 + languageName: node + linkType: hard + "got@npm:^12.1.0": version: 12.1.0 resolution: "got@npm:12.1.0" @@ -4777,6 +4812,7 @@ __metadata: prism-react-renderer: ^1.3.5 react: ^18.2.0 react-dom: ^18.2.0 + sabnzbd-api: ^1.5.0 sharp: ^0.30.7 systeminformation: ^5.12.1 typescript: ^4.7.4 @@ -4870,6 +4906,16 @@ __metadata: languageName: node linkType: hard +"http2-wrapper@npm:^1.0.0-beta.5.2": + version: 1.0.3 + resolution: "http2-wrapper@npm:1.0.3" + dependencies: + quick-lru: ^5.1.1 + resolve-alpn: ^1.0.0 + checksum: 74160b862ec699e3f859739101ff592d52ce1cb207b7950295bf7962e4aa1597ef709b4292c673bece9c9b300efad0559fc86c71b1409c7a1e02b7229456003e + languageName: node + linkType: hard + "http2-wrapper@npm:^2.1.10": version: 2.1.11 resolution: "http2-wrapper@npm:2.1.11" @@ -6603,6 +6649,13 @@ __metadata: languageName: node linkType: hard +"p-cancelable@npm:^2.0.0": + version: 2.1.1 + resolution: "p-cancelable@npm:2.1.1" + checksum: 3dba12b4fb4a1e3e34524535c7858fc82381bbbd0f247cc32dedc4018592a3950ce66b106d0880b4ec4c2d8d6576f98ca885dc1d7d0f274d1370be20e9523ddf + languageName: node + linkType: hard + "p-cancelable@npm:^3.0.0": version: 3.0.0 resolution: "p-cancelable@npm:3.0.0" @@ -7128,7 +7181,7 @@ __metadata: languageName: node linkType: hard -"resolve-alpn@npm:^1.2.0": +"resolve-alpn@npm:^1.0.0, resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" checksum: f558071fcb2c60b04054c99aebd572a2af97ef64128d59bef7ab73bd50d896a222a056de40ffc545b633d99b304c259ea9d0c06830d5c867c34f0bfa60b8eae0 @@ -7260,6 +7313,16 @@ __metadata: languageName: node linkType: hard +"sabnzbd-api@npm:^1.5.0": + version: 1.5.0 + resolution: "sabnzbd-api@npm:1.5.0" + dependencies: + form-data: ^4.0.0 + got: ^11.8.2 + checksum: e52b6978f7f4c4df1857b3be5a400182c3f494bf68f1c496bb0e56d7a629947cdd088aff9ae0cb331337574b1302ff13c7d75228761876d7f0e825c7269b54ff + languageName: node + linkType: hard + "safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" From 4afa09fd7a8a494ea2820c922babe089e73575f3 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Thu, 25 Aug 2022 18:47:06 +0200 Subject: [PATCH 02/17] Cleanup --- src/components/AppShelf/AppShelf.tsx | 4 +- src/modules/index.ts | 2 +- src/modules/nzb/index.ts | 1 - src/modules/torrents/TotalDownloadsModule.tsx | 3 +- .../NzbModule.tsx => usenet/UsenetModule.tsx} | 56 ++++++++++++------- src/modules/usenet/index.ts | 2 + src/modules/usenet/types.ts | 13 +++++ src/pages/api/modules/usenet/history.ts | 52 +++++++++++++++++ .../api/modules/{nzbs.ts => usenet/index.ts} | 7 ++- src/tools/types.ts | 9 --- 10 files changed, 112 insertions(+), 37 deletions(-) delete mode 100644 src/modules/nzb/index.ts rename src/modules/{nzb/NzbModule.tsx => usenet/UsenetModule.tsx} (71%) create mode 100644 src/modules/usenet/index.ts create mode 100644 src/modules/usenet/types.ts create mode 100644 src/pages/api/modules/usenet/history.ts rename src/pages/api/modules/{nzbs.ts => usenet/index.ts} (89%) diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx index 85d44a2b8..62be7994c 100644 --- a/src/components/AppShelf/AppShelf.tsx +++ b/src/components/AppShelf/AppShelf.tsx @@ -16,7 +16,7 @@ import { useConfig } from '../../tools/state'; import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem'; import { ModuleMenu, ModuleWrapper } from '../../modules/moduleWrapper'; -import { NzbModule, TorrentsModule } from '../../modules'; +import { UsenetModule, TorrentsModule } from '../../modules'; import TorrentsComponent from '../../modules/torrents/TorrentsModule'; const AppShelf = (props: any) => { @@ -184,7 +184,7 @@ const AppShelf = (props: any) => { {getItems()} - + ); }; diff --git a/src/modules/index.ts b/src/modules/index.ts index c903fdbad..f3b7292a7 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -7,4 +7,4 @@ export * from './search'; export * from './weather'; export * from './docker'; export * from './overseerr'; -export * from './nzb'; +export * from './usenet'; diff --git a/src/modules/nzb/index.ts b/src/modules/nzb/index.ts deleted file mode 100644 index cd8cec9fc..000000000 --- a/src/modules/nzb/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { NzbModule } from './NzbModule'; diff --git a/src/modules/torrents/TotalDownloadsModule.tsx b/src/modules/torrents/TotalDownloadsModule.tsx index ade7bb870..e0e7f05af 100644 --- a/src/modules/torrents/TotalDownloadsModule.tsx +++ b/src/modules/torrents/TotalDownloadsModule.tsx @@ -34,8 +34,7 @@ export default function TotalDownloadsComponent() { (service) => service.type === 'qBittorrent' || service.type === 'Transmission' || - service.type === 'Deluge' || - 'Sabnzbd' + service.type === 'Deluge' ) ?? []; const [torrentHistory, torrentHistoryHandlers] = useListState([]); diff --git a/src/modules/nzb/NzbModule.tsx b/src/modules/usenet/UsenetModule.tsx similarity index 71% rename from src/modules/nzb/NzbModule.tsx rename to src/modules/usenet/UsenetModule.tsx index 31374c550..63ec02134 100644 --- a/src/modules/nzb/NzbModule.tsx +++ b/src/modules/usenet/UsenetModule.tsx @@ -1,4 +1,14 @@ -import { Center, Progress, ScrollArea, Skeleton, Table, Text, Title, Tooltip } from '@mantine/core'; +import { + Center, + Progress, + ScrollArea, + Skeleton, + Table, + Tabs, + Text, + Title, + Tooltip, +} from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { IconDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons'; import axios from 'axios'; @@ -11,7 +21,7 @@ import { IModule } from '../ModuleTypes'; dayjs.extend(duration); -export const NzbComponent: FunctionComponent = () => { +export const UsenetComponent: FunctionComponent = () => { const [nzbs, setNzbs] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -20,7 +30,7 @@ export const NzbComponent: FunctionComponent = () => { const getData = async () => { try { - const response = await axios.get('/api/modules/nzbs'); + const response = await axios.get('/api/modules/usenet'); setNzbs(response.data); } catch (error) { setNzbs([]); @@ -38,7 +48,7 @@ export const NzbComponent: FunctionComponent = () => { } }; - const interval = setInterval(getData, 10000); + const interval = setInterval(getData, 5000); getData(); () => { @@ -112,26 +122,34 @@ export const NzbComponent: FunctionComponent = () => { } return ( - - {rows.length > 0 ? ( - - {ths} - {rows} -
- ) : ( -
- Queue is empty -
- )} -
+ + + Queue + History + + + + {rows.length > 0 ? ( + + {ths} + {rows} +
+ ) : ( +
+ Queue is empty +
+ )} +
+
+
); }; -export const NzbModule: IModule = { +export const UsenetModule: IModule = { id: 'usenet', title: 'Usenet', icon: IconDownload, - component: NzbComponent, + component: UsenetComponent, }; -export default NzbComponent; +export default UsenetComponent; diff --git a/src/modules/usenet/index.ts b/src/modules/usenet/index.ts new file mode 100644 index 000000000..6ee3e560e --- /dev/null +++ b/src/modules/usenet/index.ts @@ -0,0 +1,2 @@ +export { UsenetModule } from './UsenetModule'; +export * from './types'; diff --git a/src/modules/usenet/types.ts b/src/modules/usenet/types.ts new file mode 100644 index 000000000..88831e8ce --- /dev/null +++ b/src/modules/usenet/types.ts @@ -0,0 +1,13 @@ +export interface UsenetQueueItem { + name: string; + progress: number; + size: number; + id: string; + state: 'paused' | 'downloading' | 'queued'; + eta: number; +} +export interface UsenetHistoryItem { + name: string; + size: number; + id: string; +} diff --git a/src/pages/api/modules/usenet/history.ts b/src/pages/api/modules/usenet/history.ts new file mode 100644 index 000000000..23b50f476 --- /dev/null +++ b/src/pages/api/modules/usenet/history.ts @@ -0,0 +1,52 @@ +import { getCookie } from 'cookies-next'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { Client } from 'sabnzbd-api'; +import { UsenetHistoryItem } from '../../../../modules'; +import { getConfig } from '../../../../tools/getConfig'; +import { Config } from '../../../../tools/types'; + +dayjs.extend(duration); + +async function Get(req: NextApiRequest, res: NextApiResponse) { + try { + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const nzbServices = config.services.filter((service) => service.type === 'Sabnzbd'); + + const history: UsenetHistoryItem[] = []; + + await Promise.all( + nzbServices.map(async (service) => { + if (!service.apiKey) { + throw new Error(`API Key for service "${service.name}" is missing`); + } + const queue = await new Client(service.url, service.apiKey).history(); + + queue.slots.forEach((slot) => { + history.push({ + id: slot.nzo_id, + name: slot.name, + size: slot.bytes * 1000, + }); + }); + }) + ); + + return res.status(200).json(history); + } catch (err) { + return res.status(401).json(err); + } +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Filter out if the reuqest is a POST or a GET + if (req.method === 'GET') { + return Get(req, res); + } + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); +}; diff --git a/src/pages/api/modules/nzbs.ts b/src/pages/api/modules/usenet/index.ts similarity index 89% rename from src/pages/api/modules/nzbs.ts rename to src/pages/api/modules/usenet/index.ts index 4eb098277..9c3565098 100644 --- a/src/pages/api/modules/nzbs.ts +++ b/src/pages/api/modules/usenet/index.ts @@ -3,8 +3,9 @@ import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import { NextApiRequest, NextApiResponse } from 'next'; import { Client } from 'sabnzbd-api'; -import { getConfig } from '../../../tools/getConfig'; -import { Config, DownloadItem } from '../../../tools/types'; +import { UsenetQueueItem } from '../../../../modules'; +import { getConfig } from '../../../../tools/getConfig'; +import { Config } from '../../../../tools/types'; dayjs.extend(duration); @@ -14,7 +15,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; const nzbServices = config.services.filter((service) => service.type === 'Sabnzbd'); - const downloads: DownloadItem[] = []; + const downloads: UsenetQueueItem[] = []; await Promise.all( nzbServices.map(async (service) => { diff --git a/src/tools/types.ts b/src/tools/types.ts index 7eab83639..08babdb39 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -188,12 +188,3 @@ export interface serviceItem { newTab?: boolean; status?: string[]; } - -export interface DownloadItem { - name: string; - progress: number; - size: number; - id: string; - state: 'paused' | 'downloading' | 'queued'; - eta: number; -} From c44a01fbc3c041860066c1d067e9e7145ae50859 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Thu, 25 Aug 2022 21:07:41 +0200 Subject: [PATCH 03/17] More cleanup and history added --- package.json | 199 ++++++++++++----------- src/modules/usenet/UsenetHistoryList.tsx | 71 ++++++++ src/modules/usenet/UsenetModule.tsx | 135 ++------------- src/modules/usenet/UsenetQueueList.tsx | 87 ++++++++++ src/modules/usenet/types.ts | 7 + src/pages/api/modules/usenet/history.ts | 3 +- src/pages/api/modules/usenet/index.ts | 2 +- src/pages/index.tsx | 14 +- src/tools/hooks/api.ts | 21 +++ yarn.lock | 44 +++++ 10 files changed, 357 insertions(+), 226 deletions(-) create mode 100644 src/modules/usenet/UsenetHistoryList.tsx create mode 100644 src/modules/usenet/UsenetQueueList.tsx create mode 100644 src/tools/hooks/api.ts diff --git a/package.json b/package.json index 46b61bcb0..d3519c067 100644 --- a/package.json +++ b/package.json @@ -1,101 +1,102 @@ { - "name": "homarr", - "version": "0.9.2", - "description": "Homarr - A homepage for your server.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/ajnart/homarr" - }, - "scripts": { - "dev": "next dev", - "build": "next build", - "analyze": "ANALYZE=true next build", - "start": "next start", - "typecheck": "tsc --noEmit", - "export": "next build && next export", - "lint": "next lint", - "jest": "jest", - "jest:watch": "jest --watch", - "prettier:check": "prettier --check \"**/*.{ts,tsx}\"", - "prettier:write": "prettier --write \"**/*.{ts,tsx}\"", - "test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest", - "ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write" - }, - "dependencies": { - "@ctrl/deluge": "^4.1.0", - "@ctrl/qbittorrent": "^4.1.0", - "@ctrl/shared-torrent": "^4.1.1", - "@ctrl/transmission": "^4.1.1", - "@dnd-kit/core": "^6.0.5", - "@dnd-kit/sortable": "^7.0.1", - "@dnd-kit/utilities": "^3.2.0", - "@emotion/react": "^11.10.0", - "@emotion/server": "^11.10.0", - "@mantine/carousel": "^5.1.0", - "@mantine/core": "^5.2.3", - "@mantine/dates": "^5.2.3", - "@mantine/dropzone": "^5.2.3", - "@mantine/form": "^5.2.3", - "@mantine/hooks": "^5.2.3", - "@mantine/modals": "^5.2.3", - "@mantine/next": "^5.2.3", - "@mantine/notifications": "^5.2.3", - "@mantine/prism": "^5.0.0", - "@nivo/core": "^0.79.0", - "@nivo/line": "^0.79.1", - "@tabler/icons": "^1.78.0", - "add": "^2.0.6", - "axios": "^0.27.2", - "consola": "^2.15.3", - "cookies-next": "^2.1.1", - "country-flag-icons": "^1.5.5", - "dayjs": "^1.11.5", - "dockerode": "^3.3.2", - "embla-carousel-react": "^7.0.0", - "framer-motion": "^6.5.1", - "i18next": "^21.9.1", - "i18next-browser-languagedetector": "^6.1.5", - "i18next-http-backend": "^1.4.1", - "js-file-download": "^0.4.12", - "next": "12.1.6", - "next-i18next": "^11.3.0", - "prism-react-renderer": "^1.3.5", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sabnzbd-api": "^1.5.0", - "sharp": "^0.30.7", - "systeminformation": "^5.12.1", - "uuid": "^8.3.2", - "yarn": "^1.22.19" - }, - "devDependencies": { - "@next/bundle-analyzer": "^12.1.4", - "@next/eslint-plugin-next": "^12.1.4", - "@types/dockerode": "^3.3.9", - "@types/node": "17.0.1", - "@types/react": "17.0.1", - "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.30.7", - "@typescript-eslint/parser": "^5.30.7", - "eslint": "^8.20.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-mantine": "^2.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "^26.6.0", - "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-react": "^7.30.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-testing-library": "^5.5.1", - "eslint-plugin-unused-imports": "^2.0.0", - "jest": "^28.1.3", - "prettier": "^2.7.1", - "typescript": "^4.7.4" - }, - "resolutions": { - "@types/react": "17.0.2", - "@types/react-dom": "17.0.2" - }, - "packageManager": "yarn@3.2.1" + "name": "homarr", + "version": "0.9.2", + "description": "Homarr - A homepage for your server.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/ajnart/homarr" + }, + "scripts": { + "dev": "next dev", + "build": "next build", + "analyze": "ANALYZE=true next build", + "start": "next start", + "typecheck": "tsc --noEmit", + "export": "next build && next export", + "lint": "next lint", + "jest": "jest", + "jest:watch": "jest --watch", + "prettier:check": "prettier --check \"**/*.{ts,tsx}\"", + "prettier:write": "prettier --write \"**/*.{ts,tsx}\"", + "test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest", + "ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write" + }, + "dependencies": { + "@ctrl/deluge": "^4.1.0", + "@ctrl/qbittorrent": "^4.1.0", + "@ctrl/shared-torrent": "^4.1.1", + "@ctrl/transmission": "^4.1.1", + "@dnd-kit/core": "^6.0.5", + "@dnd-kit/sortable": "^7.0.1", + "@dnd-kit/utilities": "^3.2.0", + "@emotion/react": "^11.10.0", + "@emotion/server": "^11.10.0", + "@mantine/carousel": "^5.1.0", + "@mantine/core": "^5.2.3", + "@mantine/dates": "^5.2.3", + "@mantine/dropzone": "^5.2.3", + "@mantine/form": "^5.2.3", + "@mantine/hooks": "^5.2.3", + "@mantine/modals": "^5.2.3", + "@mantine/next": "^5.2.3", + "@mantine/notifications": "^5.2.3", + "@mantine/prism": "^5.0.0", + "@nivo/core": "^0.79.0", + "@nivo/line": "^0.79.1", + "@tabler/icons": "^1.78.0", + "@tanstack/react-query": "^4.2.1", + "add": "^2.0.6", + "axios": "^0.27.2", + "consola": "^2.15.3", + "cookies-next": "^2.1.1", + "country-flag-icons": "^1.5.5", + "dayjs": "^1.11.5", + "dockerode": "^3.3.2", + "embla-carousel-react": "^7.0.0", + "framer-motion": "^6.5.1", + "i18next": "^21.9.1", + "i18next-browser-languagedetector": "^6.1.5", + "i18next-http-backend": "^1.4.1", + "js-file-download": "^0.4.12", + "next": "12.1.6", + "next-i18next": "^11.3.0", + "prism-react-renderer": "^1.3.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sabnzbd-api": "^1.5.0", + "sharp": "^0.30.7", + "systeminformation": "^5.12.1", + "uuid": "^8.3.2", + "yarn": "^1.22.19" + }, + "devDependencies": { + "@next/bundle-analyzer": "^12.1.4", + "@next/eslint-plugin-next": "^12.1.4", + "@types/dockerode": "^3.3.9", + "@types/node": "17.0.1", + "@types/react": "17.0.1", + "@types/uuid": "^8.3.4", + "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.7", + "eslint": "^8.20.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-mantine": "^2.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jest": "^26.6.0", + "eslint-plugin-jsx-a11y": "^6.6.1", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^5.5.1", + "eslint-plugin-unused-imports": "^2.0.0", + "jest": "^28.1.3", + "prettier": "^2.7.1", + "typescript": "^4.7.4" + }, + "resolutions": { + "@types/react": "17.0.2", + "@types/react-dom": "17.0.2" + }, + "packageManager": "yarn@3.2.1" } diff --git a/src/modules/usenet/UsenetHistoryList.tsx b/src/modules/usenet/UsenetHistoryList.tsx new file mode 100644 index 000000000..c2b8e8da5 --- /dev/null +++ b/src/modules/usenet/UsenetHistoryList.tsx @@ -0,0 +1,71 @@ +import { Center, Table, Text, Title, Tooltip, useMantineTheme } from '@mantine/core'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { FunctionComponent } from 'react'; +import { humanFileSize } from '../../tools/humanFileSize'; +import { UsenetHistoryItem } from './types'; + +dayjs.extend(duration); + +interface UsenetHistoryListProps { + items: UsenetHistoryItem[]; +} + +export const UsenetHistoryList: FunctionComponent = ({ items }) => { + const theme = useMantineTheme(); + + if (items.length <= 0) { + return ( +
+ Queue is empty +
+ ); + } + + return ( + + + + + + + + + + + + + + + {items.map((history) => ( + + + + + + ))} + +
NameSizeDownload Duration
+ + + {history.name} + + + + {humanFileSize(history.size)} + + + {dayjs + .duration(history.time, 's') + .format(history.time < 60 ? 's [seconds]' : 'm [minutes] s [seconds] ')} + +
+ ); +}; diff --git a/src/modules/usenet/UsenetModule.tsx b/src/modules/usenet/UsenetModule.tsx index 63ec02134..8fae8b3e3 100644 --- a/src/modules/usenet/UsenetModule.tsx +++ b/src/modules/usenet/UsenetModule.tsx @@ -1,115 +1,16 @@ -import { - Center, - Progress, - ScrollArea, - Skeleton, - Table, - Tabs, - Text, - Title, - Tooltip, -} from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; -import { IconDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons'; -import axios from 'axios'; -import dayjs from 'dayjs'; -import { FunctionComponent, useEffect, useState } from 'react'; -import duration from 'dayjs/plugin/duration'; -import { humanFileSize } from '../../tools/humanFileSize'; -import { DownloadItem } from '../../tools/types'; -import { IModule } from '../ModuleTypes'; +import { Skeleton, Tabs, useMantineTheme } from '@mantine/core'; +import { IconDownload } from '@tabler/icons'; +import { FunctionComponent } from 'react'; -dayjs.extend(duration); +import { IModule } from '../ModuleTypes'; +import { useGetUsenetDownloads, useGetUsenetHistory } from '../../tools/hooks/api'; +import { UsenetQueueList } from './UsenetQueueList'; +import { UsenetHistoryList } from './UsenetHistoryList'; export const UsenetComponent: FunctionComponent = () => { - const [nzbs, setNzbs] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - setIsLoading(true); - - const getData = async () => { - try { - const response = await axios.get('/api/modules/usenet'); - setNzbs(response.data); - } catch (error) { - setNzbs([]); - showNotification({ - title: 'Error fetching torrents', - autoClose: 1000, - disallowClose: true, - id: 'fail-torrent-downloads-module', - color: 'red', - message: - 'Please check your config for any potential errors, check the console for more info', - }); - } finally { - setIsLoading(false); - } - }; - - const interval = setInterval(getData, 5000); - getData(); - - () => { - clearInterval(interval); - }; - }, []); - - const ths = ( - - - Name - Size - ETA - Progress - - ); - - const rows = nzbs.map((nzb) => ( - - - {nzb.state === 'paused' ? ( - - ) : ( - - )} - - - - - {nzb.name} - - - - - {humanFileSize(nzb.size * 1000 * 1000)} - - - {nzb.eta <= 0 ? ( - - Paused - - ) : ( - {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} - )} - - - {nzb.progress.toFixed(1)}% - - - - )); + const theme = useMantineTheme(); + const { isLoading, data: nzbs = [] } = useGetUsenetDownloads(); + const { data: history = [] } = useGetUsenetHistory(); if (isLoading) { return ( @@ -128,18 +29,10 @@ export const UsenetComponent: FunctionComponent = () => { History - - {rows.length > 0 ? ( - - {ths} - {rows} -
- ) : ( -
- Queue is empty -
- )} -
+ +
+ + ); diff --git a/src/modules/usenet/UsenetQueueList.tsx b/src/modules/usenet/UsenetQueueList.tsx new file mode 100644 index 000000000..3d7a4e205 --- /dev/null +++ b/src/modules/usenet/UsenetQueueList.tsx @@ -0,0 +1,87 @@ +import { Center, Progress, Table, Text, Title, Tooltip, useMantineTheme } from '@mantine/core'; +import { IconPlayerPause, IconPlayerPlay } from '@tabler/icons'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { FunctionComponent } from 'react'; +import { humanFileSize } from '../../tools/humanFileSize'; +import { UsenetQueueItem } from './types'; + +dayjs.extend(duration); +interface UsenetQueueListProps { + items: UsenetQueueItem[]; +} + +export const UsenetQueueList: FunctionComponent = ({ items }) => { + const theme = useMantineTheme(); + + if (items.length <= 0) { + return ( +
+ Queue is empty +
+ ); + } + + return ( + + + + + + + + + + + {items.map((nzb) => ( + + + + + + + + ))} + +
+ NameSizeETAProgress
+ {nzb.state === 'paused' ? ( + + ) : ( + + )} + + + + {nzb.name} + + + + {humanFileSize(nzb.size)} + + {nzb.eta <= 0 ? ( + + Paused + + ) : ( + {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} + )} + + {nzb.progress.toFixed(1)}% + 0 ? theme.primaryColor : 'lightgrey'} + value={nzb.progress} + size="lg" + style={{ width: '100%' }} + /> +
+ ); +}; diff --git a/src/modules/usenet/types.ts b/src/modules/usenet/types.ts index 88831e8ce..a4dd8c5a0 100644 --- a/src/modules/usenet/types.ts +++ b/src/modules/usenet/types.ts @@ -1,6 +1,9 @@ export interface UsenetQueueItem { name: string; progress: number; + /** + * Size in bytes + */ size: number; id: string; state: 'paused' | 'downloading' | 'queued'; @@ -8,6 +11,10 @@ export interface UsenetQueueItem { } export interface UsenetHistoryItem { name: string; + /** + * Size in bytes + */ size: number; id: string; + time: number; } diff --git a/src/pages/api/modules/usenet/history.ts b/src/pages/api/modules/usenet/history.ts index 23b50f476..4f418be68 100644 --- a/src/pages/api/modules/usenet/history.ts +++ b/src/pages/api/modules/usenet/history.ts @@ -28,7 +28,8 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { history.push({ id: slot.nzo_id, name: slot.name, - size: slot.bytes * 1000, + size: slot.bytes, + time: slot.download_time, }); }); }) diff --git a/src/pages/api/modules/usenet/index.ts b/src/pages/api/modules/usenet/index.ts index 9c3565098..cedb154e8 100644 --- a/src/pages/api/modules/usenet/index.ts +++ b/src/pages/api/modules/usenet/index.ts @@ -36,7 +36,7 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { eta: eta.asSeconds(), name: slot.filename, progress: parseFloat(slot.percentage), - size: parseFloat(slot.mb), + size: parseFloat(slot.mb) * 1000 * 1000, state: slot.status.toLowerCase() as any, }); }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 604ff91a8..70ae411da 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,6 +3,8 @@ import { GetServerSidePropsContext } from 'next'; import { useEffect } from 'react'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + import AppShelf from '../components/AppShelf/AppShelf'; import LoadConfigComponent from '../components/Config/LoadConfig'; import { Config } from '../tools/types'; @@ -62,6 +64,8 @@ export async function getServerSideProps({ return getConfig(configName as string, translations); } +const queryClient = new QueryClient(); + export default function HomePage(props: any) { const { config: initialConfig }: { config: Config } = props; const { setConfig } = useConfig(); @@ -73,9 +77,11 @@ export default function HomePage(props: any) { setConfig(migratedConfig); }, [initialConfig]); return ( - - - - + + + + + + ); } diff --git a/src/tools/hooks/api.ts b/src/tools/hooks/api.ts new file mode 100644 index 000000000..305888ce0 --- /dev/null +++ b/src/tools/hooks/api.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { UsenetHistoryItem, UsenetQueueItem } from '../../modules'; + +export const useGetUsenetDownloads = () => + useQuery( + ['usenetDownloads'], + async () => (await axios.get('/api/modules/usenet')).data, + { + refetchInterval: 1000, + } + ); + +export const useGetUsenetHistory = () => + useQuery( + ['usenetHistory'], + async () => (await axios.get('/api/modules/usenet/history')).data, + { + refetchInterval: 1000, + } + ); diff --git a/yarn.lock b/yarn.lock index 905d3b641..95c494a5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,6 +1918,33 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:^4.0.0-beta.1": + version: 4.2.1 + resolution: "@tanstack/query-core@npm:4.2.1" + checksum: f71854969e02de6c2cfbe25e8b11e275b61e1297a902e0d5c4beac580a87db99555c1c21d536d838ce5e0664bc49da7b60a3c6b8de334c7004c5005fe2a48030 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^4.2.1": + version: 4.2.1 + resolution: "@tanstack/react-query@npm:4.2.1" + dependencies: + "@tanstack/query-core": ^4.0.0-beta.1 + "@types/use-sync-external-store": ^0.0.3 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: "*" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: bbf3a808645c26c649971dc182bb9a7ed7a1d89f6456b60685c6081b8be6ae84ae83b39c7eacb96c4f3b6677ca001d8114037329951987b7a8d65de53b28c862 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -2163,6 +2190,13 @@ __metadata: languageName: node linkType: hard +"@types/use-sync-external-store@npm:^0.0.3": + version: 0.0.3 + resolution: "@types/use-sync-external-store@npm:0.0.3" + checksum: 161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e + languageName: node + linkType: hard + "@types/uuid@npm:^8.3.4": version: 8.3.4 resolution: "@types/uuid@npm:8.3.4" @@ -4775,6 +4809,7 @@ __metadata: "@nivo/core": ^0.79.0 "@nivo/line": ^0.79.1 "@tabler/icons": ^1.78.0 + "@tanstack/react-query": ^4.2.1 "@types/dockerode": ^3.3.9 "@types/node": 17.0.1 "@types/react": 17.0.1 @@ -8164,6 +8199,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" From d9d3d3de45193611df29cd60ce2971c9d1aeaafa Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Fri, 26 Aug 2022 10:46:34 +0200 Subject: [PATCH 04/17] History done --- src/modules/torrents/TorrentsModule.tsx | 3 +- src/modules/usenet/UsenetHistoryList.tsx | 129 ++++++++++++++--------- src/modules/usenet/UsenetModule.tsx | 46 ++++---- src/modules/usenet/UsenetQueueList.tsx | 44 ++++++-- src/pages/api/modules/usenet/history.ts | 52 +++++---- src/pages/api/modules/usenet/index.ts | 72 ++++++++----- src/tools/hooks/api.ts | 30 ++++-- src/tools/hooks/useGetServiceByType.ts | 14 +++ 8 files changed, 256 insertions(+), 134 deletions(-) create mode 100644 src/tools/hooks/useGetServiceByType.ts diff --git a/src/modules/torrents/TorrentsModule.tsx b/src/modules/torrents/TorrentsModule.tsx index 04a9b6b3b..c7d3d5beb 100644 --- a/src/modules/torrents/TorrentsModule.tsx +++ b/src/modules/torrents/TorrentsModule.tsx @@ -42,8 +42,7 @@ export default function TorrentsComponent() { (service) => service.type === 'qBittorrent' || service.type === 'Transmission' || - service.type === 'Deluge' || - service.type === 'Sabnzbd' + service.type === 'Deluge' ) ?? []; const hideComplete: boolean = diff --git a/src/modules/usenet/UsenetHistoryList.tsx b/src/modules/usenet/UsenetHistoryList.tsx index c2b8e8da5..7b1b7792b 100644 --- a/src/modules/usenet/UsenetHistoryList.tsx +++ b/src/modules/usenet/UsenetHistoryList.tsx @@ -1,20 +1,39 @@ -import { Center, Table, Text, Title, Tooltip, useMantineTheme } from '@mantine/core'; +import { Center, Pagination, Skeleton, Table, Text, Title, Tooltip } from '@mantine/core'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; -import { FunctionComponent } from 'react'; +import { FunctionComponent, useState } from 'react'; +import { useGetUsenetHistory } from '../../tools/hooks/api'; import { humanFileSize } from '../../tools/humanFileSize'; -import { UsenetHistoryItem } from './types'; dayjs.extend(duration); interface UsenetHistoryListProps { - items: UsenetHistoryItem[]; + serviceId: string; } -export const UsenetHistoryList: FunctionComponent = ({ items }) => { - const theme = useMantineTheme(); +const PAGE_SIZE = 10; - if (items.length <= 0) { +export const UsenetHistoryList: FunctionComponent = ({ serviceId }) => { + const [page, setPage] = useState(1); + + const { data, isLoading } = useGetUsenetHistory({ + limit: PAGE_SIZE, + offset: (page - 1) * PAGE_SIZE, + serviceId, + }); + const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE); + + if (isLoading) { + return ( + <> + + + + + ); + } + + if (!data || data.items.length <= 0) { return (
Queue is empty @@ -23,49 +42,59 @@ export const UsenetHistoryList: FunctionComponent = ({ i } return ( - - - - - - - - - - - - - - - {items.map((history) => ( - - - - +
+
NameSizeDownload Duration
- - - {history.name} - - - - {humanFileSize(history.size)} - - - {dayjs - .duration(history.time, 's') - .format(history.time < 60 ? 's [seconds]' : 'm [minutes] s [seconds] ')} - -
+ + + + + + + + + + - ))} - -
NameSizeDownload Duration
+ + + {data.items.map((history) => ( + + + + + {history.name} + + + + + {humanFileSize(history.size)} + + + + {dayjs + .duration(history.time, 's') + .format(history.time < 60 ? 's [seconds]' : 'm [minutes] s [seconds] ')} + + + + ))} + + + + ); }; diff --git a/src/modules/usenet/UsenetModule.tsx b/src/modules/usenet/UsenetModule.tsx index 8fae8b3e3..da9837767 100644 --- a/src/modules/usenet/UsenetModule.tsx +++ b/src/modules/usenet/UsenetModule.tsx @@ -1,38 +1,42 @@ -import { Skeleton, Tabs, useMantineTheme } from '@mantine/core'; +import { Group, Select, Tabs } from '@mantine/core'; import { IconDownload } from '@tabler/icons'; -import { FunctionComponent } from 'react'; +import { FunctionComponent, useState } from 'react'; import { IModule } from '../ModuleTypes'; -import { useGetUsenetDownloads, useGetUsenetHistory } from '../../tools/hooks/api'; import { UsenetQueueList } from './UsenetQueueList'; import { UsenetHistoryList } from './UsenetHistoryList'; +import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType'; export const UsenetComponent: FunctionComponent = () => { - const theme = useMantineTheme(); - const { isLoading, data: nzbs = [] } = useGetUsenetDownloads(); - const { data: history = [] } = useGetUsenetHistory(); + const downloadServices = useGetServiceByType('Sabnzbd'); - if (isLoading) { - return ( - <> - - - - - ); + const [selectedServiceId, setSelectedService] = useState(downloadServices[0]?.id); + + if (!selectedServiceId) { + return null; } return ( - - - Queue - History - + + + + Queue + History + + {downloadServices.length > 1 && ( + = ({ serviceId }) => { const theme = useMantineTheme(); + const { t } = useTranslation('modules/usenet'); + const [page, setPage] = useState(1); const { data, isLoading, isError, error } = useGetUsenetDownloads({ limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE, serviceId, }); + const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE); if (isLoading) { return ( @@ -50,7 +56,7 @@ export const UsenetQueueList: FunctionComponent = ({ servi return ( } my="lg" title="Error!" color="red" radius="md"> - Some error has occured while fetching data: + {t('queue.error')} {(error as AxiosError)?.response?.data as string} @@ -62,71 +68,88 @@ export const UsenetQueueList: FunctionComponent = ({ servi if (!data || data.items.length <= 0) { return (
- Queue is empty + {t('queue.empty')}
); } return ( - - - - - - - - - - - {data.items.map((nzb) => ( - - - - - - + <> +
- NameSizeETAProgress
- {nzb.state === 'paused' ? ( - - ) : ( - - )} - - - - {nzb.name} - - - - {humanFileSize(nzb.size)} - - {nzb.eta <= 0 ? ( - - Paused - - ) : ( - {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} - )} - - {nzb.progress.toFixed(1)}% - 0 ? theme.primaryColor : 'lightgrey'} - value={nzb.progress} - size="lg" - style={{ width: '100%' }} - /> -
+ + + + + + - ))} - -
+ {t('queue.header.name')}{t('queue.header.size')}{t('queue.header.eta')}{t('queue.header.progress')}
+ + + {data.items.map((nzb) => ( + + + {nzb.state === 'paused' ? ( + + ) : ( + + )} + + + + + {nzb.name} + + + + + {humanFileSize(nzb.size)} + + + {nzb.eta <= 0 ? ( + + {t('queue.paused')} + + ) : ( + {dayjs.duration(nzb.eta, 's').format('H:mm:ss')} + )} + + + {nzb.progress.toFixed(1)}% + 0 ? theme.primaryColor : 'lightgrey'} + value={nzb.progress} + size="lg" + style={{ width: '100%' }} + /> + + + ))} + + + {totalPages > 1 && ( + + )} + ); }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 78f046777..d57b7d782 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -8,9 +8,11 @@ import { NotificationsProvider } from '@mantine/notifications'; import { useHotkeys } from '@mantine/hooks'; import { ModalsProvider } from '@mantine/modals'; import { appWithTranslation } from 'next-i18next'; +import { QueryClientProvider } from '@tanstack/react-query'; import { ConfigProvider } from '../tools/state'; import { theme } from '../tools/theme'; import { ColorTheme } from '../tools/color'; +import { queryClient } from '../tools/queryClient'; function App(this: any, props: AppProps & { colorScheme: ColorScheme }) { const { Component, pageProps } = props; @@ -40,43 +42,44 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) { - - - - + + + - - - - - - - - - - + primaryColor, + primaryShade, + colorScheme, + }} + withGlobalStyles + withNormalizeCSS + > + + + + + + + + + + + ); } diff --git a/src/pages/api/modules/usenet/index.ts b/src/pages/api/modules/usenet/index.ts index e7d5a69cf..60217decb 100644 --- a/src/pages/api/modules/usenet/index.ts +++ b/src/pages/api/modules/usenet/index.ts @@ -3,29 +3,28 @@ import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import { NextApiRequest, NextApiResponse } from 'next'; import { Client } from 'sabnzbd-api'; -import { UsenetQueueItem } from '../../../../modules'; import { getConfig } from '../../../../tools/getConfig'; import { getServiceById } from '../../../../tools/hooks/useGetServiceByType'; import { Config } from '../../../../tools/types'; dayjs.extend(duration); -export interface UsenetQueueRequestParams { +export interface UsenetInfoRequestParams { serviceId: string; - offset: number; - limit: number; } -export interface UsenetQueueResponse { - items: UsenetQueueItem[]; - total: number; +export interface UsenetInfoResponse { + paused: boolean; + sizeLeft: number; + speed: number; + eta: number; } async function Get(req: NextApiRequest, res: NextApiResponse) { try { const configName = getCookie('config-name', { req }); const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; - const { limit, offset, serviceId } = req.query as any as UsenetQueueRequestParams; + const { serviceId } = req.query as any as UsenetInfoRequestParams; const service = getServiceById(config, serviceId); @@ -36,29 +35,21 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { if (!service.apiKey) { throw new Error(`API Key for service "${service.name}" is missing`); } - const queue = await new Client(service.url, service.apiKey).queue(offset, limit); - 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); + const queue = await new Client(service.url, service.apiKey).queue(0, -1); - 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 [hours, minutes, seconds] = queue.timeleft.split(':'); + const eta = dayjs.duration({ + hour: parseInt(hours, 10), + minutes: parseInt(minutes, 10), + seconds: parseInt(seconds, 10), + } as any); - const response: UsenetQueueResponse = { - items, - total: queue.noofslots_total, + const response: UsenetInfoResponse = { + paused: queue.paused, + sizeLeft: parseFloat(queue.mbleft) * 1024 * 1024, + speed: parseFloat(queue.kbpersec) * 1000, + eta: eta.asSeconds(), }; return res.status(200).json(response); diff --git a/src/pages/api/modules/usenet/pause.ts b/src/pages/api/modules/usenet/pause.ts new file mode 100644 index 000000000..816158562 --- /dev/null +++ b/src/pages/api/modules/usenet/pause.ts @@ -0,0 +1,49 @@ +import { getCookie } from 'cookies-next'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { Client } from 'sabnzbd-api'; +import { getConfig } from '../../../../tools/getConfig'; +import { getServiceById } from '../../../../tools/hooks/useGetServiceByType'; +import { Config } from '../../../../tools/types'; + +dayjs.extend(duration); + +export interface UsenetPauseRequestParams { + serviceId: string; +} + +async function Post(req: NextApiRequest, res: NextApiResponse) { + try { + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const { serviceId } = req.query as any as UsenetPauseRequestParams; + + const service = getServiceById(config, serviceId); + + if (!service) { + throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); + } + + if (!service.apiKey) { + throw new Error(`API Key for service "${service.name}" is missing`); + } + + const result = await new Client(service.url, service.apiKey).queuePause(); + + return res.status(200).json(result); + } catch (err) { + return res.status(500).send((err as any).message); + } +} + +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', + }); +}; diff --git a/src/pages/api/modules/usenet/queue.ts b/src/pages/api/modules/usenet/queue.ts new file mode 100644 index 000000000..8eab23fff --- /dev/null +++ b/src/pages/api/modules/usenet/queue.ts @@ -0,0 +1,79 @@ +import { getCookie } from 'cookies-next'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { Client } from 'sabnzbd-api'; +import { UsenetQueueItem } from '../../../../modules'; +import { getConfig } from '../../../../tools/getConfig'; +import { getServiceById } from '../../../../tools/hooks/useGetServiceByType'; +import { Config } from '../../../../tools/types'; + +dayjs.extend(duration); + +export interface UsenetQueueRequestParams { + serviceId: string; + offset: number; + limit: number; +} + +export interface UsenetQueueResponse { + items: UsenetQueueItem[]; + total: number; +} + +async function Get(req: NextApiRequest, res: NextApiResponse) { + try { + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const { limit, offset, serviceId } = req.query as any as UsenetQueueRequestParams; + + const service = getServiceById(config, serviceId); + + if (!service) { + throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); + } + + if (!service.apiKey) { + throw new Error(`API Key for service "${service.name}" is missing`); + } + const queue = await new Client(service.url, service.apiKey).queue(offset, limit); + + 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 response: UsenetQueueResponse = { + items, + total: queue.noofslots, + }; + + return res.status(200).json(response); + } catch (err) { + return res.status(500).send((err as any).message); + } +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Filter out if the reuqest is a POST or a GET + if (req.method === 'GET') { + return Get(req, res); + } + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); +}; diff --git a/src/pages/api/modules/usenet/resume.ts b/src/pages/api/modules/usenet/resume.ts new file mode 100644 index 000000000..adf8a9372 --- /dev/null +++ b/src/pages/api/modules/usenet/resume.ts @@ -0,0 +1,50 @@ +import { getCookie } from 'cookies-next'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { Client } from 'sabnzbd-api'; +import { getConfig } from '../../../../tools/getConfig'; +import { getServiceById } from '../../../../tools/hooks/useGetServiceByType'; +import { Config } from '../../../../tools/types'; + +dayjs.extend(duration); + +export interface UsenetResumeRequestParams { + serviceId: string; + nzbId?: string; +} + +async function Post(req: NextApiRequest, res: NextApiResponse) { + try { + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const { serviceId } = req.query as any as UsenetResumeRequestParams; + + const service = getServiceById(config, serviceId); + + if (!service) { + throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`); + } + + if (!service.apiKey) { + throw new Error(`API Key for service "${service.name}" is missing`); + } + + const result = await new Client(service.url, service.apiKey).queueResume(); + + return res.status(200).json(result); + } catch (err) { + return res.status(500).send((err as any).message); + } +} + +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', + }); +}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 70ae411da..724e8e882 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,8 +3,6 @@ import { GetServerSidePropsContext } from 'next'; import { useEffect } from 'react'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; - import AppShelf from '../components/AppShelf/AppShelf'; import LoadConfigComponent from '../components/Config/LoadConfig'; import { Config } from '../tools/types'; @@ -52,6 +50,7 @@ export async function getServerSideProps({ 'modules/date', 'modules/calendar', 'modules/dlspeed', + 'modules/usenet', 'modules/search', 'modules/torrents-status', 'modules/weather', @@ -64,8 +63,6 @@ export async function getServerSideProps({ return getConfig(configName as string, translations); } -const queryClient = new QueryClient(); - export default function HomePage(props: any) { const { config: initialConfig }: { config: Config } = props; const { setConfig } = useConfig(); @@ -77,11 +74,9 @@ export default function HomePage(props: any) { setConfig(migratedConfig); }, [initialConfig]); return ( - - - - - - + + + + ); } diff --git a/src/tools/hooks/api.ts b/src/tools/hooks/api.ts index 9b0f1b8d6..22cb6987b 100644 --- a/src/tools/hooks/api.ts +++ b/src/tools/hooks/api.ts @@ -1,17 +1,41 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import axios from 'axios'; -import { UsenetQueueRequestParams, UsenetQueueResponse } from '../../pages/api/modules/usenet'; +import { Results } from 'sabnzbd-api'; +import { + UsenetQueueRequestParams, + UsenetQueueResponse, +} from '../../pages/api/modules/usenet/queue'; import { UsenetHistoryRequestParams, UsenetHistoryResponse, } from '../../pages/api/modules/usenet/history'; +import { UsenetInfoRequestParams, UsenetInfoResponse } from '../../pages/api/modules/usenet'; +import { UsenetPauseRequestParams } from '../../pages/api/modules/usenet/pause'; +import { queryClient } from '../queryClient'; +import { UsenetResumeRequestParams } from '../../pages/api/modules/usenet/resume'; + +export const useGetUsenetInfo = (params: UsenetInfoRequestParams) => + useQuery( + ['usenetInfo', params.serviceId], + async () => + ( + await axios.get('/api/modules/usenet', { + params, + }) + ).data, + { + refetchInterval: 1000, + keepPreviousData: true, + retry: 2, + } + ); export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) => useQuery( ['usenetDownloads', ...Object.values(params)], async () => ( - await axios.get('/api/modules/usenet', { + await axios.get('/api/modules/usenet/queue', { params, }) ).data, @@ -37,3 +61,91 @@ export const useGetUsenetHistory = (params: UsenetHistoryRequestParams) => retry: 2, } ); + +export const usePauseUsenetQueue = (params: UsenetPauseRequestParams) => + useMutation( + ['usenetPause', ...Object.values(params)], + async () => + ( + await axios.post( + '/api/modules/usenet/pause', + {}, + { + params, + } + ) + ).data, + { + async onMutate() { + await queryClient.cancelQueries(['usenetInfo', params.serviceId]); + const previousInfo = queryClient.getQueryData([ + 'usenetInfo', + params.serviceId, + ]); + + if (previousInfo) { + queryClient.setQueryData(['usenetInfo', params.serviceId], { + ...previousInfo, + paused: true, + }); + } + + return { previousInfo }; + }, + onError(err, _, context) { + if (context?.previousInfo) { + queryClient.setQueryData( + ['usenetInfo', params.serviceId], + context.previousInfo + ); + } + }, + onSettled() { + queryClient.invalidateQueries(['usenetInfo', params.serviceId]); + }, + } + ); + +export const useResumeUsenetQueue = (params: UsenetResumeRequestParams) => + useMutation( + ['usenetResume', ...Object.values(params)], + async () => + ( + await axios.post( + '/api/modules/usenet/resume', + {}, + { + params, + } + ) + ).data, + { + async onMutate() { + await queryClient.cancelQueries(['usenetInfo', params.serviceId]); + const previousInfo = queryClient.getQueryData([ + 'usenetInfo', + params.serviceId, + ]); + + if (previousInfo) { + queryClient.setQueryData(['usenetInfo', params.serviceId], { + ...previousInfo, + paused: false, + }); + } + + return { previousInfo }; + }, + onError(err, _, context) { + if (context?.previousInfo) { + queryClient.setQueryData( + ['usenetInfo', params.serviceId], + context.previousInfo + ); + } + }, + onSettled() { + queryClient.invalidateQueries(['usenetInfo', params.serviceId]); + }, + } + ); diff --git a/src/tools/queryClient.ts b/src/tools/queryClient.ts new file mode 100644 index 000000000..6d46de591 --- /dev/null +++ b/src/tools/queryClient.ts @@ -0,0 +1,3 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient(); From bf93fc87ee14cf519e79fa9376da893fc31c03d3 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Fri, 26 Aug 2022 16:13:54 +0200 Subject: [PATCH 09/17] revert default.json --- data/configs/default.json | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/data/configs/default.json b/data/configs/default.json index 415c095ab..066e4ac36 100644 --- a/data/configs/default.json +++ b/data/configs/default.json @@ -7,42 +7,12 @@ "type": "Other", "icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" - }, - { - "id": "4cb1c2af-9f23-4f52-86e1-0eac25673b56", - "type": "Sabnzbd", - "name": "Sabnzbd", - "icon": "/favicon.png", - "url": "https://sabnzbd.jannesv.be", - "apiKey": "2982e4afbc6d42d6bb5863751a354d20", - "openedUrl": "https://sabnzbd.jannesv.be" } ], - "settings": { - "searchUrl": "https://google.com/search?q=", - "primaryColor": "grape" - }, + "settings": {}, "modules": { "Search Bar": { "enabled": true - }, - "Download Speed": { - "enabled": false - }, - "Torrent": { - "enabled": false - }, - "Dash.": { - "enabled": true - }, - "Usenet": { - "enabled": true - }, - "usenet": { - "enabled": true - }, - "docker": { - "enabled": true } } -} \ No newline at end of file +} From 281f7bacb0def28c378c714dec04fbb7ea1257c5 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Fri, 26 Aug 2022 16:43:46 +0200 Subject: [PATCH 10/17] Fix minor issues --- data/configs/default.json | 4 +++- public/locales/en/modules/usenet.json | 8 ++++++++ src/modules/usenet/UsenetModule.tsx | 25 +++++++++++++++++++++++-- src/modules/usenet/UsenetQueueList.tsx | 18 +++++++++++------- src/tools/hooks/api.ts | 9 ++++++--- 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/data/configs/default.json b/data/configs/default.json index 066e4ac36..07dfe37b0 100644 --- a/data/configs/default.json +++ b/data/configs/default.json @@ -9,7 +9,9 @@ "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" } ], - "settings": {}, + "settings": { + "searchUrl": "https://google.com/search?q=" + }, "modules": { "Search Bar": { "enabled": true diff --git a/public/locales/en/modules/usenet.json b/public/locales/en/modules/usenet.json index 59f298103..30231fb80 100644 --- a/public/locales/en/modules/usenet.json +++ b/public/locales/en/modules/usenet.json @@ -3,6 +3,14 @@ "name": "Usenet", "description": "Show the queue and history of supported services" }, + "card": { + "errors": { + "noDownloadClients": { + "title": "No supported download clients found!", + "text": "Add a download service to view your current downloads" + } + } + }, "tabs": { "queue": "Queue", "history": "History" diff --git a/src/modules/usenet/UsenetModule.tsx b/src/modules/usenet/UsenetModule.tsx index 155734d59..f03084e69 100644 --- a/src/modules/usenet/UsenetModule.tsx +++ b/src/modules/usenet/UsenetModule.tsx @@ -1,6 +1,6 @@ -import { Badge, Button, Group, Select, Tabs } from '@mantine/core'; +import { Badge, Button, Group, Select, Stack, Tabs, Text, Title } from '@mantine/core'; import { IconDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons'; -import { FunctionComponent, useState } from 'react'; +import { FunctionComponent, useEffect, useState } from 'react'; import { useTranslation } from 'next-i18next'; import dayjs from 'dayjs'; @@ -11,18 +11,39 @@ import { UsenetHistoryList } from './UsenetHistoryList'; import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType'; import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api'; import { humanFileSize } from '../../tools/humanFileSize'; +import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem'; dayjs.extend(duration); export const UsenetComponent: FunctionComponent = () => { const downloadServices = useGetServiceByType('Sabnzbd'); + const { t } = useTranslation('modules/usenet'); const [selectedServiceId, setSelectedService] = useState(downloadServices[0]?.id); const { data } = useGetUsenetInfo({ serviceId: selectedServiceId! }); + + useEffect(() => { + if (!selectedServiceId && downloadServices.length) { + setSelectedService(downloadServices[0].id); + } + }, [downloadServices, selectedServiceId]); + const { mutate: pause } = usePauseUsenetQueue({ serviceId: selectedServiceId! }); const { mutate: resume } = useResumeUsenetQueue({ serviceId: selectedServiceId! }); + if (downloadServices.length === 0) { + return ( + + {t('card.errors.noDownloadClients.title')} + + {t('card.errors.noDownloadClients.text')} + + + + ); + } + if (!selectedServiceId) { return null; } diff --git a/src/modules/usenet/UsenetQueueList.tsx b/src/modules/usenet/UsenetQueueList.tsx index 7198b9511..bc6d5ea0c 100644 --- a/src/modules/usenet/UsenetQueueList.tsx +++ b/src/modules/usenet/UsenetQueueList.tsx @@ -1,6 +1,6 @@ import { + ActionIcon, Alert, - Button, Center, Code, Group, @@ -90,13 +90,17 @@ export const UsenetQueueList: FunctionComponent = ({ servi {nzb.state === 'paused' ? ( - + + + + + ) : ( - + + + + + )} diff --git a/src/tools/hooks/api.ts b/src/tools/hooks/api.ts index 22cb6987b..ee4b5cf1d 100644 --- a/src/tools/hooks/api.ts +++ b/src/tools/hooks/api.ts @@ -14,6 +14,8 @@ import { UsenetPauseRequestParams } from '../../pages/api/modules/usenet/pause'; import { queryClient } from '../queryClient'; import { UsenetResumeRequestParams } from '../../pages/api/modules/usenet/resume'; +const POLLING_INTERVAL = 2000; + export const useGetUsenetInfo = (params: UsenetInfoRequestParams) => useQuery( ['usenetInfo', params.serviceId], @@ -24,9 +26,10 @@ export const useGetUsenetInfo = (params: UsenetInfoRequestParams) => }) ).data, { - refetchInterval: 1000, + refetchInterval: POLLING_INTERVAL, keepPreviousData: true, retry: 2, + enabled: !!params.serviceId, } ); @@ -40,7 +43,7 @@ export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) => }) ).data, { - refetchInterval: 1000, + refetchInterval: POLLING_INTERVAL, keepPreviousData: true, retry: 2, } @@ -56,7 +59,7 @@ export const useGetUsenetHistory = (params: UsenetHistoryRequestParams) => }) ).data, { - refetchInterval: 1000, + refetchInterval: POLLING_INTERVAL, keepPreviousData: true, retry: 2, } From 4db9dba2f469944b031e3c1bc50a70805c7d9d02 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Fri, 26 Aug 2022 16:47:26 +0200 Subject: [PATCH 11/17] lint --- @types/react-i18next.d.ts | 68 -------------------- src/components/AppShelf/SmallServiceItem.tsx | 2 +- 2 files changed, 1 insertion(+), 69 deletions(-) delete mode 100644 @types/react-i18next.d.ts diff --git a/@types/react-i18next.d.ts b/@types/react-i18next.d.ts deleted file mode 100644 index 30da9af59..000000000 --- a/@types/react-i18next.d.ts +++ /dev/null @@ -1,68 +0,0 @@ -import 'react-i18next'; - -import common from '../public/locales/en/common.json'; -import appShelf from '../public/locales/en/layout/app-shelf.json'; -import addServiceAppShelf from '../public/locales/en/layout/add-service-app-shelf.json'; -import appShelfMenu from '../public/locales/en/layout/app-shelf-menu.json'; -import commonSettings from '../public/locales/en/settings/common.json'; -import themeSelector from '../public/locales/en/settings/general/theme-selector.json'; -import configChanger from '../public/locales/en/settings/general/config-changer.json'; -import i18n from '../public/locales/en/settings/general/internationalization.json'; -import moduleEnabler from '../public/locales/en/settings/general/module-enabler.json'; -import searchEngine from '../public/locales/en/settings/general/search-engine.json'; -import widgetPositions from '../public/locales/en/settings/general/widget-positions.json'; -import colorSelector from '../public/locales/en/settings/customization/color-selector.json'; -import pageAppearance from '../public/locales/en/settings/customization/page-appearance.json'; -import shadeSelector from '../public/locales/en/settings/customization/shade-selector.json'; -import appWidth from '../public/locales/en/settings/customization/app-width.json'; -import opacitySelector from '../public/locales/en/settings/customization/opacity-selector.json'; -import commonModule from '../public/locales/en/modules/common.json'; -import dateModule from '../public/locales/en/modules/date.json'; -import calendarModule from '../public/locales/en/modules/calendar.json'; -import dlSpeedModule from '../public/locales/en/modules/dlspeed.json'; -import usenetModule from '../public/locales/en/modules/usenet.json'; -import searchModule from '../public/locales/en/modules/search.json'; -import torrentsModule from '../public/locales/en/modules/torrents-status.json'; -import weatherModule from '../public/locales/en/modules/weather.json'; -import pingModule from '../public/locales/en/modules/ping.json'; -import dockerModule from '../public/locales/en/modules/docker.json'; -import dashDotModule from '../public/locales/en/modules/dashdot.json'; -import overseerrModule from '../public/locales/en/modules/overseerr.json'; -import mediaCardsModule from '../public/locales/en/modules/common-media-cards.json'; - -declare module 'react-i18next' { - interface CustomTypeOptions { - defaultNS: 'common'; - resources: { - common: typeof common; - 'layout/app-shelf': typeof appShelf; - 'layout/add-service-app-shelf': typeof addServiceAppShelf; - 'layout/app-shelf-menu': typeof appShelfMenu; - 'settings/common': typeof commonSettings; - 'settings/general/theme-selector': typeof themeSelector; - 'settings/general/config-changer': typeof configChanger; - 'settings/general/internationalization': typeof i18n; - 'settings/general/module-enabler': typeof moduleEnabler; - 'settings/general/search-engine': typeof searchEngine; - 'settings/general/widget-positions': typeof widgetPositions; - 'settings/customization/color-selector': typeof colorSelector; - 'settings/customization/page-appearance': typeof pageAppearance; - 'settings/customization/shade-selector': typeof shadeSelector; - 'settings/customization/app-width': typeof appWidth; - 'settings/customization/opacity-selector': typeof opacitySelector; - 'modules/common': typeof commonModule; - 'modules/date': typeof dateModule; - 'modules/calendar': typeof calendarModule; - 'modules/dlspeed': typeof dlSpeedModule; - 'modules/usenet': typeof usenetModule; - 'modules/search': typeof searchModule; - 'modules/torrents-status': typeof torrentsModule; - 'modules/weather': typeof weatherModule; - 'modules/ping': typeof pingModule; - 'modules/docker': typeof dockerModule; - 'modules/dashdot': typeof dashDotModule; - 'modules/overseerr': typeof overseerrModule; - 'modules/common-media-cards': typeof mediaCardsModule; - }; - } -} diff --git a/src/components/AppShelf/SmallServiceItem.tsx b/src/components/AppShelf/SmallServiceItem.tsx index 46cb29db6..98c8bbf53 100644 --- a/src/components/AppShelf/SmallServiceItem.tsx +++ b/src/components/AppShelf/SmallServiceItem.tsx @@ -1,4 +1,4 @@ -import { Anchor, Avatar, Group, Text } from '@mantine/core'; +import { Avatar, Group, Text } from '@mantine/core'; interface smallServiceItem { label: string; From 12e7eb63575e5faa419415c075ab27ac85c9f346 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Fri, 26 Aug 2022 21:38:28 +0200 Subject: [PATCH 12/17] Address PR comments --- public/locales/en/common.json | 9 ++++++-- public/locales/en/modules/usenet.json | 10 +++++++-- src/modules/usenet/UsenetHistoryList.tsx | 27 +++++++++++++----------- src/modules/usenet/UsenetQueueList.tsx | 10 +++++++-- src/tools/parseDuration.ts | 20 ++++++++++++++++++ 5 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 src/tools/parseDuration.ts diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 9a0b9a9d9..64232fcf5 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -2,5 +2,10 @@ "actions": { "save": "Save" }, - "tip": "Tip: " -} \ No newline at end of file + "tip": "Tip: ", + "time": { + "seconds": "seconds", + "minutes": "minutes", + "hours": "hours" + } +} diff --git a/public/locales/en/modules/usenet.json b/public/locales/en/modules/usenet.json index 30231fb80..efdd2adfb 100644 --- a/public/locales/en/modules/usenet.json +++ b/public/locales/en/modules/usenet.json @@ -27,7 +27,10 @@ "progress": "Progress" }, "empty": "Queue is empty.", - "error": "Some error has occured while fetching data:", + "error": { + "title": "Error!", + "message": "Some error has occured while fetching data:" + }, "paused": "Paused" }, "history": { @@ -37,7 +40,10 @@ "duration": "Download Duration" }, "empty": "Queue is empty.", - "error": "Some error has occured while fetching data:", + "error": { + "title": "Error!", + "message": "Some error has occured while fetching data:" + }, "paused": "Paused" } } diff --git a/src/modules/usenet/UsenetHistoryList.tsx b/src/modules/usenet/UsenetHistoryList.tsx index ba5a29601..f886e5ad3 100644 --- a/src/modules/usenet/UsenetHistoryList.tsx +++ b/src/modules/usenet/UsenetHistoryList.tsx @@ -18,6 +18,7 @@ import { useTranslation } from 'next-i18next'; import { FunctionComponent, useState } from 'react'; import { useGetUsenetHistory } from '../../tools/hooks/api'; import { humanFileSize } from '../../tools/humanFileSize'; +import { parseDuration } from '../../tools/parseDuration'; dayjs.extend(duration); @@ -29,7 +30,7 @@ const PAGE_SIZE = 10; export const UsenetHistoryList: FunctionComponent = ({ serviceId }) => { const [page, setPage] = useState(1); - const { t } = useTranslation('modules/usenet'); + const { t } = useTranslation(['modules/usenet', 'common']); const { data, isLoading, isError, error } = useGetUsenetHistory({ limit: PAGE_SIZE, @@ -51,8 +52,14 @@ export const UsenetHistoryList: FunctionComponent = ({ s if (isError) { return ( - } my="lg" title="Error!" color="red" radius="md"> - {t('history.error')} + } + my="lg" + title={t('modules/usenet:history.error.title')} + color="red" + radius="md" + > + {t('modules/usenet:history.error.message')} {(error as AxiosError)?.response?.data as string} @@ -64,7 +71,7 @@ export const UsenetHistoryList: FunctionComponent = ({ s if (!data || data.items.length <= 0) { return (
- {t('history.empty')} + {t('modules/usenet:history.empty')}
); } @@ -79,9 +86,9 @@ export const UsenetHistoryList: FunctionComponent = ({ s - {t('history.header.name')} - {t('history.header.size')} - {t('history.header.duration')} + {t('modules/usenet:history.header.name')} + {t('modules/usenet:history.header.size')} + {t('modules/usenet:history.header.duration')} @@ -105,11 +112,7 @@ export const UsenetHistoryList: FunctionComponent = ({ s {humanFileSize(history.size)} - - {dayjs - .duration(history.time, 's') - .format(history.time < 60 ? 's [seconds]' : 'm [minutes] s [seconds] ')} - + {parseDuration(history.time, t)} ))} diff --git a/src/modules/usenet/UsenetQueueList.tsx b/src/modules/usenet/UsenetQueueList.tsx index bc6d5ea0c..e1e4d59f2 100644 --- a/src/modules/usenet/UsenetQueueList.tsx +++ b/src/modules/usenet/UsenetQueueList.tsx @@ -55,8 +55,14 @@ export const UsenetQueueList: FunctionComponent = ({ servi if (isError) { return ( - } my="lg" title="Error!" color="red" radius="md"> - {t('queue.error')} + } + my="lg" + title={t('queue.error.title')} + color="red" + radius="md" + > + {t('queue.error.message')} {(error as AxiosError)?.response?.data as string} diff --git a/src/tools/parseDuration.ts b/src/tools/parseDuration.ts new file mode 100644 index 000000000..d59de519a --- /dev/null +++ b/src/tools/parseDuration.ts @@ -0,0 +1,20 @@ +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import { TFunction } from 'next-i18next'; + +dayjs.extend(duration); + +export const parseDuration = (time: number, t: TFunction): string => { + const etaDuration = dayjs.duration(time, 's'); + + let eta = etaDuration.format(`s [${t('common:time.seconds')}]`); + + if (etaDuration.asMinutes() > 1) { + eta = etaDuration.format(`m [${t('common:time.minutes')}] `) + eta; + } + if (etaDuration.asHours() > 1) { + eta = etaDuration.format(`H [${t('common:time.hours')}] `) + eta; + } + + return eta; +}; From 4b2cd519f708c54db33e6fd33423d64f30570f3f Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Wed, 31 Aug 2022 18:00:10 +0200 Subject: [PATCH 13/17] Quick FIX --- src/components/AppShelf/AppShelf.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx index 974729dac..e83ca7f5d 100644 --- a/src/components/AppShelf/AppShelf.tsx +++ b/src/components/AppShelf/AppShelf.tsx @@ -18,6 +18,7 @@ import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem'; import { ModuleMenu, ModuleWrapper } from '../../modules/moduleWrapper'; import { UsenetModule, TorrentsModule } from '../../modules'; import TorrentsComponent from '../../modules/torrents/TorrentsModule'; +import UsenetComponent from '../../modules/usenet/UsenetModule'; const AppShelf = (props: any) => { const { config, setConfig } = useConfig(); @@ -126,7 +127,11 @@ const AppShelf = (props: any) => { const noCategory = config.services.filter( (e) => e.category === undefined || e.category === null ); - const downloadEnabled = config.modules?.[TorrentsModule.id]?.enabled ?? false; + + const usenetEnabled = config.modules?.[TorrentsModule.id]?.enabled ?? false; + const torrentEnabled = config.modules?.[UsenetModule.id]?.enabled ?? false; + + const downloadEnabled = usenetEnabled || torrentEnabled; // Create an item with 0: true, 1: true, 2: true... For each category return ( // TODO: Style accordion so that the bar is transparent to the user settings @@ -170,8 +175,18 @@ const AppShelf = (props: any) => { ${(config.settings.appOpacity || 100) / 100}`, }} > - - + {torrentEnabled && ( + <> + + + + )} + {usenetEnabled && ( + <> + + + + )} From 5dfc1e6f4ca9bb9b7840bb32c808bb5bcf992755 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Wed, 31 Aug 2022 19:13:34 +0200 Subject: [PATCH 14/17] Fix linebreak and url --- src/modules/usenet/UsenetQueueList.tsx | 4 +++- src/pages/api/modules/usenet/history.ts | 3 ++- src/pages/api/modules/usenet/index.ts | 4 +++- src/pages/api/modules/usenet/pause.ts | 4 +++- src/pages/api/modules/usenet/queue.ts | 4 +++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/modules/usenet/UsenetQueueList.tsx b/src/modules/usenet/UsenetQueueList.tsx index e1e4d59f2..a000dbd20 100644 --- a/src/modules/usenet/UsenetQueueList.tsx +++ b/src/modules/usenet/UsenetQueueList.tsx @@ -137,7 +137,9 @@ export const UsenetQueueList: FunctionComponent = ({ servi )} - {nzb.progress.toFixed(1)}% + + {nzb.progress.toFixed(1)}% + 0 ? theme.primaryColor : 'lightgrey'} diff --git a/src/pages/api/modules/usenet/history.ts b/src/pages/api/modules/usenet/history.ts index c40c72322..4143428f4 100644 --- a/src/pages/api/modules/usenet/history.ts +++ b/src/pages/api/modules/usenet/history.ts @@ -36,8 +36,9 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { if (!service.apiKey) { throw new Error(`API Key for service "${service.name}" is missing`); } + const { origin } = new URL(service.url); - const history = await new Client(service.url, service.apiKey).history(offset, limit); + const history = await new Client(origin, service.apiKey).history(offset, limit); const items: UsenetHistoryItem[] = history.slots.map((slot) => ({ id: slot.nzo_id, diff --git a/src/pages/api/modules/usenet/index.ts b/src/pages/api/modules/usenet/index.ts index 60217decb..f40d2c11a 100644 --- a/src/pages/api/modules/usenet/index.ts +++ b/src/pages/api/modules/usenet/index.ts @@ -36,7 +36,9 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { throw new Error(`API Key for service "${service.name}" is missing`); } - const queue = await new Client(service.url, service.apiKey).queue(0, -1); + const { origin } = new URL(service.url); + + const queue = await new Client(origin, service.apiKey).queue(0, -1); const [hours, minutes, seconds] = queue.timeleft.split(':'); const eta = dayjs.duration({ diff --git a/src/pages/api/modules/usenet/pause.ts b/src/pages/api/modules/usenet/pause.ts index 816158562..53270d6be 100644 --- a/src/pages/api/modules/usenet/pause.ts +++ b/src/pages/api/modules/usenet/pause.ts @@ -29,7 +29,9 @@ async function Post(req: NextApiRequest, res: NextApiResponse) { throw new Error(`API Key for service "${service.name}" is missing`); } - const result = await new Client(service.url, service.apiKey).queuePause(); + const { origin } = new URL(service.url); + + const result = await new Client(origin, service.apiKey).queuePause(); return res.status(200).json(result); } catch (err) { diff --git a/src/pages/api/modules/usenet/queue.ts b/src/pages/api/modules/usenet/queue.ts index 8eab23fff..d1fc07d1d 100644 --- a/src/pages/api/modules/usenet/queue.ts +++ b/src/pages/api/modules/usenet/queue.ts @@ -36,7 +36,9 @@ async function Get(req: NextApiRequest, res: NextApiResponse) { if (!service.apiKey) { throw new Error(`API Key for service "${service.name}" is missing`); } - const queue = await new Client(service.url, service.apiKey).queue(offset, limit); + + const { origin } = new URL(service.url); + const queue = await new Client(origin, service.apiKey).queue(offset, limit); const items: UsenetQueueItem[] = queue.slots.map((slot) => { const [hours, minutes, seconds] = slot.timeleft.split(':'); From d5d8f45ece0c47f90eda6d882013b7a37167aa46 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Wed, 31 Aug 2022 19:16:16 +0200 Subject: [PATCH 15/17] Fix resume --- src/pages/api/modules/usenet/resume.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/api/modules/usenet/resume.ts b/src/pages/api/modules/usenet/resume.ts index adf8a9372..f3b155f06 100644 --- a/src/pages/api/modules/usenet/resume.ts +++ b/src/pages/api/modules/usenet/resume.ts @@ -30,7 +30,9 @@ async function Post(req: NextApiRequest, res: NextApiResponse) { throw new Error(`API Key for service "${service.name}" is missing`); } - const result = await new Client(service.url, service.apiKey).queueResume(); + const { origin } = new URL(service.url); + + const result = await new Client(origin, service.apiKey).queueResume(); return res.status(200).json(result); } catch (err) { From 3d98b389558a43ee44b53c46325ab564c0f337eb Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Thu, 1 Sep 2022 10:36:46 +0200 Subject: [PATCH 16/17] Fix lint --- src/components/AppShelf/AppShelf.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx index e83ca7f5d..c125aba92 100644 --- a/src/components/AppShelf/AppShelf.tsx +++ b/src/components/AppShelf/AppShelf.tsx @@ -18,7 +18,7 @@ import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem'; import { ModuleMenu, ModuleWrapper } from '../../modules/moduleWrapper'; import { UsenetModule, TorrentsModule } from '../../modules'; import TorrentsComponent from '../../modules/torrents/TorrentsModule'; -import UsenetComponent from '../../modules/usenet/UsenetModule'; +import { UsenetComponent } from '../../modules/usenet/UsenetModule'; const AppShelf = (props: any) => { const { config, setConfig } = useConfig(); From 91a8510859c6338fe725a9227ea5cae635547b84 Mon Sep 17 00:00:00 2001 From: Jannes Vandepitte Date: Thu, 1 Sep 2022 11:01:47 +0200 Subject: [PATCH 17/17] Cleanup --- src/components/AppShelf/AppShelf.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx index c125aba92..4cc1212c3 100644 --- a/src/components/AppShelf/AppShelf.tsx +++ b/src/components/AppShelf/AppShelf.tsx @@ -1,5 +1,13 @@ import React, { useState } from 'react'; -import { Accordion, Grid, Paper, Stack, useMantineColorScheme } from '@mantine/core'; +import { + Accordion, + Divider, + Grid, + Paper, + Stack, + Title, + useMantineColorScheme, +} from '@mantine/core'; import { closestCenter, DndContext, @@ -128,8 +136,8 @@ const AppShelf = (props: any) => { (e) => e.category === undefined || e.category === null ); - const usenetEnabled = config.modules?.[TorrentsModule.id]?.enabled ?? false; - const torrentEnabled = config.modules?.[UsenetModule.id]?.enabled ?? false; + const torrentEnabled = config.modules?.[TorrentsModule.id]?.enabled ?? false; + const usenetEnabled = config.modules?.[UsenetModule.id]?.enabled ?? false; const downloadEnabled = usenetEnabled || torrentEnabled; // Create an item with 0: true, 1: true, 2: true... For each category @@ -164,7 +172,6 @@ const AppShelf = (props: any) => { {t('accordions.downloads.text')} { > {torrentEnabled && ( <> + Torrents )} {usenetEnabled && ( <> + {torrentEnabled && } + + Usenet +