From 915187a6120dff969dcfe9d4bf9e473673f8ebf0 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Fri, 13 Jun 2025 13:35:52 +0100 Subject: [PATCH] feat: use sliders for seeder ranges and fix some options not being multi-option --- packages/core/src/db/schemas.ts | 22 +- packages/core/src/main.ts | 182 ++++++---- packages/core/src/utils/constants.ts | 3 + .../frontend/src/components/menu/filters.tsx | 338 +++++++++++++----- 4 files changed, 372 insertions(+), 173 deletions(-) diff --git a/packages/core/src/db/schemas.ts b/packages/core/src/db/schemas.ts index 941a287d..527b0707 100644 --- a/packages/core/src/db/schemas.ts +++ b/packages/core/src/db/schemas.ts @@ -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(), diff --git a/packages/core/src/main.ts b/packages/core/src/main.ts index 0135f3aa..bd8176ab 100644 --- a/packages/core/src/main.ts +++ b/packages/core/src/main.ts @@ -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 => { 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) { diff --git a/packages/core/src/utils/constants.ts b/packages/core/src/utils/constants.ts index 1840ea6c..81a17b4a 100644 --- a/packages/core/src/utils/constants.ts +++ b/packages/core/src/utils/constants.ts @@ -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=', ]; diff --git a/packages/frontend/src/components/menu/filters.tsx b/packages/frontend/src/components/menu/filters.tsx index 4ccb7542..7b2ed6f5 100644 --- a/packages/frontend/src/components/menu/filters.tsx +++ b/packages/frontend/src/components/menu/filters.tsx @@ -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" >
-
- { - setUserData((prev) => ({ - ...prev, - requiredSeeders: { - ...prev.requiredSeeders, - min: value, - }, - })); - }} - /> - { - setUserData((prev) => ({ - ...prev, - requiredSeeders: { - ...prev.requiredSeeders, - max: value, - }, - })); - }} - /> +
+
+
+ + 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" + /> +
+ + {userData.requiredSeederRange?.[0] || MIN_SEEDERS} + + + {userData.requiredSeederRange?.[1] || MAX_SEEDERS} + +
+
+
+ + newValue !== undefined && + setUserData((prev) => ({ + ...prev, + requiredSeederRange: [ + newValue, + prev.requiredSeederRange?.[1] || MAX_SEEDERS, + ], + })) + } + /> + + newValue !== undefined && + setUserData((prev) => ({ + ...prev, + requiredSeederRange: [ + prev.requiredSeederRange?.[0] || MIN_SEEDERS, + 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" + /> +
+ + {userData.excludeSeederRange?.[0] || MIN_SEEDERS} + + + {userData.excludeSeederRange?.[1] || MAX_SEEDERS} + +
+
+
+ + newValue !== undefined && + setUserData((prev) => ({ + ...prev, + excludeSeederRange: [ + newValue, + prev.excludeSeederRange?.[1] || MAX_SEEDERS, + ], + })) + } + /> + + newValue !== undefined && + setUserData((prev) => ({ + ...prev, + excludeSeederRange: [ + prev.excludeSeederRange?.[0] || MIN_SEEDERS, + 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" + /> +
+ + {userData.includeSeederRange?.[0] || MIN_SEEDERS} + + + {userData.includeSeederRange?.[1] || MAX_SEEDERS} + +
+
+
+ + newValue !== undefined && + setUserData((prev) => ({ + ...prev, + includeSeederRange: [ + newValue, + prev.includeSeederRange?.[1] || MAX_SEEDERS, + ], + })) + } + /> + + newValue !== undefined && + setUserData((prev) => ({ + ...prev, + includeSeederRange: [ + prev.includeSeederRange?.[0] || MIN_SEEDERS, + newValue, + ], + })) + } + /> +
+
-
- + ({ + label: type, + value: type, + }))} + value={userData.seederRangeTypes || []} onValueChange={(value) => { setUserData((prev) => ({ ...prev, - excludedSeeders: { - ...prev.excludedSeeders, - min: value, - }, - })); - }} - /> - { - setUserData((prev) => ({ - ...prev, - excludedSeeders: { - ...prev.excludedSeeders, - max: value, - }, - })); - }} - /> -
-
- { - setUserData((prev) => ({ - ...prev, - includedSeeders: { - ...prev.includedSeeders, - min: 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 />
@@ -1036,6 +1188,7 @@ function Content() {