Skip to content

Commit

Permalink
Add option to configure ignored tracker subdomains
Browse files Browse the repository at this point in the history
Issue #213
  • Loading branch information
qu1ck committed Sep 30, 2024
1 parent e11930d commit f6a62d9
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/components/modals/daemon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ export function DaemonSettingsModal(props: ModalState) {
>
<Box pos="relative">
<LoadingOverlay visible={fetchStatus === "fetching"} overlayBlur={2} />
<Tabs defaultValue="polling" mih="28rem">
<Tabs defaultValue="polling" mih="33rem">
<Tabs.List>
<Tabs.Tab value="polling" p="lg">Polling</Tabs.Tab>
<Tabs.Tab value="download" p="lg">Download</Tabs.Tab>
Expand Down
65 changes: 62 additions & 3 deletions src/components/modals/interfacepanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@

import React, { useCallback, useEffect, useState } from "react";
import type { ColorScheme } from "@mantine/core";
import { Checkbox, Grid, MultiSelect, NativeSelect, NumberInput, Textarea, useMantineTheme } from "@mantine/core";
import { Box, Checkbox, Grid, HoverCard, MultiSelect, NativeSelect, NumberInput, Text, Textarea, useMantineTheme } from "@mantine/core";
import type { UseFormReturnType } from "@mantine/form";
import ColorChooser from "components/colorchooser";
import { useGlobalStyleOverrides } from "themehooks";
import { DeleteTorrentDataOptions } from "config";
import type { ColorSetting, DeleteTorrentDataOption, StyleOverrides } from "config";
import { ColorSchemeToggle } from "components/miscbuttons";
import { Label } from "./common";
import * as Icon from "react-bootstrap-icons";
const { TAURI, invoke } = await import(/* webpackChunkName: "taurishim" */"taurishim");

export interface InterfaceFormValues {
Expand All @@ -39,6 +40,7 @@ export interface InterfaceFormValues {
numLastSaveDirs: number,
sortLastSaveDirs: boolean,
preconfiguredLabels: string[],
ignoredTrackerPrefixes: string[],
defaultTrackers: string[],
},
}
Expand All @@ -59,7 +61,7 @@ export function InterfaceSettigsPanel<V extends InterfaceFormValues>(props: { fo
}
}, []);

const { setFieldValue } = props.form as unknown as UseFormReturnType<InterfaceFormValues>;
const { setFieldValue, setFieldError, clearFieldError } = props.form as unknown as UseFormReturnType<InterfaceFormValues>;

useEffect(() => {
setFieldValue("interface.theme", theme.colorScheme);
Expand Down Expand Up @@ -101,6 +103,17 @@ export function InterfaceSettigsPanel<V extends InterfaceFormValues>(props: { fo
setFieldValue("interface.preconfiguredLabels", labels);
}, [setFieldValue]);

const setIgnoredTrackerPrefixes = useCallback((prefixes: string[]) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ = new RegExp(`^(?<prefix>(${prefixes.join("|")})\\d*)\\.[^.]+\\.[^.]+$`, "i");
setFieldValue("interface.ignoredTrackerPrefixes", prefixes);
clearFieldError("interface.ignoredTrackerPrefixes");
} catch (SyntaxError) {
setFieldError("interface.ignoredTrackerPrefixes", "Invalid regex");
}
}, [setFieldValue, setFieldError, clearFieldError]);

