mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-18 11:11:10 +01:00
✨ Add label filter for torrent widget (#865)
This commit is contained in:
214
src/widgets/torrent/TorrentTile.spec.ts
Normal file
214
src/widgets/torrent/TorrentTile.spec.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { NormalizedTorrent, TorrentState } from '@ctrl/shared-torrent';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { ITorrent, filterTorrents } from './TorrentTile';
|
||||
|
||||
describe('TorrentTile', () => {
|
||||
it('filter torrents when stale', () => {
|
||||
// arrange
|
||||
const widget: ITorrent = {
|
||||
id: 'abc',
|
||||
area: {
|
||||
type: 'sidebar',
|
||||
properties: {
|
||||
location: 'left',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
type: 'torrents-status',
|
||||
properties: {
|
||||
labelFilter: [],
|
||||
labelFilterIsWhitelist: false,
|
||||
displayCompletedTorrents: true,
|
||||
displayStaleTorrents: false,
|
||||
},
|
||||
};
|
||||
const torrents: NormalizedTorrent[] = [
|
||||
constructTorrent('ABC', 'Nice Torrent', false, 672),
|
||||
constructTorrent('HH', 'I am completed', true, 0),
|
||||
constructTorrent('HH', 'I am stale', false, 0),
|
||||
];
|
||||
|
||||
// act
|
||||
const filtered = filterTorrents(widget, torrents);
|
||||
|
||||
// assert
|
||||
expect(filtered.length).toBe(2);
|
||||
expect(filtered.includes(torrents[0])).toBe(true);
|
||||
expect(filtered.includes(torrents[1])).toBe(true);
|
||||
expect(filtered.includes(torrents[2])).toBe(false);
|
||||
});
|
||||
|
||||
it('not filter torrents when stale', () => {
|
||||
// arrange
|
||||
const widget: ITorrent = {
|
||||
id: 'abc',
|
||||
area: {
|
||||
type: 'sidebar',
|
||||
properties: {
|
||||
location: 'left',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
type: 'torrents-status',
|
||||
properties: {
|
||||
labelFilter: [],
|
||||
labelFilterIsWhitelist: false,
|
||||
displayCompletedTorrents: true,
|
||||
displayStaleTorrents: true,
|
||||
},
|
||||
};
|
||||
const torrents: NormalizedTorrent[] = [
|
||||
constructTorrent('ABC', 'Nice Torrent', false, 672),
|
||||
constructTorrent('HH', 'I am completed', true, 0),
|
||||
constructTorrent('HH', 'I am stale', false, 0),
|
||||
];
|
||||
|
||||
// act
|
||||
const filtered = filterTorrents(widget, torrents);
|
||||
|
||||
// assert
|
||||
expect(filtered.length).toBe(3);
|
||||
expect(filtered.includes(torrents[0])).toBe(true);
|
||||
expect(filtered.includes(torrents[1])).toBe(true);
|
||||
expect(filtered.includes(torrents[2])).toBe(true);
|
||||
});
|
||||
|
||||
it('filter when completed', () => {
|
||||
// arrange
|
||||
const widget: ITorrent = {
|
||||
id: 'abc',
|
||||
area: {
|
||||
type: 'sidebar',
|
||||
properties: {
|
||||
location: 'left',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
type: 'torrents-status',
|
||||
properties: {
|
||||
labelFilter: [],
|
||||
labelFilterIsWhitelist: false,
|
||||
displayCompletedTorrents: false,
|
||||
displayStaleTorrents: true,
|
||||
},
|
||||
};
|
||||
const torrents: NormalizedTorrent[] = [
|
||||
constructTorrent('ABC', 'Nice Torrent', false, 672),
|
||||
constructTorrent('HH', 'I am completed', true, 0),
|
||||
constructTorrent('HH', 'I am stale', false, 0),
|
||||
];
|
||||
|
||||
// act
|
||||
const filtered = filterTorrents(widget, torrents);
|
||||
|
||||
// assert
|
||||
expect(filtered.length).toBe(2);
|
||||
expect(filtered.at(0)).toBe(torrents[0]);
|
||||
expect(filtered.includes(torrents[1])).toBe(false);
|
||||
expect(filtered.at(1)).toBe(torrents[2]);
|
||||
});
|
||||
|
||||
it('filter by label when whitelist', () => {
|
||||
// arrange
|
||||
const widget: ITorrent = {
|
||||
id: 'abc',
|
||||
area: {
|
||||
type: 'sidebar',
|
||||
properties: {
|
||||
location: 'left',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
type: 'torrents-status',
|
||||
properties: {
|
||||
labelFilter: ['music', 'movie'],
|
||||
labelFilterIsWhitelist: true,
|
||||
displayCompletedTorrents: true,
|
||||
displayStaleTorrents: true,
|
||||
},
|
||||
};
|
||||
const torrents: NormalizedTorrent[] = [
|
||||
constructTorrent('1', 'A sick drop', false, 672, 'music'),
|
||||
constructTorrent('2', 'I cried', true, 0, 'movie'),
|
||||
constructTorrent('3', 'Great Animations', false, 0, 'anime'),
|
||||
];
|
||||
|
||||
// act
|
||||
const filtered = filterTorrents(widget, torrents);
|
||||
|
||||
// assert
|
||||
expect(filtered.length).toBe(2);
|
||||
expect(filtered.at(0)).toBe(torrents[0]);
|
||||
expect(filtered.at(1)).toBe(torrents[1]);
|
||||
expect(filtered.includes(torrents[2])).toBe(false);
|
||||
});
|
||||
|
||||
it('filter by label when blacklist', () => {
|
||||
// arrange
|
||||
const widget: ITorrent = {
|
||||
id: 'abc',
|
||||
area: {
|
||||
type: 'sidebar',
|
||||
properties: {
|
||||
location: 'left',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
type: 'torrents-status',
|
||||
properties: {
|
||||
labelFilter: ['music', 'movie'],
|
||||
labelFilterIsWhitelist: false,
|
||||
displayCompletedTorrents: false,
|
||||
displayStaleTorrents: true,
|
||||
},
|
||||
};
|
||||
const torrents: NormalizedTorrent[] = [
|
||||
constructTorrent('ABC', 'Nice Torrent', false, 672, 'anime'),
|
||||
constructTorrent('HH', 'I am completed', true, 0, 'movie'),
|
||||
constructTorrent('HH', 'I am stale', false, 0, 'tv'),
|
||||
];
|
||||
|
||||
// act
|
||||
const filtered = filterTorrents(widget, torrents);
|
||||
|
||||
// assert
|
||||
expect(filtered.length).toBe(2);
|
||||
expect(filtered.at(0)).toBe(torrents[0]);
|
||||
expect(filtered.includes(torrents[1])).toBe(false);
|
||||
expect(filtered.at(1)).toBe(torrents[2]);
|
||||
});
|
||||
});
|
||||
|
||||
const constructTorrent = (
|
||||
id: string,
|
||||
name: string,
|
||||
isCompleted: boolean,
|
||||
downloadSpeed: number,
|
||||
label?: string,
|
||||
): NormalizedTorrent => ({
|
||||
id,
|
||||
name,
|
||||
connectedPeers: 1,
|
||||
connectedSeeds: 4,
|
||||
dateAdded: '0',
|
||||
downloadSpeed,
|
||||
eta: 500,
|
||||
isCompleted,
|
||||
progress: 50,
|
||||
queuePosition: 1,
|
||||
ratio: 5.6,
|
||||
raw: false,
|
||||
savePath: '/downloads',
|
||||
state: TorrentState.downloading,
|
||||
stateMessage: 'Downloading',
|
||||
totalDownloaded: 23024335,
|
||||
totalPeers: 10,
|
||||
totalSeeds: 450,
|
||||
totalSize: 839539535,
|
||||
totalSelected: 0,
|
||||
totalUploaded: 378535535,
|
||||
uploadSpeed: 8349,
|
||||
label,
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Center,
|
||||
@@ -11,17 +13,22 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
import { IconFileDownload } from '@tabler/icons';
|
||||
|
||||
import { IconFileDownload, IconInfoCircle } from '@tabler/icons';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
|
||||
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
||||
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||
import { AppIntegrationType } from '../../types/app';
|
||||
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
import { MIN_WIDTH_MOBILE } from '../../constants/constants';
|
||||
import { AppIntegrationType } from '../../types/app';
|
||||
import { useGetDownloadClientsQueue } from '../../hooks/widgets/download-speed/useGetNetworkSpeed';
|
||||
import { NormalizedDownloadQueueResponse } from '../../types/api/downloads/queue/NormalizedDownloadQueueResponse';
|
||||
|
||||
import { BitTorrrentQueueItem } from './TorrentQueueItem';
|
||||
|
||||
dayjs.extend(duration);
|
||||
@@ -41,6 +48,14 @@ const definition = defineWidget({
|
||||
type: 'switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
labelFilterIsWhitelist: {
|
||||
type: 'switch',
|
||||
defaultValue: true,
|
||||
},
|
||||
labelFilter: {
|
||||
type: 'multiple-text',
|
||||
defaultValue: [] as string[],
|
||||
},
|
||||
},
|
||||
gridstack: {
|
||||
minWidth: 2,
|
||||
@@ -59,7 +74,7 @@ interface TorrentTileProps {
|
||||
|
||||
function TorrentTile({ widget }: TorrentTileProps) {
|
||||
const { t } = useTranslation('modules/torrents-status');
|
||||
const { width } = useElementSize();
|
||||
const { width, ref } = useElementSize();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -121,23 +136,17 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const torrents = data.apps
|
||||
.flatMap((app) => (app.type === 'torrent' ? app.torrents : []))
|
||||
.filter((torrent) => (widget.properties.displayCompletedTorrents ? true : !torrent.isCompleted))
|
||||
.filter((torrent) =>
|
||||
widget.properties.displayStaleTorrents
|
||||
? true
|
||||
: torrent.isCompleted || torrent.downloadSpeed > 0
|
||||
);
|
||||
const torrents = data.apps.flatMap((app) => (app.type === 'torrent' ? app.torrents : []));
|
||||
const filteredTorrents = filterTorrents(widget, torrents);
|
||||
|
||||
const difference = new Date().getTime() - dataUpdatedAt;
|
||||
const duration = dayjs.duration(difference, 'ms');
|
||||
const humanizedDuration = duration.humanize();
|
||||
|
||||
return (
|
||||
<Flex direction="column" sx={{ height: '100%' }}>
|
||||
<Flex direction="column" sx={{ height: '100%' }} ref={ref}>
|
||||
<ScrollArea sx={{ height: '100%', width: '100%' }} mb="xs">
|
||||
<Table highlightOnHover p="sm">
|
||||
<Table striped highlightOnHover p="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('card.table.header.name')}</th>
|
||||
@@ -149,9 +158,24 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{torrents.map((torrent, index) => (
|
||||
{filteredTorrents.map((torrent, index) => (
|
||||
<BitTorrrentQueueItem key={index} torrent={torrent} app={undefined} />
|
||||
))}
|
||||
|
||||
{filteredTorrents.length !== torrents.length && (
|
||||
<tr>
|
||||
<td colSpan={width > MIN_WIDTH_MOBILE ? 6 : 3}>
|
||||
<Flex gap="xs" align="center" justify="center">
|
||||
<IconInfoCircle opacity={0.7} size={18} />
|
||||
<Text align="center" color="dimmed">
|
||||
{t('card.table.body.filterHidingItems', {
|
||||
count: torrents.length - filteredTorrents.length,
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
@@ -170,4 +194,43 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
|
||||
let result = torrents;
|
||||
if (!widget.properties.displayCompletedTorrents) {
|
||||
result = result.filter((torrent) => !torrent.isCompleted);
|
||||
}
|
||||
|
||||
if (widget.properties.labelFilter.length > 0) {
|
||||
result = filterTorrentsByLabels(
|
||||
result,
|
||||
widget.properties.labelFilter,
|
||||
widget.properties.labelFilterIsWhitelist
|
||||
);
|
||||
}
|
||||
|
||||
result = filterStaleTorrent(widget, result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const filterStaleTorrent = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
|
||||
if (widget.properties.displayStaleTorrents) {
|
||||
return torrents;
|
||||
}
|
||||
|
||||
return torrents.filter((torrent) => torrent.isCompleted || torrent.downloadSpeed > 0);
|
||||
};
|
||||
|
||||
const filterTorrentsByLabels = (
|
||||
torrents: NormalizedTorrent[],
|
||||
labels: string[],
|
||||
isWhitelist: boolean
|
||||
) => {
|
||||
if (isWhitelist) {
|
||||
return torrents.filter((torrent) => torrent.label && labels.includes(torrent.label));
|
||||
}
|
||||
|
||||
return torrents.filter((torrent) => !labels.includes(torrent.label as string));
|
||||
};
|
||||
|
||||
export default definition;
|
||||
|
||||
Reference in New Issue
Block a user