mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-13 08:55:48 +01:00
@@ -24,7 +24,7 @@ export const downloadRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
configName: z.string(),
|
configName: z.string(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const config = getConfig(input.configName);
|
const config = getConfig(input.configName);
|
||||||
@@ -44,7 +44,7 @@ export const downloadRouter = createTRPCRouter({
|
|||||||
return response;
|
return response;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Consola.error(
|
Consola.error(
|
||||||
`Error communicating with your download client '${app.name}' (${app.id}): ${err}`
|
`Error communicating with your download client '${app.name}' (${app.id}): ${err}`,
|
||||||
);
|
);
|
||||||
failedClients.push(app.id);
|
failedClients.push(app.id);
|
||||||
return {
|
return {
|
||||||
@@ -67,7 +67,7 @@ export const downloadRouter = createTRPCRouter({
|
|||||||
|
|
||||||
if (failedClients.length > 0) {
|
if (failedClients.length > 0) {
|
||||||
Consola.warn(
|
Consola.warn(
|
||||||
`${failedClients.length} download clients failed. Please check your configuration and the above log`
|
`${failedClients.length} download clients failed. Please check your configuration and the above log`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +76,31 @@ export const downloadRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const GetDataFromClient = async (
|
const GetDataFromClient = async (
|
||||||
app: ConfigAppType
|
app: ConfigAppType,
|
||||||
): Promise<NormalizedDownloadAppStat | undefined> => {
|
): Promise<NormalizedDownloadAppStat | undefined> => {
|
||||||
const reduceTorrent = (data: AllClientData): NormalizedDownloadAppStat => ({
|
const reduceTorrent = (data: AllClientData): NormalizedDownloadAppStat => ({
|
||||||
type: 'torrent',
|
type: 'torrent',
|
||||||
appId: app.id,
|
appId: app.id,
|
||||||
success: true,
|
success: true,
|
||||||
torrents: data.torrents,
|
torrents: data.torrents.map((torrent) => ({
|
||||||
|
name: torrent.name,
|
||||||
|
eta: torrent.eta,
|
||||||
|
state: torrent.state,
|
||||||
|
progress: torrent.progress,
|
||||||
|
totalSeeds: torrent.totalSeeds,
|
||||||
|
totalSelected: torrent.totalSelected,
|
||||||
|
totalPeers: torrent.totalPeers,
|
||||||
|
ratio: torrent.ratio,
|
||||||
|
uploadSpeed: torrent.uploadSpeed,
|
||||||
|
downloadSpeed: torrent.downloadSpeed,
|
||||||
|
isCompleted: torrent.isCompleted,
|
||||||
|
totalDownloaded: torrent.totalDownloaded,
|
||||||
|
totalUploaded: torrent.totalUploaded,
|
||||||
|
label: torrent.label,
|
||||||
|
queuePosition: torrent.queuePosition,
|
||||||
|
stateMessage: torrent.stateMessage,
|
||||||
|
dateAdded: torrent.dateAdded
|
||||||
|
})),
|
||||||
totalDownload: data.torrents
|
totalDownload: data.torrents
|
||||||
.map((torrent) => torrent.downloadSpeed)
|
.map((torrent) => torrent.downloadSpeed)
|
||||||
.reduce((acc, torrent) => acc + torrent, 0),
|
.reduce((acc, torrent) => acc + torrent, 0),
|
||||||
@@ -100,7 +118,7 @@ const GetDataFromClient = async (
|
|||||||
await new Deluge({
|
await new Deluge({
|
||||||
baseUrl: app.url,
|
baseUrl: app.url,
|
||||||
password: findField(app, 'password'),
|
password: findField(app, 'password'),
|
||||||
}).getAllData()
|
}).getAllData(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'transmission': {
|
case 'transmission': {
|
||||||
@@ -109,7 +127,7 @@ const GetDataFromClient = async (
|
|||||||
baseUrl: app.url,
|
baseUrl: app.url,
|
||||||
username: findField(app, 'username'),
|
username: findField(app, 'username'),
|
||||||
password: findField(app, 'password'),
|
password: findField(app, 'password'),
|
||||||
}).getAllData()
|
}).getAllData(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'qBittorrent': {
|
case 'qBittorrent': {
|
||||||
@@ -118,7 +136,7 @@ const GetDataFromClient = async (
|
|||||||
baseUrl: app.url,
|
baseUrl: app.url,
|
||||||
username: findField(app, 'username'),
|
username: findField(app, 'username'),
|
||||||
password: findField(app, 'password'),
|
password: findField(app, 'password'),
|
||||||
}).getAllData()
|
}).getAllData(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'sabnzbd': {
|
case 'sabnzbd': {
|
||||||
|
|||||||
@@ -13,7 +13,25 @@ export type NormalizedDownloadAppStat = {
|
|||||||
|
|
||||||
export type TorrentTotalDownload = {
|
export type TorrentTotalDownload = {
|
||||||
type: 'torrent';
|
type: 'torrent';
|
||||||
torrents: NormalizedTorrent[];
|
torrents: {
|
||||||
|
name: string;
|
||||||
|
state: NormalizedTorrent['state'];
|
||||||
|
totalSelected: number;
|
||||||
|
totalPeers: number;
|
||||||
|
totalSeeds: number;
|
||||||
|
eta: number;
|
||||||
|
progress: number;
|
||||||
|
ratio: number;
|
||||||
|
uploadSpeed: number;
|
||||||
|
downloadSpeed: number;
|
||||||
|
isCompleted: boolean;
|
||||||
|
totalDownloaded: number;
|
||||||
|
totalUploaded: number;
|
||||||
|
label?: string;
|
||||||
|
queuePosition: number;
|
||||||
|
stateMessage: string;
|
||||||
|
dateAdded: string;
|
||||||
|
}[];
|
||||||
totalDownload: number;
|
totalDownload: number;
|
||||||
totalUpload: number;
|
totalUpload: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -9,7 +8,6 @@ import {
|
|||||||
Progress,
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
createStyles,
|
|
||||||
useMantineTheme
|
useMantineTheme
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
@@ -25,9 +23,10 @@ import {
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { humanFileSize } from '~/tools/humanFileSize';
|
import { humanFileSize } from '~/tools/humanFileSize';
|
||||||
import { AppType } from '~/types/app';
|
import { AppType } from '~/types/app';
|
||||||
|
import { TorrentTotalDownload } from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
|
||||||
interface TorrentQueueItemProps {
|
interface TorrentQueueItemProps {
|
||||||
torrent: NormalizedTorrent;
|
torrent: TorrentTotalDownload['torrents'][0];
|
||||||
app?: AppType;
|
app?: AppType;
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
@@ -147,9 +146,3 @@ export const TorrentQueuePopover = ({ torrent, app }: Omit<TorrentQueueItemProps
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles(() => ({
|
|
||||||
noTextBreak: {
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { type MRT_ColumnDef, MRT_Table, useMantineReactTable } from 'mantine-react-table';
|
||||||
MRT_Table,
|
|
||||||
useMantineReactTable,
|
|
||||||
type MRT_ColumnDef,
|
|
||||||
} from 'mantine-react-table';
|
|
||||||
|
|
||||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Center,
|
Center,
|
||||||
@@ -26,12 +20,13 @@ import duration from 'dayjs/plugin/duration';
|
|||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useCardStyles } from '~/components/layout/Common/useCardStyles';
|
|
||||||
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
|
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
|
||||||
import { calculateETA } from '~/tools/client/calculateEta';
|
import { calculateETA } from '~/tools/client/calculateEta';
|
||||||
import { humanFileSize } from '~/tools/humanFileSize';
|
import { humanFileSize } from '~/tools/humanFileSize';
|
||||||
import { NormalizedDownloadQueueResponse } from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
import {
|
||||||
import { AppIntegrationType } from '~/types/app';
|
NormalizedDownloadQueueResponse,
|
||||||
|
TorrentTotalDownload,
|
||||||
|
} from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||||
|
|
||||||
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
|
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
@@ -41,8 +36,6 @@ import { TorrentQueuePopover } from './TorrentQueueItem';
|
|||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const downloadAppTypes: AppIntegrationType['type'][] = ['deluge', 'qBittorrent', 'transmission'];
|
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
id: 'torrents-status',
|
id: 'torrents-status',
|
||||||
icon: IconFileDownload,
|
icon: IconFileDownload,
|
||||||
@@ -96,7 +89,6 @@ interface TorrentTileProps {
|
|||||||
function TorrentTile({ widget }: TorrentTileProps) {
|
function TorrentTile({ widget }: TorrentTileProps) {
|
||||||
const { t } = useTranslation('modules/torrents-status');
|
const { t } = useTranslation('modules/torrents-status');
|
||||||
const { width, ref } = useElementSize();
|
const { width, ref } = useElementSize();
|
||||||
const { classes } = useCardStyles(true);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -110,9 +102,9 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
dataUpdatedAt: number;
|
dataUpdatedAt: number;
|
||||||
} = useGetDownloadClientsQueue();
|
} = useGetDownloadClientsQueue();
|
||||||
|
|
||||||
let torrents: NormalizedTorrent[] = [];
|
let torrents: TorrentTotalDownload['torrents'] = [];
|
||||||
if(!(isError || !data || data.apps.length === 0 || Object.values(data.apps).length < 1)) {
|
if (!(isError || !data || data.apps.length === 0 || Object.values(data.apps).length < 1)) {
|
||||||
torrents = data.apps.flatMap((app) => (app.type === 'torrent' ? app.torrents : []))
|
torrents = data.apps.flatMap((app) => (app.type === 'torrent' ? app.torrents : []));
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredTorrents = filterTorrents(widget, torrents);
|
const filteredTorrents = filterTorrents(widget, torrents);
|
||||||
@@ -125,29 +117,29 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
const ratioGlobal = getTorrentsRatio(widget, torrents, false);
|
const ratioGlobal = getTorrentsRatio(widget, torrents, false);
|
||||||
const ratioWithFilter = getTorrentsRatio(widget, torrents, true);
|
const ratioWithFilter = getTorrentsRatio(widget, torrents, true);
|
||||||
|
|
||||||
const columns = useMemo<MRT_ColumnDef<NormalizedTorrent>[]>(() => [
|
const columns = useMemo<MRT_ColumnDef<TorrentTotalDownload['torrents'][0]>[]>(() => [
|
||||||
{
|
{
|
||||||
id: "dateAdded",
|
id: 'dateAdded',
|
||||||
accessorFn: (row) => new Date(row.dateAdded),
|
accessorFn: (row) => new Date(row.dateAdded),
|
||||||
header: "dateAdded",
|
header: 'dateAdded',
|
||||||
maxSize: 1
|
maxSize: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: t('card.table.header.name'),
|
header: t('card.table.header.name'),
|
||||||
Cell: ({ cell, row }) => (
|
Cell: ({ cell, row }) => (
|
||||||
<Popover
|
<Popover
|
||||||
withArrow
|
withArrow
|
||||||
withinPortal
|
withinPortal
|
||||||
radius="lg"
|
radius="lg"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
transitionProps={{
|
transitionProps={{
|
||||||
transition: 'pop',
|
transition: 'pop',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Text
|
<Text
|
||||||
maw={"30vw"}
|
maw={'30vw'}
|
||||||
size="xs"
|
size="xs"
|
||||||
lineClamp={1}
|
lineClamp={1}
|
||||||
>
|
>
|
||||||
@@ -160,35 +152,35 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
</Popover>
|
</Popover>
|
||||||
),
|
),
|
||||||
maxSize: 1,
|
maxSize: 1,
|
||||||
size: 1
|
size: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'totalSelected',
|
accessorKey: 'totalSelected',
|
||||||
header: t('card.table.header.size'),
|
header: t('card.table.header.size'),
|
||||||
Cell: ({ cell }) => formatSize(Number(cell.getValue())),
|
Cell: ({ cell }) => formatSize(Number(cell.getValue())),
|
||||||
sortDescFirst: true,
|
sortDescFirst: true,
|
||||||
maxSize: 1
|
maxSize: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'uploadSpeed',
|
accessorKey: 'uploadSpeed',
|
||||||
header: t('card.table.header.upload'),
|
header: t('card.table.header.upload'),
|
||||||
Cell: ({ cell }) => formatSpeed(Number(cell.getValue())),
|
Cell: ({ cell }) => formatSpeed(Number(cell.getValue())),
|
||||||
sortDescFirst: true,
|
sortDescFirst: true,
|
||||||
maxSize: 1
|
maxSize: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'downloadSpeed',
|
accessorKey: 'downloadSpeed',
|
||||||
header: t('card.table.header.download'),
|
header: t('card.table.header.download'),
|
||||||
Cell: ({ cell }) => formatSpeed(Number(cell.getValue())),
|
Cell: ({ cell }) => formatSpeed(Number(cell.getValue())),
|
||||||
sortDescFirst: true,
|
sortDescFirst: true,
|
||||||
maxSize: 1
|
maxSize: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'eta',
|
accessorKey: 'eta',
|
||||||
header: t('card.table.header.estimatedTimeOfArrival'),
|
header: t('card.table.header.estimatedTimeOfArrival'),
|
||||||
Cell: ({ cell }) => formatETA(Number(cell.getValue())),
|
Cell: ({ cell }) => formatETA(Number(cell.getValue())),
|
||||||
sortDescFirst: true,
|
sortDescFirst: true,
|
||||||
maxSize: 1
|
maxSize: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'progress',
|
accessorKey: 'progress',
|
||||||
@@ -321,14 +313,14 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
|
export const filterTorrents = (widget: ITorrent, torrents: TorrentTotalDownload['torrents']) => {
|
||||||
let result = torrents;
|
let result = torrents;
|
||||||
if (!widget.properties.displayCompletedTorrents) {
|
if (!widget.properties.displayCompletedTorrents) {
|
||||||
result = result.filter(
|
result = result.filter(
|
||||||
(torrent) =>
|
(torrent) =>
|
||||||
!torrent.isCompleted ||
|
!torrent.isCompleted ||
|
||||||
(widget.properties.displayActiveTorrents &&
|
(widget.properties.displayActiveTorrents &&
|
||||||
torrent.uploadSpeed > widget.properties.speedLimitOfActiveTorrents * 1024)
|
torrent.uploadSpeed > widget.properties.speedLimitOfActiveTorrents * 1024),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +328,7 @@ export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[])
|
|||||||
result = filterTorrentsByLabels(
|
result = filterTorrentsByLabels(
|
||||||
result,
|
result,
|
||||||
widget.properties.labelFilter,
|
widget.properties.labelFilter,
|
||||||
widget.properties.labelFilterIsWhitelist
|
widget.properties.labelFilterIsWhitelist,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +337,7 @@ export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[])
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterStaleTorrent = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
|
const filterStaleTorrent = (widget: ITorrent, torrents: TorrentTotalDownload['torrents']) => {
|
||||||
if (widget.properties.displayStaleTorrents) {
|
if (widget.properties.displayStaleTorrents) {
|
||||||
return torrents;
|
return torrents;
|
||||||
}
|
}
|
||||||
@@ -354,9 +346,9 @@ const filterStaleTorrent = (widget: ITorrent, torrents: NormalizedTorrent[]) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filterTorrentsByLabels = (
|
const filterTorrentsByLabels = (
|
||||||
torrents: NormalizedTorrent[],
|
torrents: TorrentTotalDownload['torrents'],
|
||||||
labels: string[],
|
labels: string[],
|
||||||
isWhitelist: boolean
|
isWhitelist: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (isWhitelist) {
|
if (isWhitelist) {
|
||||||
return torrents.filter((torrent) => torrent.label && labels.includes(torrent.label));
|
return torrents.filter((torrent) => torrent.label && labels.includes(torrent.label));
|
||||||
@@ -367,8 +359,8 @@ const filterTorrentsByLabels = (
|
|||||||
|
|
||||||
export const getTorrentsRatio = (
|
export const getTorrentsRatio = (
|
||||||
widget: ITorrent,
|
widget: ITorrent,
|
||||||
torrents: NormalizedTorrent[],
|
torrents: TorrentTotalDownload['torrents'],
|
||||||
applyAllFilter: boolean
|
applyAllFilter: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (applyAllFilter) {
|
if (applyAllFilter) {
|
||||||
torrents = filterTorrents(widget, torrents);
|
torrents = filterTorrents(widget, torrents);
|
||||||
@@ -376,18 +368,18 @@ export const getTorrentsRatio = (
|
|||||||
torrents = filterTorrentsByLabels(
|
torrents = filterTorrentsByLabels(
|
||||||
torrents,
|
torrents,
|
||||||
widget.properties.labelFilter,
|
widget.properties.labelFilter,
|
||||||
widget.properties.labelFilterIsWhitelist
|
widget.properties.labelFilterIsWhitelist,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalDownloadedSum = torrents.reduce(
|
let totalDownloadedSum = torrents.reduce(
|
||||||
(sum, torrent) => sum + torrent.totalDownloaded,
|
(sum, torrent) => sum + torrent.totalDownloaded,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return totalDownloadedSum > 0
|
return totalDownloadedSum > 0
|
||||||
? torrents.reduce((sum, torrent) => sum + torrent.totalUploaded, 0) /
|
? torrents.reduce((sum, torrent) => sum + torrent.totalUploaded, 0) /
|
||||||
totalDownloadedSum
|
totalDownloadedSum
|
||||||
: -1;
|
: -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user