diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index 2efc8767..1aae9209 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -43,6 +43,8 @@ import { safeRegexTest, compileRegex, formRegexFromKeywords, + Cache, + getTextHash, } from '@aiostreams/utils'; import { errorStream } from './responses'; @@ -158,12 +160,28 @@ export class AIOStreams { ? this.config.regexSortPatterns || Settings.DEFAULT_REGEX_SORT_PATTERNS : undefined; - const sortRegexes = sortRegexPatterns - ? sortRegexPatterns - .split(/\s+/) - .filter(Boolean) - .map((pattern) => compileRegex(pattern, 'i')) - : undefined; + const sortRegexes: { name?: string; regex: RegExp }[] | undefined = + sortRegexPatterns + ? sortRegexPatterns + .split(/\s+/) + .filter(Boolean) + .map((pattern) => { + const delimiter = '<::>'; + const delimiterIndex = pattern.indexOf(delimiter); + if (delimiterIndex !== -1) { + const name = pattern + .slice(0, delimiterIndex) + .replace(/_/g, ' '); + const regexPattern = pattern.slice( + delimiterIndex + delimiter.length + ); + + const regex = compileRegex(regexPattern, 'i'); + return { name, regex }; + } + return { regex: compileRegex(pattern, 'i') }; + }) + : undefined; excludeRegex || excludeKeywordsRegex || @@ -176,7 +194,7 @@ export class AIOStreams { `Exclude Keywords: ${excludeKeywordsRegex}\n` + `Required Regex: ${requiredRegex}\n` + `Required Keywords: ${requiredKeywordsRegex}\n` + - `Sort Regexes: ${sortRegexes?.join(' --> ')}\n` + `Sort Regexes: ${sortRegexes?.map((regex) => `${regex.name || 'Unnamed'}: ${regex.regex}`).join(' --> ')}\n` ) : []; @@ -492,6 +510,30 @@ export class AIOStreams { filteredResults.length - cleanedStreams.length; filteredResults = cleanedStreams; } + // pre compute highest indexes for regexSortPatterns + const startPrecomputeTime = new Date().getTime(); + const sortRegexCache = Cache.getInstance('sortRegexCache'); + filteredResults.forEach((stream: ParsedStream) => { + if (sortRegexes) { + for (let i = 0; i < sortRegexes.length; i++) { + const regex = sortRegexes[i]; + if (stream.filename) { + const match = safeRegexTest(regex.regex, stream.filename); + if (match) { + stream.regexMatch = regex.name; + sortRegexCache.set(getTextHash(stream.filename), i, 60); + + break; + } + } + } + } + }); + logger.info( + `Precomputed sortRegex indexes for ${filteredResults.length} streams in ${getTimeTakenSincePoint( + startPrecomputeTime + )}` + ); // Apply sorting const sortStartTime = new Date().getTime(); // initially sort by filename to ensure consistent results @@ -813,18 +855,22 @@ export class AIOStreams { if (!a.filename) return direction === 'asc' ? -1 : 1; if (!b.filename) return direction === 'asc' ? 1 : -1; - // Test patterns in order - for (const regex of compiledRegexPatterns) { - const aMatch = safeRegexTest(regex, a.filename); - const bMatch = safeRegexTest(regex, b.filename); - // If both match or both don't match, continue to next pattern - if ((aMatch && bMatch) || (!aMatch && !bMatch)) continue; + const cache = Cache.getInstance('sortRegexCache'); - // If one matches and the other doesn't, use direction to determine order - return direction === 'asc' ? (aMatch ? 1 : -1) : aMatch ? -1 : 1; + const aHighestIndex = cache.get(getTextHash(a.filename)); + const bHighestIndex = cache.get(getTextHash(b.filename)); + + // If both have a regex match, sort by the highest index + if (aHighestIndex !== undefined && bHighestIndex !== undefined) { + return direction === 'asc' + ? bHighestIndex - aHighestIndex + : aHighestIndex - bHighestIndex; } + // If one has a regex match and the other doesn't, sort by the one that does + if (aHighestIndex !== undefined) return direction === 'asc' ? 1 : -1; + if (bHighestIndex !== undefined) return direction === 'asc' ? -1 : 1; - // If we get here, no patterns matched or all patterns matched the same way + // If both have no regex match, they are equal return 0; } catch (e) { return 0; diff --git a/packages/formatters/src/custom.ts b/packages/formatters/src/custom.ts index e884f802..c39e7f17 100644 --- a/packages/formatters/src/custom.ts +++ b/packages/formatters/src/custom.ts @@ -83,6 +83,7 @@ export type ParseValue = { visualTags: string[] | null; audioTags: string[] | null; releaseGroup: string | null; + regexMatch: string | null; encode: string | null; indexer: string | null; year: string | null; @@ -137,6 +138,7 @@ const convertStreamToParseValue = (stream: ParsedStream): ParseValue => { audioTags: stream.audioTags, releaseGroup: stream.releaseGroup === 'Unknown' ? null : stream.releaseGroup, + regexMatch: stream.regexMatch || null, encode: stream.encode === 'Unknown' ? null : stream.encode, indexer: stream.indexers || null, seeders: stream.torrent?.seeders || null, diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index ca8979b1..b704e1bf 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -46,6 +46,7 @@ export interface ParsedStream extends ParsedNameData { indexers?: string; releaseGroup: string; personal?: boolean; + regexMatch?: string; stream?: { subtitles?: Subtitle[]; behaviorHints?: { diff --git a/packages/utils/src/settings.ts b/packages/utils/src/settings.ts index f2fee33e..77fbf35b 100644 --- a/packages/utils/src/settings.ts +++ b/packages/utils/src/settings.ts @@ -63,9 +63,14 @@ const multipleRegex = makeValidator((x) => { ); } // try compiling each pattern to check for validity - + // each "pattern" is regexName::regexPattern, where regexName is optional + // we need to only extract the regexPattern part, patterns.forEach((p) => { try { + const delimiterIndex = p.indexOf('<::>'); + if (delimiterIndex !== -1) { + p = p.slice(delimiterIndex + 2); + } new RegExp(p); } catch (e) { throw new EnvError(`Invalid regex pattern: ${p}`);