diff --git a/packages/core/src/main.ts b/packages/core/src/main.ts index f3041e6a..3f98e655 100644 --- a/packages/core/src/main.ts +++ b/packages/core/src/main.ts @@ -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) => { diff --git a/packages/core/src/parser/streamExpression.ts b/packages/core/src/parser/streamExpression.ts index 2c67ec43..64fbdb33 100644 --- a/packages/core/src/parser/streamExpression.ts +++ b/packages/core/src/parser/streamExpression.ts @@ -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 { - const parser = new StreamSelector(); + const parser = new StreamSelector('movie'); const streams = [ parser.createTestStream({ type: 'debrid' }), parser.createTestStream({ type: 'debrid' }), diff --git a/packages/core/src/streams/fetcher.ts b/packages/core/src/streams/fetcher.ts index 827f5bcc..98421789 100644 --- a/packages/core/src/streams/fetcher.ts +++ b/packages/core/src/streams/fetcher.ts @@ -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)}` diff --git a/packages/core/src/streams/filterer.ts b/packages/core/src/streams/filterer.ts index 046d7ff3..72dc6092 100644 --- a/packages/core/src/streams/filterer.ts +++ b/packages/core/src/streams/filterer.ts @@ -21,11 +21,183 @@ import { calculateAbsoluteEpisode } from '../builtins/utils/general.js'; const logger = createLogger('filterer'); +interface Reason { + total: number; + details: Record; +} + +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 { - interface SkipReason { - total: number; - details: Record; - } 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 = { - 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 = { - 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 { - const selector = new StreamSelector(); + let queryType = type; + + if (AnimeDatabase.getInstance().isAnime(id)) { + queryType = 'anime'; + } + const selector = new StreamSelector(queryType); const streamsToKeep = new Set(); if ( !this.userData.includedStreamExpressions || @@ -1281,21 +1305,16 @@ class StreamFilterer { } public async applyStreamExpressionFilters( - streams: ParsedStream[] + streams: ParsedStream[], + type: string, + id: string ): Promise { - const skipReasons: Record< - string, - { total: number; details: Record } - > = { - 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(); // 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(); // 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( diff --git a/packages/core/src/streams/precomputer.ts b/packages/core/src/streams/precomputer.ts index 11ef47ea..d0516854 100644 --- a/packages/core/src/streams/precomputer.ts +++ b/packages/core/src/streams/precomputer.ts @@ -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(); // Go through each preferred filter condition, from highest to lowest priority.