feat: improve regex sorting performance, allow naming regexes with name<::>prefix, and give name in custom formatter with stream.regexMatch

This commit is contained in:
Viren070
2025-05-07 13:26:05 +01:00
parent 2c8c7b5dd6
commit cb71ae91c9
4 changed files with 71 additions and 17 deletions
+62 -16
View File
@@ -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<string, number>('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<string, number>('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;
+2
View File
@@ -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,
+1
View File
@@ -46,6 +46,7 @@ export interface ParsedStream extends ParsedNameData {
indexers?: string;
releaseGroup: string;
personal?: boolean;
regexMatch?: string;
stream?: {
subtitles?: Subtitle[];
behaviorHints?: {
+6 -1
View File
@@ -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}`);