Custom matching rules for automatic filtering and acceptance of manga.
This module evaluates user-defined regex patterns against manga metadata.
Rules can skip (exclude) or accept (boost confidence) manga based on pattern matches
across various metadata fields: titles, author, genres, tags, format, country, source,
description, and status.
/** * Confidence floor for accept rule boosts on exact title matches. * @source */ exportconstACCEPT_RULE_CONFIDENCE_FLOOR_EXACT = 0.85;
/** * Confidence floor for accept rule boosts on regular (non-exact) matches. * @source */ exportconstACCEPT_RULE_CONFIDENCE_FLOOR_REGULAR = 0.75;
/** * Cache for compiled regex patterns to avoid repeated compilation. * Keys: `${ruleId}:${pattern}:${flags}`. Size capped at 1000 entries with FIFO eviction. * @source */ constregexCache = newMap<string, RegExp>();
/** * Maximum number of compiled regex patterns to cache. * @constant * @source */ constMAX_REGEX_CACHE_SIZE = 1000;
/** * Gets the effective target fields for a rule, applying fallback default if needed. * @paramrule - The custom rule * @returns Array of target fields, defaulting to ['titles'] if not set * @source */ functiongetEffectiveTargetFields(rule: CustomRule): CustomRuleTarget[] { returnrule.targetFields?.length ? rule.targetFields : ["titles"]; }
/** * Clears all compiled regex patterns from the cache. * Called when custom rules are updated to prevent stale patterns. * @source */ exportfunctionclearRegexCache(): void { regexCache.clear(); console.debug(`[CustomRules] Regex cache cleared`); }
/** * Extracts all title variants from manga data. * @parammanga - AniList manga data * @paramkenmeiManga - Kenmei manga data * @returns Array of title strings * @source */ functionextractTitles( manga: AniListManga, kenmeiManga: KenmeiManga, ): string[] { constvalues: string[] = [];
// AniList titles if (manga.title?.romaji) values.push(manga.title.romaji); if (manga.title?.english) values.push(manga.title.english); if (manga.title?.native) values.push(manga.title.native); if (manga.synonyms) values.push(...manga.synonyms);
// Kenmei titles if (kenmeiManga.title) values.push(kenmeiManga.title); if (kenmeiManga.alternativeTitles) { values.push(...kenmeiManga.alternativeTitles); }
returnvalues; }
/** * Extracts author/staff names from manga data. * Filters AniList staff by relevant roles: Story, Art, Original Creator. * @parammanga - AniList manga data * @paramkenmeiManga - Kenmei manga data * @returns Array of author/staff names * @source */ functionextractAuthors( manga: AniListManga, kenmeiManga: KenmeiManga, ): string[] { constvalues: string[] = [];
// Kenmei author if (kenmeiManga.author) values.push(kenmeiManga.author);
// AniList staff (filter by relevant roles) if (manga.staff?.edges) { constrelevantRoles = ["Story", "Art", "Original Creator"]; for (constedgeofmanga.staff.edges) { constrole = edge.role; if (role && relevantRoles.some((r) =>role.includes(r))) { if (edge.node?.name?.full) { values.push(edge.node.name.full); } } } }
returnvalues; }
/** * Extracts tag names and categories from manga data. * @parammanga - AniList manga data * @returns Array of tag names and categories * @source */ functionextractTags(manga: AniListManga): string[] { constvalues: string[] = [];
if (manga.tags) { for (consttagofmanga.tags) { if (tag.name) values.push(tag.name); if (tag.category) values.push(tag.category); } }
returnvalues; }
/** * Extracts description text with HTML stripped from manga data. * @parammanga - AniList manga data * @paramkenmeiManga - Kenmei manga data * @returns Array of description strings * @source */ functionextractDescriptions( manga: AniListManga, kenmeiManga: KenmeiManga, ): string[] { constvalues: string[] = [];
// Strip HTML tags from description if (manga.description) { conststripped = manga.description.replaceAll(/<[^>]*>/g, ""); if (stripped) values.push(stripped); } if (kenmeiManga.notes) values.push(kenmeiManga.notes);
returnvalues; }
/** * Extracts metadata values for a specific target field from manga data. * @paramtargetField - The metadata field to extract (titles, author, genres, tags, etc.) * @parammanga - The AniList manga data * @paramkenmeiManga - The Kenmei manga data * @returns Array of string values from the specified field * @source */ functionextractMetadataValues( targetField: CustomRuleTarget, manga: AniListManga, kenmeiManga: KenmeiManga, ): string[] { switch (targetField) { case"titles": returnextractTitles(manga, kenmeiManga);
case"status": { constvalues: string[] = []; if (manga.status) values.push(manga.status); if (kenmeiManga.status) values.push(kenmeiManga.status); returnvalues; }
default: return []; } }
/** * Tests a custom rule pattern against manga metadata fields. * @paramrule - The custom rule with regex pattern and target fields * @parammanga - The AniList manga data * @paramkenmeiManga - The Kenmei manga data * @returns True if the pattern matches any value in the target fields, false otherwise * @source */ functiontestRuleAgainstMetadata( rule: CustomRule, manga: AniListManga, kenmeiManga: KenmeiManga, ): boolean { try { // Use same effective target fields as used in logging consttargetFields = getEffectiveTargetFields(rule);
// Extract metadata values from all target fields constallValues: string[] = []; for (consttargetFieldoftargetFields) { constvalues = extractMetadataValues(targetField, manga, kenmeiManga); allValues.push(...values); }
// If no values extracted, no match if (allValues.length === 0) { returnfalse; }
// Include Unicode flag (u) for better international title support constflags = `u${rule.caseSensitive?"":"i"}`;
if (!regex) { regex = newRegExp(rule.pattern, flags); regexCache.set(cacheKey, regex);
// Implement FIFO eviction when cache exceeds max size if (regexCache.size > MAX_REGEX_CACHE_SIZE) { constfirstKey = regexCache.keys().next().value; if (firstKey) { regexCache.delete(firstKey); console.debug( `[CustomRules] Regex cache evicted oldest entry (size: ${regexCache.size}/${MAX_REGEX_CACHE_SIZE})`, ); } } }
// Cap per-value length to prevent catastrophic backtracking // 10,000 characters is a reasonable limit for most practical use cases constMAX_VALUE_LENGTH = 10000;
returnallValues.some((value) => { // Truncate value to prevent regex backtracking on extremely long strings consttruncatedValue = value.length > MAX_VALUE_LENGTH ? value.substring(0, MAX_VALUE_LENGTH) : value; returnregex.test(truncatedValue); }); } catch (error) { console.error( `[CustomRules] Invalid regex pattern in rule "${rule.description}": ${errorinstanceofError?error.message:"Unknown error"}`, ); returnfalse; } }
/** * Checks if manga should be skipped based on custom skip rules. * @parammanga - The AniList manga to check * @paramkenmeiManga - The original Kenmei manga entry * @paramisManualSearch - Whether this is a manual search (skip rules don't apply) * @returns True if manga should be skipped, false otherwise * @source */ exportfunctionshouldSkipByCustomRules( manga: AniListManga, kenmeiManga: KenmeiManga, isManualSearch: boolean, ): boolean { // Custom skip rules don't apply to manual searches if (isManualSearch) { returnfalse; }
constcustomRules = getMatchConfig().customRules; if (!customRules) { returnfalse; }
// Filter to enabled rules only constenabledSkipRules = skipRules.filter((rule) =>rule.enabled); if (!enabledSkipRules.length) { returnfalse; }
// Check each enabled skip rule for (construleofenabledSkipRules) { if (testRuleAgainstMetadata(rule, manga, kenmeiManga)) { constcheckedFields = getEffectiveTargetFields(rule); console.debug( `[CustomRules] ⏭️ Skipping manga "${manga.title?.romaji||manga.title?.english||"unknown"}" due to custom skip rule: "${rule.description}" (checked fields: ${checkedFields.join(", ")})`, ); returntrue; } }
returnfalse; }
/** * Checks if manga should be auto-accepted based on custom accept rules. * @parammanga - The AniList manga to check * @paramkenmeiManga - The original Kenmei manga entry * @returns Object with shouldAccept flag and matched rule if applicable * @source */ exportfunctionshouldAcceptByCustomRules( manga: AniListManga, kenmeiManga: KenmeiManga, ): { shouldAccept: boolean; matchedRule?: CustomRule } { constcustomRules = getMatchConfig().customRules; if (!customRules) { return { shouldAccept:false }; }
// Filter to enabled rules only constenabledAcceptRules = acceptRules.filter((rule) =>rule.enabled); if (!enabledAcceptRules.length) { return { shouldAccept:false }; }
// Check each enabled accept rule for (construleofenabledAcceptRules) { if (testRuleAgainstMetadata(rule, manga, kenmeiManga)) { constcheckedFields = getEffectiveTargetFields(rule); console.debug( `[CustomRules] ✅ Auto-accepting manga "${manga.title?.romaji||manga.title?.english||"unknown"}" due to custom accept rule: "${rule.description}" (checked fields: ${checkedFields.join(", ")})`, ); return { shouldAccept:true, matchedRule:rule }; } }
return { shouldAccept:false }; }
/** * Gets custom rule match information for debugging and UI display. * @parammanga - The AniList manga to check * @paramkenmeiManga - The original Kenmei manga entry * @returns Object with skip and accept rule matches if applicable * @source */ exportfunctiongetCustomRuleMatchInfo( manga: AniListManga, kenmeiManga: KenmeiManga, ): { skipMatch?: CustomRule; acceptMatch?: CustomRule } { constcustomRules = getMatchConfig().customRules; if (!customRules) { return {}; }
Custom matching rules for automatic filtering and acceptance of manga.
This module evaluates user-defined regex patterns against manga metadata. Rules can skip (exclude) or accept (boost confidence) manga based on pattern matches across various metadata fields: titles, author, genres, tags, format, country, source, description, and status.
Source