feat: use sliders for seeder ranges and fix some options not being multi-option

This commit is contained in:
Viren070
2025-06-13 13:35:52 +01:00
parent 39b1e84d87
commit 915187a612
4 changed files with 372 additions and 173 deletions
+7 -15
View File
@@ -278,24 +278,16 @@ export const UserDataSchema = z.object({
enhanceResults: z.boolean().optional(),
enhancePosters: z.boolean().optional(),
excludedSeeders: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
excludeSeederRange: z
.tuple([z.number().min(0), z.number().min(0)])
.optional(),
includedSeeders: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
includeSeederRange: z
.tuple([z.number().min(0), z.number().min(0)])
.optional(),
requiredSeeders: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
requiredSeederRange: z
.tuple([z.number().min(0), z.number().min(0)])
.optional(),
seederRangeTypes: z.array(z.enum(['p2p', 'cached', 'uncached'])).optional(),
excludeCached: z.boolean().optional(),
excludeCachedFromAddons: z.array(z.string().min(1)).optional(),
excludeCachedFromServices: z.array(z.string().min(1)).optional(),
+115 -67
View File
@@ -1350,6 +1350,50 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => `
}
};
const normaliseRange = (
range: [number, number] | undefined,
defaults: { min: number; max: number }
): [number | undefined, number | undefined] | undefined => {
if (!range) return undefined;
const [min, max] = range;
const normMin = min === defaults.min ? undefined : min;
const normMax = max === defaults.max ? undefined : max;
return normMin === undefined && normMax === undefined
? undefined
: [normMin, normMax];
};
const normaliseSeederRange = (
seederRange: [number, number] | undefined
) => {
return normaliseRange(seederRange, {
min: constants.MIN_SEEDERS,
max: constants.MAX_SEEDERS,
});
};
const normaliseSizeRange = (sizeRange: [number, number] | undefined) => {
return normaliseRange(sizeRange, {
min: constants.MIN_SIZE,
max: constants.MAX_SIZE,
});
};
const getStreamType = (
stream: ParsedStream
): 'p2p' | 'cached' | 'uncached' | undefined => {
switch (stream.type) {
case 'debrid':
return stream.service?.cached ? 'cached' : 'uncached';
case 'usenet':
return stream.service?.cached ? 'cached' : 'uncached';
case 'p2p':
return 'p2p';
default:
return undefined;
}
};
const shouldKeepStream = async (stream: ParsedStream): Promise<boolean> => {
const file = stream.parsedFile;
@@ -1433,23 +1477,42 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => `
return true;
}
const includedSeederRange = normaliseSeederRange(
this.userData.includeSeederRange
);
const excludedSeederRange = normaliseSeederRange(
this.userData.excludeSeederRange
);
const requiredSeederRange = normaliseSeederRange(
this.userData.requiredSeederRange
);
const typeForSeederRange = getStreamType(stream);
if (
this.userData.includedSeeders?.min &&
stream.torrent?.seeders &&
stream.torrent.seeders > this.userData.includedSeeders.min
includedSeederRange &&
(!this.userData.seederRangeTypes ||
(typeForSeederRange &&
this.userData.seederRangeTypes.includes(typeForSeederRange)))
) {
return true;
}
if (
this.userData.includedSeeders?.max &&
stream.torrent?.seeders &&
stream.torrent.seeders < this.userData.includedSeeders.max
) {
return true;
if (
stream.torrent?.seeders &&
includedSeederRange[0] &&
stream.torrent.seeders > includedSeederRange[0]
) {
return true;
}
if (
stream.torrent?.seeders &&
includedSeederRange[1] &&
stream.torrent.seeders < includedSeederRange[1]
) {
return true;
}
}
// Track stream type exclusions
if (this.userData.excludedStreamTypes?.includes(stream.type)) {
// Track stream type exclusions
skipReasons.excludedStreamType.total++;
skipReasons.excludedStreamType.details[stream.type] =
(skipReasons.excludedStreamType.details[stream.type] || 0) + 1;
@@ -1780,37 +1843,47 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => `
}
if (
this.userData.requiredSeeders?.min &&
stream.torrent?.seeders &&
stream.torrent.seeders < this.userData.requiredSeeders.min
requiredSeederRange &&
(!this.userData.seederRangeTypes ||
(typeForSeederRange &&
this.userData.seederRangeTypes.includes(typeForSeederRange)))
) {
skipReasons.requiredSeeders.total++;
return false;
}
if (
this.userData.requiredSeeders?.max &&
stream.torrent?.seeders &&
stream.torrent.seeders > this.userData.requiredSeeders.max
) {
skipReasons.requiredSeeders.total++;
return false;
if (
stream.torrent?.seeders &&
requiredSeederRange[0] &&
stream.torrent.seeders < requiredSeederRange[0]
) {
return false;
}
if (
stream.torrent?.seeders &&
requiredSeederRange[1] &&
stream.torrent.seeders > requiredSeederRange[1]
) {
return false;
}
}
if (
this.userData.excludedSeeders?.min &&
stream.torrent?.seeders &&
stream.torrent.seeders < this.userData.excludedSeeders.min
excludedSeederRange &&
(!this.userData.seederRangeTypes ||
(typeForSeederRange &&
this.userData.seederRangeTypes.includes(typeForSeederRange)))
) {
skipReasons.excludedSeeders.total++;
return false;
}
if (
this.userData.excludedSeeders?.max &&
stream.torrent?.seeders &&
stream.torrent.seeders > this.userData.excludedSeeders.max
) {
skipReasons.excludedSeeders.total++;
return false;
if (
stream.torrent?.seeders &&
excludedSeederRange[0] &&
stream.torrent.seeders > excludedSeederRange[0]
) {
return false;
}
if (
stream.torrent?.seeders &&
excludedSeederRange[1] &&
stream.torrent.seeders < excludedSeederRange[1]
) {
return false;
}
}
if (!performTitleMatch(stream)) {
@@ -1836,19 +1909,6 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => `
return false;
}
const useMinMax = (
minMax: [number, number] | undefined,
defaults: { min: number; max: number }
): [number | undefined, number | undefined] | undefined => {
if (!minMax) return undefined;
const [min, max] = minMax;
const normMin = min === defaults.min ? undefined : min;
const normMax = max === defaults.max ? undefined : max;
return normMin === undefined && normMax === undefined
? undefined
: [normMin, normMax];
};
const global = this.userData.size?.global;
const resolution = stream.parsedFile?.resolution
? this.userData.size?.resolution?.[
@@ -1860,24 +1920,12 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => `
let minMax: [number | undefined, number | undefined] | undefined;
if (type === 'movie') {
minMax =
useMinMax(resolution?.movies, {
min: constants.MIN_SIZE,
max: constants.MAX_SIZE,
}) ||
useMinMax(global?.movies, {
min: constants.MIN_SIZE,
max: constants.MAX_SIZE,
});
normaliseSizeRange(resolution?.movies) ||
normaliseSizeRange(global?.movies);
} else {
minMax =
useMinMax(resolution?.series, {
min: constants.MIN_SIZE,
max: constants.MAX_SIZE,
}) ||
useMinMax(global?.series, {
min: constants.MIN_SIZE,
max: constants.MAX_SIZE,
});
normaliseSizeRange(resolution?.series) ||
normaliseSizeRange(global?.series);
}
if (minMax) {
+3
View File
@@ -585,6 +585,9 @@ const SORT_CRITERIA = [
export const MIN_SIZE = 0;
export const MAX_SIZE = 100 * 1000 * 1000 * 1000; // 100GB
export const MIN_SEEDERS = 0;
export const MAX_SEEDERS = 1000;
export const DEFAULT_POSTERS = [
'aHR0cHM6Ly93d3cucG5nbWFydC5jb20vZmlsZXMvMTEvUmlja3JvbGxpbmctUE5HLVBpYy5wbmc=',
];
+247 -91
View File
@@ -59,6 +59,10 @@ import {
TYPES,
DEDUPLICATOR_KEYS,
AUDIO_CHANNELS,
MIN_SIZE,
MAX_SIZE,
MIN_SEEDERS,
MAX_SEEDERS,
} from '../../../../core/src/utils/constants';
import { PageControls } from '../shared/page-controls';
import { Switch } from '../ui/switch';
@@ -82,9 +86,6 @@ type AudioTag = (typeof AUDIO_TAGS)[number];
type AudioChannel = (typeof AUDIO_CHANNELS)[number];
type Language = (typeof LANGUAGES)[number];
const MIN_SIZE = 0;
const MAX_SIZE = 100 * 1000 * 1000 * 1000; // 100GB
const defaultPreferredResolutions: Resolution[] = [
'2160p',
'1440p',
@@ -853,100 +854,251 @@ function Content() {
description="Configure required, excluded, and included seeder ranges"
>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NumberInput
label="Required Minimum Seeders"
help="Streams with fewer seeders than this will be excluded"
value={userData.requiredSeeders?.min}
min={0}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
requiredSeeders: {
...prev.requiredSeeders,
min: value,
},
}));
}}
/>
<NumberInput
label="Required Maximum Seeders"
help="Streams with more seeders than this will be excluded"
value={userData.requiredSeeders?.max}
min={0}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
requiredSeeders: {
...prev.requiredSeeders,
max: value,
},
}));
}}
/>
<div className="space-y-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 min-w-0">
<Slider
min={MIN_SEEDERS}
max={MAX_SEEDERS}
defaultValue={[MIN_SEEDERS, MAX_SEEDERS]}
value={
userData.requiredSeederRange || [
MIN_SEEDERS,
MAX_SEEDERS,
]
}
onValueChange={(newValue) =>
newValue !== undefined &&
newValue?.[0] !== undefined &&
newValue?.[1] !== undefined &&
setUserData((prev) => ({
...prev,
requiredSeederRange: [newValue[0], newValue[1]],
}))
}
minStepsBetweenThumbs={1}
label="Required Seeder Range"
help="Streams with seeders outside this range will be excluded"
/>
<div className="flex justify-between mt-1 text-xs text-[--muted]">
<span>
{userData.requiredSeederRange?.[0] || MIN_SEEDERS}
</span>
<span>
{userData.requiredSeederRange?.[1] || MAX_SEEDERS}
</span>
</div>
</div>
<div className="flex gap-2 md:w-[240px] shrink-0">
<NumberInput
label="Min"
value={
userData.requiredSeederRange?.[0] || MIN_SEEDERS
}
min={MIN_SEEDERS}
max={userData.requiredSeederRange?.[1] || MAX_SEEDERS}
onValueChange={(newValue) =>
newValue !== undefined &&
setUserData((prev) => ({
...prev,
requiredSeederRange: [
newValue,
prev.requiredSeederRange?.[1] || MAX_SEEDERS,
],
}))
}
/>
<NumberInput
label="Max"
value={
userData.requiredSeederRange?.[1] || MAX_SEEDERS
}
min={userData.requiredSeederRange?.[0] || MIN_SEEDERS}
max={MAX_SEEDERS}
onValueChange={(newValue) =>
newValue !== undefined &&
setUserData((prev) => ({
...prev,
requiredSeederRange: [
prev.requiredSeederRange?.[0] || MIN_SEEDERS,
newValue,
],
}))
}
/>
</div>
</div>
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 min-w-0">
<Slider
min={MIN_SEEDERS}
max={MAX_SEEDERS}
defaultValue={[MIN_SEEDERS, MAX_SEEDERS]}
value={
userData.excludeSeederRange || [
MIN_SEEDERS,
MAX_SEEDERS,
]
}
onValueChange={(newValue) =>
newValue !== undefined &&
newValue?.[0] !== undefined &&
newValue?.[1] !== undefined &&
setUserData((prev) => ({
...prev,
excludeSeederRange: [newValue[0], newValue[1]],
}))
}
minStepsBetweenThumbs={1}
label="Excluded Seeder Range"
help="Streams with seeders in this range will be excluded"
/>
<div className="flex justify-between mt-1 text-xs text-[--muted]">
<span>
{userData.excludeSeederRange?.[0] || MIN_SEEDERS}
</span>
<span>
{userData.excludeSeederRange?.[1] || MAX_SEEDERS}
</span>
</div>
</div>
<div className="flex gap-2 md:w-[240px] shrink-0">
<NumberInput
label="Min"
value={
userData.excludeSeederRange?.[0] || MIN_SEEDERS
}
min={MIN_SEEDERS}
max={userData.excludeSeederRange?.[1] || MAX_SEEDERS}
onValueChange={(newValue) =>
newValue !== undefined &&
setUserData((prev) => ({
...prev,
excludeSeederRange: [
newValue,
prev.excludeSeederRange?.[1] || MAX_SEEDERS,
],
}))
}
/>
<NumberInput
label="Max"
value={
userData.excludeSeederRange?.[1] || MAX_SEEDERS
}
min={userData.excludeSeederRange?.[0] || MIN_SEEDERS}
max={MAX_SEEDERS}
onValueChange={(newValue) =>
newValue !== undefined &&
setUserData((prev) => ({
...prev,
excludeSeederRange: [
prev.excludeSeederRange?.[0] || MIN_SEEDERS,
newValue,
],
}))
}
/>
</div>
</div>
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 min-w-0">
<Slider
min={MIN_SEEDERS}
max={MAX_SEEDERS}
defaultValue={[MIN_SEEDERS, MAX_SEEDERS]}
value={
userData.includeSeederRange || [
MIN_SEEDERS,
MAX_SEEDERS,
]
}
onValueChange={(newValue) =>
newValue !== undefined &&
newValue?.[0] !== undefined &&
newValue?.[1] !== undefined &&
setUserData((prev) => ({
...prev,
includeSeederRange: [newValue[0], newValue[1]],
}))
}
minStepsBetweenThumbs={1}
label="Included Seeder Range"
help="Streams with seeders in this range will be included, ignoring other filters"
/>
<div className="flex justify-between mt-1 text-xs text-[--muted]">
<span>
{userData.includeSeederRange?.[0] || MIN_SEEDERS}
</span>
<span>
{userData.includeSeederRange?.[1] || MAX_SEEDERS}
</span>
</div>
</div>
<div className="flex gap-2 md:w-[240px] shrink-0">
<NumberInput
label="Min"
value={
userData.includeSeederRange?.[0] || MIN_SEEDERS
}
min={MIN_SEEDERS}
max={userData.includeSeederRange?.[1] || MAX_SEEDERS}
onValueChange={(newValue) =>
newValue !== undefined &&
setUserData((prev) => ({
...prev,
includeSeederRange: [
newValue,
prev.includeSeederRange?.[1] || MAX_SEEDERS,
],
}))
}
/>
<NumberInput
label="Max"
value={
userData.includeSeederRange?.[1] || MAX_SEEDERS
}
min={userData.includeSeederRange?.[0] || MIN_SEEDERS}
max={MAX_SEEDERS}
onValueChange={(newValue) =>
newValue !== undefined &&
setUserData((prev) => ({
...prev,
includeSeederRange: [
prev.includeSeederRange?.[0] || MIN_SEEDERS,
newValue,
],
}))
}
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NumberInput
label="Excluded Minimum Seeders"
help="Streams with more seeders than this will be excluded"
value={userData.excludedSeeders?.min}
min={0}
<div className="space-y-4">
<Combobox
label="Stream Types"
emptyMessage="There aren't any stream types to choose from..."
options={['p2p', 'cached', 'uncached'].map((type) => ({
label: type,
value: type,
}))}
value={userData.seederRangeTypes || []}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
excludedSeeders: {
...prev.excludedSeeders,
min: value,
},
}));
}}
/>
<NumberInput
label="Excluded Maximum Seeders"
help="Streams with fewer seeders than this will be excluded"
value={userData.excludedSeeders?.max}
min={0}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
excludedSeeders: {
...prev.excludedSeeders,
max: value,
},
}));
}}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NumberInput
label="Included Minimum Seeders"
help="Streams with more seeders than this will be included, ignoring other filters"
value={userData.includedSeeders?.min}
min={0}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
includedSeeders: {
...prev.includedSeeders,
min: value,
},
}));
}}
/>
<NumberInput
label="Included Maximum Seeders"
help="Streams with fewer seeders than this will be included, ignoring other filters"
value={userData.includedSeeders?.max}
min={0}
onValueChange={(value) => {
setUserData((prev) => ({
...prev,
includedSeeders: {
...prev.includedSeeders,
max: value,
},
seederRangeTypes: value as (
| 'p2p'
| 'cached'
| 'uncached'
)[],
}));
}}
help="Stream types that will use the seeder ranges defined above. Leave blank to apply to all stream types."
multiple
/>
</div>
</div>
@@ -1036,6 +1188,7 @@ function Content() {
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Combobox
multiple
disabled={!userData.titleMatching?.enabled}
label="Request Types"
emptyMessage="There aren't any request types to choose from..."
@@ -1057,6 +1210,7 @@ function Content() {
}}
/>
<Combobox
multiple
disabled={!userData.titleMatching?.enabled}
label="Addons"
help="Addons that will use strict title matching. Leave blank to apply to all addons."
@@ -1103,6 +1257,7 @@ function Content() {
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Combobox
multiple
disabled={!userData.seasonEpisodeMatching?.enabled}
label="Request Types"
help="Request types that will use season/episode matching. Leave blank to apply to all request types."
@@ -1124,6 +1279,7 @@ function Content() {
}}
/>
<Combobox
multiple
disabled={!userData.seasonEpisodeMatching?.enabled}
label="Addons"
help="Addons that will use season/episode matching. Leave blank to apply to all addons."