return (
<Grid align="center">
<Grid.Col span={1}>
Expand Down Expand Up @@ -161,7 +174,20 @@ export function InterfaceSettigsPanel<V extends InterfaceFormValues>(props: { fo
data={props.form.values.interface.preconfiguredLabels}
value={props.form.values.interface.preconfiguredLabels}
onChange={setPreconfiguredLabels}
label="Preconfigured labels"
label={<Box>
<span>Preconfigured labels</span>
<HoverCard width={280} shadow="md">
<HoverCard.Target>
<Icon.Question />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text size="sm">
These labels will always be present in the suggestions list
and filters even if no existing torrents have them.
</Text>
</HoverCard.Dropdown>
</HoverCard>
</Box>}
withinPortal
searchable
creatable
Expand All @@ -173,6 +199,39 @@ export function InterfaceSettigsPanel<V extends InterfaceFormValues>(props: { fo
valueComponent={Label}
/>
</Grid.Col>
<Grid.Col>
<MultiSelect
data={props.form.values.interface.ignoredTrackerPrefixes}
value={props.form.values.interface.ignoredTrackerPrefixes}
onChange={setIgnoredTrackerPrefixes}
label={<Box>
<span>Ignored tracker prefixes</span>
<HoverCard width={380} shadow="md">
<HoverCard.Target>
<Icon.Question />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text size="sm">
When subdomain of the tracker looks like one of these strings + (optional) digits,
it will be omitted. This affects grouping in filters and display in table columns.
You can use regex here for more advanced filtering, the list will be combined
using &quot;|&quot;.
</Text>
</HoverCard.Dropdown>
</HoverCard>
</Box>}
withinPortal
searchable
creatable
error={props.form.errors["interface.ignoredTrackerPrefixes"]}
getCreateLabel={(query) => `+ Add ${query}`}
onCreate={(query) => {
setIgnoredTrackerPrefixes([...props.form.values.interface.ignoredTrackerPrefixes, query]);
return query;
}}
valueComponent={Label}
/>
</Grid.Col>
<Grid.Col>
<Textarea minRows={6}
label="Default tracker list"
Expand Down
12 changes: 6 additions & 6 deletions src/components/modals/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ function ServerPanel(props: ServerPanelProps) {
setMappingsString(e.target.value);
}}
value={mappingsString}
minRows={4}
minRows={7}
/>
</Grid.Col>
</Grid>
Expand Down Expand Up @@ -352,27 +352,27 @@ export function AppSettingsModal(props: AppSettingsModalProps) {
title="Application Settings"
>
<form>
<Tabs mih="25rem" defaultValue="servers">
<Tabs mih="33rem" defaultValue="servers">
<Tabs.List>
<Tabs.Tab value="servers" p="lg">Servers</Tabs.Tab>
<Tabs.Tab value="integrations" p="lg">Integrations</Tabs.Tab>
{TAURI && <Tabs.Tab value="interface" p="lg">Interface</Tabs.Tab>}
</Tabs.List>

<Tabs.Panel value="servers" pt="md" mih="24rem">
<Flex h="100%" gap="0.5rem" mih="24rem">
<Tabs.Panel value="servers" pt="md">
<Flex h="100%" gap="0.5rem" mih="28rem">
<ServerListPanel form={form} current={currentServerIndex} setCurrent={setCurrentServerIndex} />
{currentServerIndex === -1
? <></>
: <ServerPanel form={form} current={currentServerIndex} />}
</Flex>
</Tabs.Panel>

<Tabs.Panel value="integrations" pt="md" mih="24rem">
<Tabs.Panel value="integrations" pt="md">
<IntegrationsPanel form={form} />
</Tabs.Panel>

{TAURI && <Tabs.Panel value="interface" pt="md" mih="24rem">
{TAURI && <Tabs.Panel value="interface" pt="md">
<InterfaceSettigsPanel form={form} />
</Tabs.Panel>}
</Tabs>
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ interface Settings {
numLastSaveDirs: number,
sortLastSaveDirs: boolean,
preconfiguredLabels: string[],
ignoredTrackerPrefixes: string[],
defaultTrackers: string[],
styleOverrides: StyleOverrides,
progressbarStyle?: string, // deprecated
Expand Down Expand Up @@ -280,6 +281,7 @@ const DefaultSettings: Settings = {
numLastSaveDirs: 20,
sortLastSaveDirs: false,
preconfiguredLabels: [],
ignoredTrackerPrefixes: ["t", "tr", "tk", "tracker", "bt", "open", "opentracker"],
defaultTrackers: [...DefaultTrackerList],
styleOverrides: {
dark: {},
Expand Down
18 changes: 14 additions & 4 deletions src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ const BandwidthGroupKeys = {
all: (server: string) => [server, "bandwidth-group"] as const,
};

function useIgnoredTrackerPrefixesRe() {
const config = useContext(ConfigContext);
return useMemo(() => new RegExp(`^(?<prefix>(${config.values.interface.ignoredTrackerPrefixes.join("|")})\\d*)\\.[^.]+\\.[^.]+$`, "i"),
[config.values.interface.ignoredTrackerPrefixes]);
}

export function useTorrentList(enabled: boolean, fields: TorrentFieldsType[]) {
const serverConfig = useContext(ServerConfigContext);
const client = useTransmissionClient();
Expand Down Expand Up @@ -78,6 +84,8 @@ export function useTorrentList(enabled: boolean, fields: TorrentFieldsType[]) {
? serverConfig.intervals.torrentsMinimized
: serverConfig.intervals.torrents);

const prefixesRe = useIgnoredTrackerPrefixesRe();

return useQuery({
queryKey: TorrentKeys.listAll(serverConfig.name, fields),
refetchInterval,
Expand All @@ -87,23 +95,25 @@ export function useTorrentList(enabled: boolean, fields: TorrentFieldsType[]) {
queryFn: useCallback(async () => {
const torrents = await client.getTorrents(fields);
return await Promise.all(torrents.map(
async (t: TorrentBase) => await processTorrent(t, false, client)));
}, [client, fields]),
async (t: TorrentBase) => await processTorrent(t, false, prefixesRe, client)));
}, [client, fields, prefixesRe]),
});
}

export function useTorrentDetails(torrentId: number, enabled: boolean, lookupIps: boolean, disableRefetch?: boolean) {
const serverConfig = useContext(ServerConfigContext);
const client = useTransmissionClient();

const prefixesRe = useIgnoredTrackerPrefixesRe();

return useQuery({
queryKey: TorrentKeys.details(serverConfig.name, torrentId),
refetchInterval: disableRefetch === true ? false : 1000 * serverConfig.intervals.details,
staleTime: 1000 * 5,
enabled,
queryFn: useCallback(async () => {
return await processTorrent(await client.getTorrentDetails(torrentId), TAURI && lookupIps, client);
}, [client, torrentId, lookupIps]),
return await processTorrent(await client.getTorrentDetails(torrentId), TAURI && lookupIps, prefixesRe, client);
}, [client, torrentId, lookupIps, prefixesRe]),
});
}

Expand Down
11 changes: 5 additions & 6 deletions src/rpc/torrent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,15 @@ function getTrackerStatus(torrent: TorrentBase): string {
}

const portRe = /:\d+$/;
const prefixRe = /^((t|tr|tk|tracker|bt|open|opentracker)\d*)\.[^.]+\.[^.]+$/;
const httpRe = /^https?:\/\//;

function getTorrentMainTracker(t: TorrentBase): string {
function getTorrentMainTracker(t: TorrentBase, ignoredPrefixesRe: RegExp): string {
if (t.trackerStats.length === 0) return "<No trackers>";
let host = t.trackerStats[0].host as string;
const portMatch = portRe.exec(host);
if (portMatch != null) host = host.substring(0, portMatch.index);
const prefixMatch = prefixRe.exec(host);
if (prefixMatch != null) host = host.substring(prefixMatch[1].length + 1);
const prefixMatch = ignoredPrefixesRe.exec(host);
if (prefixMatch?.groups !== undefined) host = host.substring(prefixMatch.groups.prefix.length + 1);
const httpMatch = httpRe.exec(host);
if (httpMatch != null) host = host.substring(httpMatch[0].length);
return host;
Expand All @@ -109,7 +108,7 @@ function getPeersTotal(t: TorrentBase) {
return peers;
}

export async function processTorrent(t: TorrentBase, lookupIps: boolean, client: TransmissionClient): Promise<Torrent> {
export async function processTorrent(t: TorrentBase, lookupIps: boolean, ignoredPrefixesRe: RegExp, client: TransmissionClient): Promise<Torrent> {
const peers = t.peers === undefined
? undefined
: await Promise.all(t.peers.map(async (p: PeerStatsBase) => await processPeerStats(p, lookupIps, client)));
Expand All @@ -119,7 +118,7 @@ export async function processTorrent(t: TorrentBase, lookupIps: boolean, client:
downloadDir: (t.downloadDir as string).replaceAll("\\", "/"),
cachedError: getTorrentError(t),
cachedTrackerStatus: getTrackerStatus(t),
cachedMainTracker: getTorrentMainTracker(t),
cachedMainTracker: getTorrentMainTracker(t, ignoredPrefixesRe),
cachedSeedsTotal: getSeedsTotal(t),
cachedPeersTotal: getPeersTotal(t),
peers,
Expand Down

0 comments on commit f6a62d9

Please sign in to comment.