feat: add filter statistics

feat: add queryType to SEL filters
This commit is contained in:
Viren070
2025-09-19 02:08:27 +01:00
parent 2d137473e6
commit 26713aa38d
5 changed files with 363 additions and 290 deletions
+49 -4
View File
@@ -103,11 +103,11 @@ export class AIOStreams {
this.options = options;
this.proxifier = new Proxifier(userData);
this.limiter = new StreamLimiter(userData);
this.fetcher = new Fetcher(userData);
this.filterer = new Filterer(userData);
this.precomputer = new Precomputer(userData);
this.fetcher = new Fetcher(userData, this.filterer, this.precomputer);
this.deduplicator = new Deduplicator(userData);
this.sorter = new Sorter(userData);
this.precomputer = new Precomputer(userData);
}
private setUserData(userData: UserData) {
@@ -217,6 +217,49 @@ export class AIOStreams {
}
}
const { filterDetails, includedDetails } =
this.filterer.getFormattedFilterDetails();
// append formatted filter statistics to the statistics array
// Helper to split details array into groups by 📌
function splitByPin(details: string[]): string[][] {
const groups: string[][] = [];
let currentGroup: string[] = [];
for (const line of details) {
if (line.trim().startsWith('📌')) {
if (currentGroup.length > 0) {
groups.push(currentGroup);
}
currentGroup = [line];
} else {
currentGroup.push(line);
}
}
if (currentGroup.length > 0) {
groups.push(currentGroup);
}
return groups;
}
if (filterDetails.length > 0) {
const removalGroups = splitByPin(filterDetails);
for (const group of removalGroups) {
statistics.push({
title: '🔍 Removal Reasons',
description: group.join('\n').trim(),
});
}
}
if (includedDetails.length > 0) {
const includedGroups = splitByPin(includedDetails);
for (const group of includedGroups) {
statistics.push({
title: '🔍 Included Reasons',
description: group.join('\n').trim(),
});
}
}
// return the final list of streams, followed by the error streams.
logger.info(
`Returning ${finalStreams.length} streams and ${errors.length} errors and ${statistics.length} statistic`
@@ -1257,7 +1300,7 @@ export class AIOStreams {
processedStreams = await this.deduplicator.deduplicate(processedStreams);
if (isMeta) {
await this.precomputer.precompute(processedStreams);
await this.precomputer.precompute(processedStreams, type, id);
}
let finalStreams = this.applyModifications(
@@ -1268,7 +1311,9 @@ export class AIOStreams {
processedStreams,
AnimeDatabase.getInstance().isAnime(id) ? 'anime' : type
)
)
),
type,
id
)
)
).map((stream) => {
+5 -2
View File
@@ -646,8 +646,11 @@ export class GroupConditionEvaluator extends StreamExpressionEngine {
}
export class StreamSelector extends StreamExpressionEngine {
constructor() {
private queryType: string;
constructor(queryType: string) {
super();
this.queryType = queryType;
this.parser.consts.queryType = queryType;
}
async select(
@@ -678,7 +681,7 @@ export class StreamSelector extends StreamExpressionEngine {
}
static async testSelect(condition: string): Promise<ParsedStream[]> {
const parser = new StreamSelector();
const parser = new StreamSelector('movie');
const streams = [
parser.createTestStream({ type: 'debrid' }),
parser.createTestStream({ type: 'debrid' }),
+8 -4
View File
@@ -19,10 +19,14 @@ class StreamFetcher {
private filter: StreamFilter;
private precompute: StreamPrecompute;
private deduplicate: StreamDeduplicator;
constructor(userData: UserData) {
constructor(
userData: UserData,
filter: StreamFilter,
precompute: StreamPrecompute
) {
this.userData = userData;
this.filter = new StreamFilter(userData);
this.precompute = new StreamPrecompute(userData);
this.filter = filter;
this.precompute = precompute;
this.deduplicate = new StreamDeduplicator(userData);
}
@@ -153,7 +157,7 @@ class StreamFetcher {
const filteredStreams = await this.deduplicate.deduplicate(
await this.filter.filter(groupStreams, type, id)
);
await this.precompute.precompute(filteredStreams);
await this.precompute.precompute(filteredStreams, type, id);
logger.info(
`Finished fetching from group in ${getTimeTakenSincePoint(groupStart)}`
+294 -278
View File
@@ -21,11 +21,183 @@ import { calculateAbsoluteEpisode } from '../builtins/utils/general.js';
const logger = createLogger('filterer');
interface Reason {
total: number;
details: Record<string, number>;
}
export interface FilterStatistics {
removed: {
titleMatching: Reason;
yearMatching: Reason;
seasonEpisodeMatching: Reason;
excludedStreamType: Reason;
requiredStreamType: Reason;
excludedResolution: Reason;
requiredResolution: Reason;
excludedQuality: Reason;
requiredQuality: Reason;
excludedEncode: Reason;
requiredEncode: Reason;
excludedVisualTag: Reason;
requiredVisualTag: Reason;
excludedAudioTag: Reason;
requiredAudioTag: Reason;
excludedAudioChannel: Reason;
requiredAudioChannel: Reason;
excludedLanguage: Reason;
requiredLanguage: Reason;
excludedCached: Reason;
excludedUncached: Reason;
excludedRegex: Reason;
requiredRegex: Reason;
excludedKeywords: Reason;
requiredKeywords: Reason;
excludedSeederRange: Reason;
requiredSeederRange: Reason;
excludedFilterCondition: Reason;
requiredFilterCondition: Reason;
size: Reason;
};
included: {
passthrough: Reason;
resolution: Reason;
quality: Reason;
encode: Reason;
visualTag: Reason;
audioTag: Reason;
audioChannel: Reason;
language: Reason;
streamType: Reason;
size: Reason;
seeder: Reason;
regex: Reason;
keywords: Reason;
streamExpression: Reason;
};
}
class StreamFilterer {
private userData: UserData;
private filterStatistics: FilterStatistics;
constructor(userData: UserData) {
this.userData = userData;
this.filterStatistics = {
removed: {
titleMatching: { total: 0, details: {} },
yearMatching: { total: 0, details: {} },
seasonEpisodeMatching: { total: 0, details: {} },
excludedStreamType: { total: 0, details: {} },
requiredStreamType: { total: 0, details: {} },
excludedResolution: { total: 0, details: {} },
requiredResolution: { total: 0, details: {} },
excludedQuality: { total: 0, details: {} },
requiredQuality: { total: 0, details: {} },
excludedEncode: { total: 0, details: {} },
requiredEncode: { total: 0, details: {} },
excludedVisualTag: { total: 0, details: {} },
requiredVisualTag: { total: 0, details: {} },
excludedAudioTag: { total: 0, details: {} },
requiredAudioTag: { total: 0, details: {} },
excludedAudioChannel: { total: 0, details: {} },
requiredAudioChannel: { total: 0, details: {} },
excludedLanguage: { total: 0, details: {} },
requiredLanguage: { total: 0, details: {} },
excludedCached: { total: 0, details: {} },
excludedUncached: { total: 0, details: {} },
excludedRegex: { total: 0, details: {} },
requiredRegex: { total: 0, details: {} },
excludedKeywords: { total: 0, details: {} },
requiredKeywords: { total: 0, details: {} },
excludedSeederRange: { total: 0, details: {} },
requiredSeederRange: { total: 0, details: {} },
excludedFilterCondition: { total: 0, details: {} },
requiredFilterCondition: { total: 0, details: {} },
size: { total: 0, details: {} },
},
included: {
passthrough: { total: 0, details: {} },
resolution: { total: 0, details: {} },
quality: { total: 0, details: {} },
encode: { total: 0, details: {} },
visualTag: { total: 0, details: {} },
audioTag: { total: 0, details: {} },
audioChannel: { total: 0, details: {} },
language: { total: 0, details: {} },
streamType: { total: 0, details: {} },
size: { total: 0, details: {} },
seeder: { total: 0, details: {} },
regex: { total: 0, details: {} },
keywords: { total: 0, details: {} },
streamExpression: { total: 0, details: {} },
},
};
}
private incrementRemovalReason(
reason: keyof FilterStatistics['removed'],
detail?: string
) {
this.filterStatistics.removed[reason].total++;
if (detail) {
this.filterStatistics.removed[reason].details[detail] =
(this.filterStatistics.removed[reason].details[detail] || 0) + 1;
}
}
private incrementIncludedReason(
reason: keyof FilterStatistics['included'],
detail?: string
) {
this.filterStatistics.included[reason].total++;
if (detail) {
this.filterStatistics.included[reason].details[detail] =
(this.filterStatistics.included[reason].details[detail] || 0) + 1;
}
}
public getFilterStatistics() {
return this.filterStatistics;
}
public getFormattedFilterDetails(): {
filterDetails: string[];
includedDetails: string[];
} {
const filterDetails: string[] = [];
for (const [reason, stats] of Object.entries(
this.filterStatistics.removed
)) {
if (stats.total > 0) {
// Convert camelCase to Title Case with spaces
const formattedReason = reason
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
filterDetails.push(`\n 📌 ${formattedReason} (${stats.total})`);
for (const [detail, count] of Object.entries(stats.details)) {
filterDetails.push(` • ${count}× ${detail}`);
}
}
}
const includedDetails: string[] = [];
for (const [reason, stats] of Object.entries(
this.filterStatistics.included
)) {
if (stats.total > 0) {
const formattedReason = reason
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
includedDetails.push(`\n 📌 ${formattedReason} (${stats.total})`);
for (const [detail, count] of Object.entries(stats.details)) {
includedDetails.push(` • ${count}× ${detail}`);
}
}
}
return { filterDetails, includedDetails };
}
public async filter(
@@ -33,70 +205,12 @@ class StreamFilterer {
type: string,
id: string
): Promise<ParsedStream[]> {
interface SkipReason {
total: number;
details: Record<string, number>;
}
const parsedId = IdParser.parse(id, type);
let isAnime = id.startsWith('kitsu');
if (AnimeDatabase.getInstance().isAnime(id)) {
logger.debug(
`Determined that ${id} is anime based on an existing entry in the anime database`
);
isAnime = true;
}
const skipReasons: Record<string, SkipReason> = {
titleMatching: { total: 0, details: {} },
yearMatching: { total: 0, details: {} },
seasonEpisodeMatching: { total: 0, details: {} },
excludedStreamType: { total: 0, details: {} },
requiredStreamType: { total: 0, details: {} },
excludedResolution: { total: 0, details: {} },
requiredResolution: { total: 0, details: {} },
excludedQuality: { total: 0, details: {} },
requiredQuality: { total: 0, details: {} },
excludedEncode: { total: 0, details: {} },
requiredEncode: { total: 0, details: {} },
excludedVisualTag: { total: 0, details: {} },
requiredVisualTag: { total: 0, details: {} },
excludedAudioTag: { total: 0, details: {} },
requiredAudioTag: { total: 0, details: {} },
excludedAudioChannel: { total: 0, details: {} },
requiredAudioChannel: { total: 0, details: {} },
excludedLanguage: { total: 0, details: {} },
requiredLanguage: { total: 0, details: {} },
excludedCached: { total: 0, details: {} },
requiredCached: { total: 0, details: {} },
excludedUncached: { total: 0, details: {} },
requiredUncached: { total: 0, details: {} },
excludedRegex: { total: 0, details: {} },
requiredRegex: { total: 0, details: {} },
excludedKeywords: { total: 0, details: {} },
requiredKeywords: { total: 0, details: {} },
requiredSeederRange: { total: 0, details: {} },
excludedSeederRange: { total: 0, details: {} },
excludedFilterCondition: { total: 0, details: {} },
requiredFilterCondition: { total: 0, details: {} },
size: { total: 0, details: {} },
};
const includedReasons: Record<string, SkipReason> = {
passthrough: { total: 0, details: {} },
resolution: { total: 0, details: {} },
quality: { total: 0, details: {} },
encode: { total: 0, details: {} },
visualTag: { total: 0, details: {} },
audioTag: { total: 0, details: {} },
audioChannel: { total: 0, details: {} },
language: { total: 0, details: {} },
streamType: { total: 0, details: {} },
size: { total: 0, details: {} },
seeder: { total: 0, details: {} },
regex: { total: 0, details: {} },
keywords: { total: 0, details: {} },
streamExpression: { total: 0, details: {} },
};
const start = Date.now();
const isRegexAllowed = await FeatureControl.isRegexAllowed(this.userData, [
@@ -507,9 +621,7 @@ class StreamFilterer {
const file = stream.parsedFile;
if (stream.addon.resultPassthrough) {
includedReasons.passthrough.total++;
includedReasons.passthrough.details[stream.addon.name] =
(includedReasons.passthrough.details[stream.addon.name] || 0) + 1;
this.incrementIncludedReason('passthrough', stream.addon.name);
return true;
}
@@ -545,9 +657,7 @@ class StreamFilterer {
// carry out include checks first
if (this.userData.includedStreamTypes?.includes(stream.type)) {
includedReasons.streamType.total++;
includedReasons.streamType.details[stream.type] =
(includedReasons.streamType.details[stream.type] || 0) + 1;
this.incrementIncludedReason('streamType', stream.type);
return true;
}
@@ -560,9 +670,7 @@ class StreamFilterer {
(resolution) => (file?.resolution || 'Unknown') === resolution
);
if (resolution) {
includedReasons.resolution.total++;
includedReasons.resolution.details[resolution] =
(includedReasons.resolution.details[resolution] || 0) + 1;
this.incrementIncludedReason('resolution', resolution);
}
return true;
}
@@ -576,9 +684,7 @@ class StreamFilterer {
(quality) => (file?.quality || 'Unknown') === quality
);
if (quality) {
includedReasons.quality.total++;
includedReasons.quality.details[quality] =
(includedReasons.quality.details[quality] || 0) + 1;
this.incrementIncludedReason('quality', quality);
}
return true;
}
@@ -596,9 +702,7 @@ class StreamFilterer {
)
);
if (tag) {
includedReasons.visualTag.total++;
includedReasons.visualTag.details[tag] =
(includedReasons.visualTag.details[tag] || 0) + 1;
this.incrementIncludedReason('visualTag', tag);
}
return true;
}
@@ -612,9 +716,7 @@ class StreamFilterer {
(file?.audioTags.length ? file.audioTags : ['Unknown']).includes(tag)
);
if (tag) {
includedReasons.audioTag.total++;
includedReasons.audioTag.details[tag] =
(includedReasons.audioTag.details[tag] || 0) + 1;
this.incrementIncludedReason('audioTag', tag);
}
return true;
}
@@ -633,9 +735,7 @@ class StreamFilterer {
: ['Unknown']
).includes(channel)
);
includedReasons.audioChannel.total++;
includedReasons.audioChannel.details[channel!] =
(includedReasons.audioChannel.details[channel!] || 0) + 1;
this.incrementIncludedReason('audioChannel', channel!);
return true;
}
@@ -647,9 +747,7 @@ class StreamFilterer {
const lang = this.userData.includedLanguages.find((lang) =>
(file?.languages.length ? file.languages : ['Unknown']).includes(lang)
);
includedReasons.language.total++;
includedReasons.language.details[lang!] =
(includedReasons.language.details[lang!] || 0) + 1;
this.incrementIncludedReason('language', lang!);
return true;
}
@@ -662,9 +760,7 @@ class StreamFilterer {
(encode) => (file?.encode || 'Unknown') === encode
);
if (encode) {
includedReasons.encode.total++;
includedReasons.encode.details[encode] =
(includedReasons.encode.details[encode] || 0) + 1;
this.incrementIncludedReason('encode', encode);
}
return true;
}
@@ -673,10 +769,7 @@ class StreamFilterer {
includedRegexPatterns &&
(await testRegexes(stream, includedRegexPatterns))
) {
includedReasons.regex.total++;
includedReasons.regex.details[includedRegexPatterns[0].source] =
(includedReasons.regex.details[includedRegexPatterns[0].source] ||
0) + 1;
this.incrementIncludedReason('regex', includedRegexPatterns[0].source);
return true;
}
@@ -684,10 +777,10 @@ class StreamFilterer {
includedKeywordsPattern &&
(await testRegexes(stream, [includedKeywordsPattern]))
) {
includedReasons.keywords.total++;
includedReasons.keywords.details[includedKeywordsPattern.source] =
(includedReasons.keywords.details[includedKeywordsPattern.source] ||
0) + 1;
this.incrementIncludedReason(
'keywords',
includedKeywordsPattern.source
);
return true;
}
@@ -713,27 +806,21 @@ class StreamFilterer {
includedSeederRange[0] &&
(stream.torrent?.seeders ?? 0) > includedSeederRange[0]
) {
includedReasons.seeder.total++;
includedReasons.seeder.details[includedSeederRange[0]] =
(includedReasons.seeder.details[includedSeederRange[0]] || 0) + 1;
this.incrementIncludedReason('seeder', `>${includedSeederRange[0]}`);
return true;
}
if (
includedSeederRange[1] &&
(stream.torrent?.seeders ?? 0) < includedSeederRange[1]
) {
includedReasons.seeder.total++;
includedReasons.seeder.details[includedSeederRange[1]] =
(includedReasons.seeder.details[includedSeederRange[1]] || 0) + 1;
this.incrementIncludedReason('seeder', `<${includedSeederRange[1]}`);
return true;
}
}
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;
this.incrementRemovalReason('excludedStreamType', stream.type);
return false;
}
@@ -743,9 +830,7 @@ class StreamFilterer {
this.userData.requiredStreamTypes.length > 0 &&
!this.userData.requiredStreamTypes.includes(stream.type)
) {
skipReasons.requiredStreamType.total++;
skipReasons.requiredStreamType.details[stream.type] =
(skipReasons.requiredStreamType.details[stream.type] || 0) + 1;
this.incrementRemovalReason('requiredStreamType', stream.type);
return false;
}
@@ -755,11 +840,10 @@ class StreamFilterer {
(file?.resolution || 'Unknown') as any
)
) {
skipReasons.excludedResolution.total++;
skipReasons.excludedResolution.details[file?.resolution || 'Unknown'] =
(skipReasons.excludedResolution.details[
file?.resolution || 'Unknown'
] || 0) + 1;
this.incrementRemovalReason(
'excludedResolution',
file?.resolution || 'Unknown'
);
return false;
}
@@ -770,11 +854,10 @@ class StreamFilterer {
(file?.resolution || 'Unknown') as any
)
) {
skipReasons.requiredResolution.total++;
skipReasons.requiredResolution.details[file?.resolution || 'Unknown'] =
(skipReasons.requiredResolution.details[
file?.resolution || 'Unknown'
] || 0) + 1;
this.incrementRemovalReason(
'requiredResolution',
file?.resolution || 'Unknown'
);
return false;
}
@@ -784,10 +867,10 @@ class StreamFilterer {
(file?.quality || 'Unknown') as any
)
) {
skipReasons.excludedQuality.total++;
skipReasons.excludedQuality.details[file?.quality || 'Unknown'] =
(skipReasons.excludedQuality.details[file?.quality || 'Unknown'] ||
0) + 1;
this.incrementRemovalReason(
'excludedQuality',
file?.quality || 'Unknown'
);
return false;
}
@@ -798,10 +881,10 @@ class StreamFilterer {
(file?.quality || 'Unknown') as any
)
) {
skipReasons.requiredQuality.total++;
skipReasons.requiredQuality.details[file?.quality || 'Unknown'] =
(skipReasons.requiredQuality.details[file?.quality || 'Unknown'] ||
0) + 1;
this.incrementRemovalReason(
'requiredQuality',
file?.quality || 'Unknown'
);
return false;
}
@@ -811,10 +894,10 @@ class StreamFilterer {
file?.encode || ('Unknown' as any)
)
) {
skipReasons.excludedEncode.total++;
skipReasons.excludedEncode.details[file?.encode || 'Unknown'] =
(skipReasons.excludedEncode.details[file?.encode || 'Unknown'] || 0) +
1;
this.incrementRemovalReason(
'excludedEncode',
file?.encode || 'Unknown'
);
return false;
}
@@ -825,10 +908,10 @@ class StreamFilterer {
file?.encode || ('Unknown' as any)
)
) {
skipReasons.requiredEncode.total++;
skipReasons.requiredEncode.details[file?.encode || 'Unknown'] =
(skipReasons.requiredEncode.details[file?.encode || 'Unknown'] || 0) +
1;
this.incrementRemovalReason(
'requiredEncode',
file?.encode || 'Unknown'
);
return false;
}
@@ -844,9 +927,7 @@ class StreamFilterer {
tag
)
);
skipReasons.excludedVisualTag.total++;
skipReasons.excludedVisualTag.details[tag!] =
(skipReasons.excludedVisualTag.details[tag!] || 0) + 1;
this.incrementRemovalReason('excludedVisualTag', tag!);
return false;
}
@@ -859,13 +940,10 @@ class StreamFilterer {
)
)
) {
skipReasons.requiredVisualTag.total++;
skipReasons.requiredVisualTag.details[
`${this.userData.requiredVisualTags.join(', ')}`
] =
(skipReasons.requiredVisualTag.details[
`${this.userData.requiredVisualTags.join(', ')}`
] || 0) + 1;
this.incrementRemovalReason(
'requiredVisualTag',
file?.visualTags.length ? file.visualTags.join(', ') : 'Unknown'
);
return false;
}
@@ -877,9 +955,7 @@ class StreamFilterer {
const tag = this.userData.excludedAudioTags.find((tag) =>
(file?.audioTags.length ? file.audioTags : ['Unknown']).includes(tag)
);
skipReasons.excludedAudioTag.total++;
skipReasons.excludedAudioTag.details[tag!] =
(skipReasons.excludedAudioTag.details[tag!] || 0) + 1;
this.incrementRemovalReason('excludedAudioTag', tag!);
return false;
}
@@ -890,12 +966,10 @@ class StreamFilterer {
(file?.audioTags.length ? file.audioTags : ['Unknown']).includes(tag)
)
) {
const tag = this.userData.requiredAudioTags.find((tag) =>
(file?.audioTags.length ? file.audioTags : ['Unknown']).includes(tag)
this.incrementRemovalReason(
'requiredAudioTag',
file?.audioTags.length ? file.audioTags.join(', ') : 'Unknown'
);
skipReasons.requiredAudioTag.total++;
skipReasons.requiredAudioTag.details[tag!] =
(skipReasons.requiredAudioTag.details[tag!] || 0) + 1;
return false;
}
@@ -913,9 +987,7 @@ class StreamFilterer {
: ['Unknown']
).includes(channel)
);
skipReasons.excludedAudioChannel.total++;
skipReasons.excludedAudioChannel.details[channel!] =
(skipReasons.excludedAudioChannel.details[channel!] || 0) + 1;
this.incrementRemovalReason('excludedAudioChannel', channel!);
return false;
}
@@ -929,15 +1001,10 @@ class StreamFilterer {
).includes(channel)
)
) {
const channel = this.userData.requiredAudioChannels.find((channel) =>
(file?.audioChannels.length
? file.audioChannels
: ['Unknown']
).includes(channel)
this.incrementRemovalReason(
'requiredAudioChannel',
file?.audioChannels.length ? file.audioChannels.join(', ') : 'Unknown'
);
skipReasons.requiredAudioChannel.total++;
skipReasons.requiredAudioChannel.details[channel!] =
(skipReasons.requiredAudioChannel.details[channel!] || 0) + 1;
return false;
}
@@ -948,10 +1015,10 @@ class StreamFilterer {
this.userData.excludedLanguages!.includes(lang as any)
)
) {
const lang = file?.languages[0] || 'Unknown';
skipReasons.excludedLanguage.total++;
skipReasons.excludedLanguage.details[lang] =
(skipReasons.excludedLanguage.details[lang] || 0) + 1;
this.incrementRemovalReason(
'excludedLanguage',
file?.languages.length ? file.languages.join(', ') : 'Unknown'
);
return false;
}
@@ -962,24 +1029,22 @@ class StreamFilterer {
(file?.languages.length ? file.languages : ['Unknown']).includes(lang)
)
) {
const lang = this.userData.requiredLanguages.find((lang) =>
(file?.languages.length ? file.languages : ['Unknown']).includes(lang)
this.incrementRemovalReason(
'requiredLanguage',
file?.languages.length ? file.languages.join(', ') : 'Unknown'
);
skipReasons.requiredLanguage.total++;
skipReasons.requiredLanguage.details[lang!] =
(skipReasons.requiredLanguage.details[lang!] || 0) + 1;
return false;
}
// uncached
if (this.userData.excludeUncached && stream.service?.cached === false) {
skipReasons.excludedUncached.total++;
this.incrementRemovalReason('excludedUncached');
return false;
}
if (this.userData.excludeCached && stream.service?.cached === true) {
skipReasons.excludedCached.total++;
this.incrementRemovalReason('excludedCached');
return false;
}
@@ -993,7 +1058,7 @@ class StreamFilterer {
true
) === false
) {
skipReasons.excludedCached.total++;
this.incrementRemovalReason('excludedCached');
return false;
}
@@ -1007,7 +1072,7 @@ class StreamFilterer {
false
) === false
) {
skipReasons.excludedUncached.total++;
this.incrementRemovalReason('excludedUncached');
return false;
}
@@ -1015,7 +1080,7 @@ class StreamFilterer {
excludedRegexPatterns &&
(await testRegexes(stream, excludedRegexPatterns))
) {
skipReasons.excludedRegex.total++;
this.incrementRemovalReason('excludedRegex');
return false;
}
if (
@@ -1023,7 +1088,7 @@ class StreamFilterer {
requiredRegexPatterns.length > 0 &&
!(await testRegexes(stream, requiredRegexPatterns))
) {
skipReasons.requiredRegex.total++;
this.incrementRemovalReason('requiredRegex');
return false;
}
@@ -1031,7 +1096,7 @@ class StreamFilterer {
excludedKeywordsPattern &&
(await testRegexes(stream, [excludedKeywordsPattern]))
) {
skipReasons.excludedKeywords.total++;
this.incrementRemovalReason('excludedKeywords');
return false;
}
@@ -1039,7 +1104,7 @@ class StreamFilterer {
requiredKeywordsPattern &&
!(await testRegexes(stream, [requiredKeywordsPattern]))
) {
skipReasons.requiredKeywords.total++;
this.incrementRemovalReason('requiredKeywords');
return false;
}
@@ -1053,13 +1118,10 @@ class StreamFilterer {
requiredSeederRange[0] &&
(stream.torrent?.seeders ?? 0) < requiredSeederRange[0]
) {
skipReasons.requiredSeederRange.total++;
skipReasons.requiredSeederRange.details[
`Less than ${requiredSeederRange[0]}`
] =
(skipReasons.requiredSeederRange.details[
`Less than ${requiredSeederRange[0]}`
] || 0) + 1;
this.incrementRemovalReason(
'requiredSeederRange',
`< ${requiredSeederRange[0]}`
);
return false;
}
if (
@@ -1067,13 +1129,10 @@ class StreamFilterer {
requiredSeederRange[1] &&
(stream.torrent?.seeders ?? 0) > requiredSeederRange[1]
) {
skipReasons.requiredSeederRange.total++;
skipReasons.requiredSeederRange.details[
`Greater than ${requiredSeederRange[1]}`
] =
(skipReasons.requiredSeederRange.details[
`Greater than ${requiredSeederRange[1]}`
] || 0) + 1;
this.incrementRemovalReason(
'requiredSeederRange',
`> ${requiredSeederRange[1]}`
);
return false;
}
}
@@ -1088,49 +1147,37 @@ class StreamFilterer {
excludedSeederRange[0] &&
(stream.torrent?.seeders ?? 0) > excludedSeederRange[0]
) {
skipReasons.excludedSeederRange.total++;
skipReasons.excludedSeederRange.details[
`Less than ${excludedSeederRange[0]}`
] =
(skipReasons.excludedSeederRange.details[
`Less than ${excludedSeederRange[0]}`
] || 0) + 1;
this.incrementRemovalReason(
'excludedSeederRange',
`< ${excludedSeederRange[0]}`
);
return false;
}
if (
excludedSeederRange[1] &&
(stream.torrent?.seeders ?? 0) < excludedSeederRange[1]
) {
skipReasons.excludedSeederRange.total++;
skipReasons.excludedSeederRange.details[
`Greater than ${excludedSeederRange[1]}`
] =
(skipReasons.excludedSeederRange.details[
`Greater than ${excludedSeederRange[1]}`
] || 0) + 1;
this.incrementRemovalReason(
'excludedSeederRange',
`> ${excludedSeederRange[1]}`
);
return false;
}
}
if (!performTitleMatch(stream)) {
skipReasons.titleMatching.total++;
skipReasons.titleMatching.details[
this.incrementRemovalReason(
'titleMatching',
`${stream.parsedFile?.title || 'Unknown Title'}${type === 'movie' ? ` - (${stream.parsedFile?.year || 'Unknown Year'})` : ''}`
] =
(skipReasons.titleMatching.details[
`${stream.parsedFile?.title || 'Unknown Title'}${type === 'movie' ? ` - (${stream.parsedFile?.year || 'Unknown Year'})` : ''}`
] || 0) + 1;
);
return false;
}
if (!performYearMatch(stream)) {
skipReasons.yearMatching.total++;
skipReasons.yearMatching.details[
this.incrementRemovalReason(
'yearMatching',
`${stream.parsedFile?.title || 'Unknown Title'} - ${stream.parsedFile?.year || 'Unknown Year'}`
] =
(skipReasons.yearMatching.details[
`${stream.parsedFile?.title || 'Unknown Title'} - ${stream.parsedFile?.year || 'Unknown Year'}`
] || 0) + 1;
);
return false;
}
@@ -1140,9 +1187,7 @@ class StreamFilterer {
' ' +
(stream.parsedFile?.seasonEpisode?.join(' x ') || 'Unknown');
skipReasons.seasonEpisodeMatching.total++;
skipReasons.seasonEpisodeMatching.details[detail] =
(skipReasons.seasonEpisodeMatching.details[detail] || 0) + 1;
this.incrementRemovalReason('seasonEpisodeMatching', detail);
return false;
}
@@ -1167,11 +1212,11 @@ class StreamFilterer {
if (minMax) {
if (stream.size && minMax[0] && stream.size < minMax[0]) {
skipReasons.size.total++;
this.incrementRemovalReason('size', `< ${minMax[0]}`);
return false;
}
if (stream.size && minMax[1] && stream.size > minMax[1]) {
skipReasons.size.total++;
this.incrementRemovalReason('size', `> ${minMax[1]}`);
return false;
}
}
@@ -1180,7 +1225,7 @@ class StreamFilterer {
};
const includedStreamsByExpression =
await this.applyIncludedStreamExpressions(streams);
await this.applyIncludedStreamExpressions(streams, type, id);
if (includedStreamsByExpression.length > 0) {
logger.info(
`${includedStreamsByExpression.length} streams were included by stream expressions`
@@ -1204,7 +1249,7 @@ class StreamFilterer {
...filteredStreams,
]);
// Log filter summary
// L// filter summary
const totalFiltered = streams.length - finalStreams.length;
const summary = [
@@ -1217,44 +1262,16 @@ class StreamFilterer {
];
// Add filter details if any streams were filtered
const filterDetails: string[] = [];
for (const [reason, stats] of Object.entries(skipReasons)) {
if (stats.total > 0) {
// Convert camelCase to Title Case with spaces
const formattedReason = reason
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
filterDetails.push(`\n 📌 ${formattedReason} (${stats.total})`);
for (const [detail, count] of Object.entries(stats.details)) {
filterDetails.push(` • ${count}× ${detail}`);
}
}
}
const includedDetails: string[] = [];
for (const [reason, stats] of Object.entries(includedReasons)) {
if (stats.total > 0) {
const formattedReason = reason
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
includedDetails.push(`\n 📌 ${formattedReason} (${stats.total})`);
for (const [detail, count] of Object.entries(stats.details)) {
includedDetails.push(` • ${count}× ${detail}`);
}
}
}
const { filterDetails, includedDetails } = this.getFormattedFilterDetails();
if (filterDetails.length > 0) {
summary.push('\n 🔎 Filter Details:');
summary.push(...filterDetails);
}
if (includedDetails.length > 0) {
summary.push('\n 🔎 Included Details:');
summary.push(...includedDetails);
}
summary.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
logger.info(summary.join('\n'));
@@ -1263,9 +1280,16 @@ class StreamFilterer {
}
public async applyIncludedStreamExpressions(
streams: ParsedStream[]
streams: ParsedStream[],
type: string,
id: string
): Promise<ParsedStream[]> {
const selector = new StreamSelector();
let queryType = type;
if (AnimeDatabase.getInstance().isAnime(id)) {
queryType = 'anime';
}
const selector = new StreamSelector(queryType);
const streamsToKeep = new Set<string>();
if (
!this.userData.includedStreamExpressions ||
@@ -1281,21 +1305,16 @@ class StreamFilterer {
}
public async applyStreamExpressionFilters(
streams: ParsedStream[]
streams: ParsedStream[],
type: string,
id: string
): Promise<ParsedStream[]> {
const skipReasons: Record<
string,
{ total: number; details: Record<string, number> }
> = {
excludedFilterCondition: {
total: 0,
details: {},
},
requiredFilterCondition: {
total: 0,
details: {},
},
};
let queryType = type;
if (AnimeDatabase.getInstance().isAnime(id)) {
queryType = 'anime';
}
const passthroughStreams = streams
.filter((stream) => stream.addon.resultPassthrough)
.map((stream) => stream.id);
@@ -1303,7 +1322,7 @@ class StreamFilterer {
this.userData.excludedStreamExpressions &&
this.userData.excludedStreamExpressions.length > 0
) {
const selector = new StreamSelector();
const selector = new StreamSelector(queryType);
const streamsToRemove = new Set<string>(); // Track actual stream objects to be removed
for (const expression of this.userData.excludedStreamExpressions) {
@@ -1323,9 +1342,9 @@ class StreamFilterer {
// Update skip reasons for this condition (only count newly selected streams)
if (selectedStreams.length > 0) {
skipReasons.excludedFilterCondition.total += selectedStreams.length;
skipReasons.excludedFilterCondition.details[expression] =
selectedStreams.length;
this.incrementRemovalReason('excludedFilterCondition', expression);
// skipReasons.excludedFilterCondition.details[expression] =
// selectedStreams.length;
}
} catch (error) {
logger.error(
@@ -1347,7 +1366,7 @@ class StreamFilterer {
this.userData.requiredStreamExpressions &&
this.userData.requiredStreamExpressions.length > 0
) {
const selector = new StreamSelector();
const selector = new StreamSelector(queryType);
const streamsToKeep = new Set<string>(); // Track actual stream objects to be removed
passthroughStreams.forEach((stream) => streamsToKeep.add(stream));
@@ -1363,10 +1382,7 @@ class StreamFilterer {
// Update skip reasons for this condition (only count newly selected streams)
if (selectedStreams.length > 0) {
skipReasons.requiredFilterCondition.total +=
streams.length - selectedStreams.length;
skipReasons.requiredFilterCondition.details[expression] =
streams.length - selectedStreams.length;
this.incrementRemovalReason('requiredFilterCondition', expression);
}
} catch (error) {
logger.error(
+7 -2
View File
@@ -7,6 +7,7 @@ import {
formRegexFromKeywords,
compileRegex,
parseRegex,
AnimeDatabase,
} from '../utils/index.js';
import { StreamSelector } from '../parser/streamExpression.js';
@@ -19,7 +20,11 @@ class StreamPrecomputer {
this.userData = userData;
}
public async precompute(streams: ParsedStream[]) {
public async precompute(streams: ParsedStream[], type: string, id: string) {
let queryType = type;
if (AnimeDatabase.getInstance().isAnime(id)) {
queryType = 'anime';
}
const preferredRegexPatterns =
(await FeatureControl.isRegexAllowed(
this.userData,
@@ -108,7 +113,7 @@ class StreamPrecomputer {
}
if (this.userData.preferredStreamExpressions?.length) {
const selector = new StreamSelector();
const selector = new StreamSelector(queryType);
const streamToConditionIndex = new Map<string, number>();
// Go through each preferred filter condition, from highest to lowest priority.