UNPKG

anipar

Version:

Anime title Parser built for AnimeGarden.

1,741 lines (1,735 loc) 56.5 kB
class Context { // Left cursor for consumed tokens (inclusive) left = 0; // Right cursor for consumed tokens (inclusive) right; tokens; options; result; tags; constructor(tokens, options) { this.tokens = tokens; this.options = options; this.right = tokens.length - 1; this.tags = []; this.result = {}; if (options.fansub) { this.result.fansub = { name: options.fansub }; } } update(key, value) { this.result[key] = value; } update2(key1, key2, value) { if (!this.result[key1]) { this.result[key1] = {}; } this.result[key1][key2] = value; } update3(key1, key2, key3, value) { if (!this.result[key1]) { this.result[key1] = {}; } if (!this.result[key1][key2]) { this.result[key1][key2] = {}; } this.result[key1][key2][key3] = value; } get hasEpisode() { return this.result.episode || this.result.episodes || this.result.episodesRange; } normalize() { if (this.tags.length > 0) { this.result.tags = [.../* @__PURE__ */ new Set([...this.result.tags ?? [], ...this.tags])]; } if (this.result.subtitle?.languages) { const languages = this.result.subtitle?.languages; this.result.subtitle.languages = normalizeLanguages(languages); } if (this.result.subtitle?.format) { this.result.subtitle.format = normalizeSubtitleFormat(this.result.subtitle.format); } if (this.result.subtitle?.encoding) { this.result.subtitle.encoding = normalizeSubtitleEncoding(this.result.subtitle.encoding); } if (this.result.subtitle?.encodings) { this.result.subtitle.encodings = normalizeSubtitleEncodings(this.result.subtitle.encodings); } if (this.result.file) { normalizeFileInfo(this.result.file); } if (this.result.variants) { this.result.variants = [...new Set(this.result.variants)]; } if (this.result.title && this.result.title.length > 0) { return this.result; } return void 0; } } function normalizeSubtitleFormat(format) { const trimmed = format.trim(); const upper = normalizeUpperTag(trimmed); { const res = /^(ASS|SRT)(?:[Xx×](\d+))?$/i.exec(trimmed); if (res) { return res[2] ? `${res[1].toUpperCase()}\u5B57\u5E55\xD7${res[2]}` : `${res[1].toUpperCase()}\u5B57\u5E55`; } } if (/^HARDSUBS?$/.test(upper) || /^(内嵌|內嵌)(字幕)?$/.test(trimmed)) { return "\u5185\u5D4C\u5B57\u5E55"; } if (/^SOFTSUBS?$/.test(upper)) { return "\u8F6F\u5B57\u5E55"; } if (/^(内封|內封)(字幕)?$/.test(trimmed)) { return "\u5185\u5C01\u5B57\u5E55"; } if (/^(外挂|外掛)(字幕)?$/.test(trimmed)) { return "\u5916\u6302\u5B57\u5E55"; } if (/^(内挂|內掛)(字幕)?$/.test(trimmed)) { return "\u5185\u6302\u5B57\u5E55"; } if (/^(SUB|SUBBED|SUBTITLED)$/.test(upper) || trimmed === "\u5B57\u5E55") { return "\u5B57\u5E55"; } return trimmed.replaceAll("\u5167", "\u5185").replaceAll("\u639B", "\u6302"); } function normalizeSubtitleEncoding(encoding) { const upper = normalizeUpperTag(encoding); if (upper === "GB" || upper === "BIG5") { return upper; } return upper; } function normalizeSubtitleEncodings(encodings) { const normalized = new Set(encodings.map(normalizeSubtitleEncoding)); const order = ["GB", "BIG5"]; return [ ...order.filter((encoding) => normalized.delete(encoding)), ...encodings.map(normalizeSubtitleEncoding).filter((encoding) => normalized.delete(encoding)) ]; } function normalizeFileInfo(file) { if (file.audio) { normalizeAudioInfo(file.audio); } if (file.video) { normalizeVideoInfo(file.video); } } function normalizeAudioInfo(audio) { if (audio.channels) { audio.channels = normalizeAudioChannels(audio.channels); } if (audio.codec) { audio.codec = normalizeAudioCodec(audio.codec); } if (audio.language) { audio.language = normalizeAudioLanguage(audio.language); } } function normalizeVideoInfo(video) { if (video.bitDepth) { video.bitDepth = normalizeBitDepth(video.bitDepth); } if (video.codec) { video.codec = normalizeVideoCodec(video.codec); } if (video.enhancement) { video.enhancement = normalizeUpperTag(video.enhancement); } if (video.format) { video.format = normalizeUpperTag(video.format); } if (video.fps) { video.fps = normalizeFrameRate(video.fps); } if (video.quality) { video.quality = normalizeUpperTag(video.quality); } if (video.resolution) { video.resolution = normalizeVideoResolution(video.resolution); } } function normalizeUpperTag(value) { return value.trim().toUpperCase(); } function normalizeLowerTag(value) { return value.trim().toLowerCase(); } function normalizeAudioCodec(codec) { const upper = normalizeUpperTag(codec); const codecs = /* @__PURE__ */ new Map([ ["AAC", "AAC"], ["AC3", "AC-3"], ["AC-3", "AC-3"], ["DTS", "DTS"], ["DTS-ES", "DTS-ES"], ["EAC3", "E-AC-3"], ["E-AC-3", "E-AC-3"], ["EAC3&AAC", "E-AC-3+AAC"], ["FLAC", "FLAC"], ["FLAC/AC3", "FLAC+AC-3"], ["LOSSLESS", "lossless"], ["MP3", "MP3"], ["OGG", "Ogg"], ["OPUS", "Opus"], ["QAAC", "qaac"], ["TRUEHD", "TrueHD"], ["VORBIS", "Vorbis"], ["WAV", "WAV"] ]); return codecs.get(upper) ?? normalizeLowerTag(codec); } function normalizeVideoCodec(codec) { const upper = normalizeUpperTag(codec); if (/^(H\.?264|X\.?264|AVC)$/.test(upper)) { return "AVC"; } if (/^(H\.?265|X\.?265|HEVC2?|HVC1)$/.test(upper)) { return "HEVC"; } if (/^DIVX\d*$/.test(upper)) { return "DivX"; } if (upper === "XVID") { return "Xvid"; } if (/^HI10P?$/.test(upper)) { return "Hi10P"; } if (/^HI444P*$/.test(upper)) { return upper.replace("HI", "Hi"); } return normalizeLowerTag(codec); } function normalizeAudioChannels(channels) { const upper = normalizeUpperTag(channels); if (upper === "2CH") { return "2.0"; } if (upper === "5.1CH") { return "5.1"; } return upper.replace(/CH$/, ""); } function normalizeAudioLanguage(language) { const upper = normalizeUpperTag(language); if (upper === "DUALAUDIO" || upper === "DUAL AUDIO") { return "dual audio"; } return normalizeLowerTag(language); } function normalizeBitDepth(bitDepth) { const upper = normalizeUpperTag(bitDepth); const res = /^(\d+)[-_ ]?BITS?$/.exec(upper); if (res) { return `${res[1]}-bit`; } return normalizeLowerTag(bitDepth); } function normalizeFrameRate(fps) { const upper = normalizeUpperTag(fps); const res = /^(\d+(?:\.\d+)?)\s*FPS$/.exec(upper); return res ? `${res[1]}fps` : normalizeLowerTag(fps); } function normalizeVideoResolution(resolution) { const trimmed = resolution.trim(); const upper = trimmed.toUpperCase(); { const res = /^(\d{3,4})P$/i.exec(trimmed); if (res) { return `${res[1]}p`; } } { const res = /^(\d{3,5})[Xx×](\d{3,5})$/.exec(trimmed); if (res) { return `${res[1]}x${res[2]}`; } } if (/^\d+K$/.test(upper)) { return upper; } return trimmed; } const NormalizedLanguages = ["\u7B80", "\u7E41", "\u7CA4", "\u65E5", "\u82F1", "\u6CF0"]; function normalizeLanguage(language) { const trimmed = language.trim(); const upper = trimmed.toUpperCase(); if (/^(CN|CHINESE|ZH|中|中文|中字|国语中字|國語中字)$/.test(upper)) { return void 0; } const languages = []; const matches = { \u7B80: /(^|[^A-Z])CHS($|[^A-Z])|ZH[-_]?HANS|简|簡|简体|簡體|简中|簡中/i.test(trimmed), \u7E41: /(^|[^A-Z])CHT($|[^A-Z])|ZH[-_]?HANT|繁|繁体|繁體|繁中|BIG5/i.test(trimmed), \u7CA4: /(^|[^A-Z])YUE($|[^A-Z])|粤|粵|广东话|廣東話|CANTONESE/i.test(trimmed), \u65E5: /(^|[^A-Z])(JP|JPN|JA)($|[^A-Z])|日|日本|JAPANESE/i.test(trimmed), \u82F1: /(^|[^A-Z])(EN|ENG)($|[^A-Z])|英|ENGLISH/i.test(trimmed), \u6CF0: /(^|[^A-Z])(TH|THA)($|[^A-Z])|泰|THAI/i.test(trimmed) }; if (/[中华華]/.test(trimmed) && !matches.\u7B80 && !matches.\u7E41 && !matches.\u7CA4) { return void 0; } for (const language2 of NormalizedLanguages) { if (matches[language2]) { languages.push(language2); } } return languages.length > 0 ? languages : void 0; } function normalizeLanguages(languages) { const normalized = /* @__PURE__ */ new Set(); const unknown = []; for (const language of languages) { const parts = normalizeLanguage(language); if (parts) { parts.forEach((part) => normalized.add(part)); } else if (!unknown.includes(language)) { unknown.push(language); } } return [...NormalizedLanguages.filter((language) => normalized.has(language)), ...unknown]; } class Token { text; left; right; constructor(text, left, right) { this.text = text; this.left = left; this.right = right; } get isWrapped() { return this.left && this.right; } slice(start, end) { const text = this.text.slice(start, end); return new Token(text, this.left, this.right); } trim() { const text = this.text.trim(); return new Token(text, this.left, this.right); } toString() { return `${this.left ?? ""}${this.text}${this.right ?? ""}`; } } const Wrappers = /* @__PURE__ */ new Map([ ["[", "]"], ["\u3010", "\u3011"], ["(", ")"], ["\uFF08", "\uFF09"], ["{", "}"] ]); new Map([...Wrappers.entries()].map(([k, v]) => [v, k])); function tokenize(text) { const tokens = []; let cursor = 0; let cur = ""; let left = void 0; let right = void 0; while (cursor < text.length) { const char = text[cursor]; if (Wrappers.has(char)) { if (!left) { if (cur) { tokens.push(new Token(cur)); cur = ""; } left = char; right = Wrappers.get(char); } else { cur += char; } } else if (left && right && char === right) { tokens.push(new Token(cur, left, right)); cur = ""; left = void 0; right = void 0; } else { cur += char; } cursor += 1; } if (cur) { tokens.push(new Token(cur)); } return tokens.filter((t) => t.text); } const FileExtensions = /* @__PURE__ */ new Set([ "3GP", "AVI", "DIVX", "FLV", "M2TS", "MKV", "MOV", "MP4", "MPG", "OGM", "RM", "RMVB", "TS", "WEBM", "WMV" ]); function parseFileExtension(title) { const match = /\.([^.\/\s]+)\s*$/.exec(title); if (!match) return { title }; const extension = match[1].toUpperCase(); if (!FileExtensions.has(extension)) return { title }; return { title: title.slice(0, match.index).trimEnd(), extension }; } const WrappedEpisodeRE1 = /^(?<type>TV|OVA|OAD|SP)?(?<ep1>\d+)(?:\.(?<sub>\d))?(?:[vV](?<version>\d+))?(?:(?:\s+|_|-)?(?<ep_type>[^\d集]+))?$|^第(?<ep2>\d+)[集话話]$|^S(?<season>\d+)E(?<ep3>\d+)((?:[_|(])?(?<ep_type>[^\d][^)]*)(?:))?)?$/; const WrappedEpisodeRE2 = /^(?<ep1>\d+)(?:\+(?<type>[^\d]+)|,|&|、)(?<ep2>\d+)$/; const WrappedSeasonRE = /^(?:S|Season)(\d+)\s*(Fin|End)?$/; const WrappedMovieRE = /^Movie [vV](\d+)$/; const WrappedEpisodesRange1 = /^(?<type>TV|OVA|OAD|SP)?(?<ep1>\d+)(?:\.(?<sub1>\d))?[-~](?<ep2>\d+)(?:\.(?<sub2>\d))?\s*[_]?(?<range_type>.*)$/; const WrappedEpisodesRange2 = /^全(\d+)集$/; const WrappedSeasonsRE = /^S(?<season1>\d)\+S(?<season2>\d)$/; const WrappedSeasonsRangeRE = /^S(?<season1>\d)-S(?<season2>\d)$/; const WrappedVolumeRE = /^(?:Vol|vol|Volume|volume)\.?\s*(?<vol>\d+)$/; const WrappedVolumesRangeRE = /^(?:Vol|vol|Volume|volume)\.?\s*(?<vol1>\d+)-(?<vol2>\d+)\s+(?<type>.*)$/; function matchEpiodes(ctx, text) { text = text.trimEnd(); { const res = WrappedEpisodeRE1.exec(text); if (res) { const epText = res.groups?.ep1 || res.groups?.ep2 || res.groups?.ep3; const ep = +epText; if (!Number.isNaN(ep)) { if (1949 <= ep && ep <= 2099 && text === epText && ctx.hasEpisode) { ctx.update("year", ep); return true; } ctx.update2("episode", "number", ep); if (res.groups?.sub && !Number.isNaN(+res.groups.sub)) { ctx.update2("episode", "numberSub", +res.groups.sub); } if (res.groups?.version && !Number.isNaN(+res.groups.version)) { ctx.update("version", +res.groups.version); } if (res.groups?.type) { const type = res.groups.type.trim(); ctx.update("type", type); } if (res.groups?.ep_type) { const type = res.groups.ep_type.trim(); ctx.update2("episode", "type", type); } if (res.groups?.season && !Number.isNaN(+res.groups.season)) { const season = +res.groups.season; ctx.update2("season", "number", season); } return true; } } } { const res = WrappedEpisodeRE2.exec(text); if (res) { const ep1 = res.groups?.ep1 ? +res.groups.ep1 : NaN; const ep2 = res.groups?.ep2 ? +res.groups.ep2 : NaN; const type = res.groups?.type; if (!Number.isNaN(ep1) && !Number.isNaN(ep2)) { if (type) { ctx.update2("episode", "number", ep1); ctx.update("episodes", [ { number: ep2, type } ]); } else { ctx.update("episodes", [ { number: ep1 }, { number: ep2 } ]); } return true; } } } { const res = WrappedMovieRE.exec(text); if (res) { if (res[1] && !Number.isNaN(res[1])) { ctx.update("version", +res[1]); } ctx.update("type", "Movie"); return true; } } { const res = WrappedEpisodesRange1.exec(text); if (res) { const from = res.groups?.ep1 ? +res.groups.ep1 : NaN; const to = res.groups?.ep2 ? +res.groups.ep2 : NaN; if (!Number.isNaN(from) && !Number.isNaN(to)) { ctx.update2("episodesRange", "from", from); ctx.update2("episodesRange", "to", to); const type = res.groups?.type ? res.groups.type.trim() : void 0; if (type) { ctx.update("type", type); } const episodesRangeType = res.groups?.range_type ? res.groups.range_type.trim() : void 0; if (episodesRangeType) { const exec2 = /[vV](\d+)$/.exec(episodesRangeType); if (exec2) { const version = +exec2[1]; if (!Number.isNaN(version)) { ctx.update("version", version); ctx.update2( "episodesRange", "type", episodesRangeType.slice(0, episodesRangeType.length - exec2[0].length) ); } else { ctx.update2("episodesRange", "type", episodesRangeType); } } else { ctx.update2("episodesRange", "type", episodesRangeType); } } const sub1 = res.groups?.sub1 ? +res.groups?.sub1 : NaN; const sub2 = res.groups?.sub2 ? +res.groups?.sub2 : NaN; if (!Number.isNaN(sub1)) { ctx.update2("episodesRange", "fromSub", sub1); } if (!Number.isNaN(sub2)) { ctx.update2("episodesRange", "toSub", sub2); } return true; } } } { const res = WrappedEpisodesRange2.exec(text); if (res) { const to = +res[1]; if (!Number.isNaN(to)) { ctx.update2("episodesRange", "from", 1); ctx.update2("episodesRange", "to", to); return true; } } } { const res = WrappedSeasonRE.exec(text); if (res) { const season = +res[1]; if (!Number.isNaN(season)) { ctx.update2("season", "number", season); const tag = res[2]; if (tag) { ctx.tags.push(tag.trim()); } return true; } } } { const res = WrappedSeasonsRE.exec(text); if (res) { const season1 = res.groups?.season1 ? +res.groups.season1 : NaN; const season2 = res.groups?.season2 ? +res.groups.season2 : NaN; if (!Number.isNaN(season1) && !Number.isNaN(season2)) { ctx.update("seasons", [{ number: season1 }, { number: season2 }]); } return true; } } { const res = WrappedSeasonsRangeRE.exec(text); if (res) { const season1 = res.groups?.season1 ? +res.groups.season1 : NaN; const season2 = res.groups?.season2 ? +res.groups.season2 : NaN; if (!Number.isNaN(season1) && !Number.isNaN(season2)) { ctx.update2("seasonsRange", "from", season1); ctx.update2("seasonsRange", "to", season2); } return true; } } { const res = WrappedVolumeRE.exec(text); if (res) { const vol = res.groups?.vol ? +res.groups.vol : NaN; if (!Number.isNaN(vol)) { ctx.update2("volume", "number", vol); return true; } } } { const res = WrappedVolumesRangeRE.exec(text); if (res) { const vol1 = res.groups?.vol1 ? +res.groups.vol1 : NaN; const vol2 = res.groups?.vol2 ? +res.groups.vol2 : NaN; const type = res.groups?.type; if (!Number.isNaN(vol1) && !Number.isNaN(vol2)) { ctx.update2("volumesRange", "from", vol1); ctx.update2("volumesRange", "to", vol1); if (type) { ctx.update2("volumesRange", "type", type); } return true; } } } return false; } function parseWrappedEpisodes(ctx) { if (ctx.hasEpisode) { return true; } const token = ctx.tokens[ctx.right]; if (token.isWrapped && matchEpiodes(ctx, token.text)) { ctx.right -= 1; return true; } return false; } const SuffixEpisodeRE = [ /\s*- (?<type>SP|OVA)?(?<ep1>\d+)(?:\.(?<sub>\d))?(?:[vV](?<version>\d+))?(?<ep_type>\s+[^\-]*)?(?:\s*-)?$/, /\s+S(?<season>\d+)E(?<ep1>\d+)$/, /\s*第(?<ep1>\d+)(?:\.(?<sub>\d))?[集话話]$/, /\s+S(?<season1>\d+)-S(?<season2>\d+)$/ ]; const SuffixepisodesRangeRE = /- (?<from>\d+)-(?<to>\d+)(?:\s+(?<type>.+))?$/; const Types = /* @__PURE__ */ new Set([ "GEKIJOUBAN", "MOVIE", "OAD", "OAV", "ONA", "OVA", "SPECIAL", "SPECIALS", "TV", "\u5F00\u64AD\u7EAA\u5FF5\u7279\u522B\u7BC7", "\u958B\u64AD\u7D00\u5FF5\u7279\u5225\u7BC7", "\u5F00\u7BC7\u7EAA\u5FF5\u7279\u522B\u7BC7", "\u958B\u7BC7\u7D00\u5FF5\u7279\u5225\u7BC7", "\u7279\u522B\u7BC7", "\u7279\u5225\u7BC7", "\u7279\u5225\u7DE8", "\u7279\u522B\u8BDD", "\u7279\u5225\u8BDD", "\u7279\u5225\u8A71", "\u756A\u5916\u7BC7", "\u756A\u5916\u7DE8", "\u5267\u573A\u7248", "\u5287\u5834\u7248", "\u603B\u96C6\u7BC7", "\u7E3D\u96C6\u7BC7", // "\u5E7F\u64AD\u5267", "\u6717\u8BFB\u5267", // "SP", // "ED", "ENDING", "NCED", "NCOP", "OP", "OPENING", "PREVIEW", "PV", "\u7279\u522B\u7BC7PV", // "\u5408\u96C6", "\u4FEE\u6B63\u5408\u96C6" ]); function parseSuffixTextInlineEpisodes(ctx, text) { if (ctx.hasEpisode) { return text; } text = text.trimEnd(); { const res = SuffixepisodesRangeRE.exec(text); if (res) { const from = res.groups?.from ? +res.groups.from : NaN; const to = res.groups?.to ? +res.groups.to : NaN; if (!Number.isNaN(from) && !Number.isNaN(to)) { ctx.update2("episodesRange", "from", from); ctx.update2("episodesRange", "to", to); const type = res.groups?.type?.trim(); if (type) { const version = /[vV](\d+)$/.exec(type); if (version) { const versionNumber = +version[1]; if (!Number.isNaN(versionNumber)) { ctx.update("version", versionNumber); const typeWithoutVersion = type.slice(0, type.length - version[0].length).trim(); if (typeWithoutVersion) { ctx.update2("episodesRange", "type", typeWithoutVersion); } } else { ctx.update2("episodesRange", "type", type); } } else { ctx.update2("episodesRange", "type", type); } } return text.slice(0, text.length - res[0].length).trimEnd(); } } } for (const RE of SuffixEpisodeRE) { const res = RE.exec(text); if (res) { const ep = res.groups?.ep1 ? +res.groups?.ep1 : NaN; if (!Number.isNaN(ep)) { if (res.groups?.type) { ctx.update("type", res.groups.type); } ctx.update2("episode", "number", ep); const numberSub = res.groups?.sub ? +res.groups?.sub : NaN; if (numberSub && !Number.isNaN(numberSub)) { ctx.update2("episode", "numberSub", numberSub); } const version = res.groups?.version ? +res.groups.version : NaN; if (version && !Number.isNaN(version)) { ctx.update("version", version); } const season = res.groups?.season ? +res.groups.season : NaN; if (!Number.isNaN(season)) { ctx.update2("season", "number", season); } return text.slice(0, text.length - res[0].length).trimEnd(); } { const season1 = res.groups?.season1 ? +res.groups.season1 : NaN; const season2 = res.groups?.season2 ? +res.groups.season2 : NaN; if (!Number.isNaN(season1) && !Number.isNaN(season2) && season1 < season2) { ctx.update2("seasonsRange", "from", season1); ctx.update2("seasonsRange", "to", season2); return text.slice(0, text.length - res[0].length).trimEnd(); } } } } for (const type of Types) { const toMatch = ` - ${type}`; if (text.endsWith(toMatch)) { ctx.update("type", type); return text.slice(0, text.length - toMatch.length).trimEnd(); } } return text; } function parseSuffixEpisodes(ctx) { if (ctx.hasEpisode) { return true; } const token = ctx.tokens[ctx.right]; const text = token.text.trimEnd(); if (token.isWrapped) { return parseWrappedEpisodes(ctx); } else { const trimmed = parseSuffixTextInlineEpisodes(ctx, text); if (trimmed !== text) { ctx.tokens[ctx.right] = new Token(trimmed, token.left, token.right); return true; } } return false; } const SuffixSeasonOrEpisodesRes = [ [ /Parts? (\d+)$/, (res, ctx) => { const part = +res[1]; if (!Number.isNaN(part)) { ctx.update2("part", "number", part); return true; } return false; } ], [ /第\s*(\d+)\s*部分$/, (res, ctx) => { const part = +res[1]; if (!Number.isNaN(part)) { ctx.update2("part", "number", part); return true; } return false; } ], [ /(?:S|Season\s?)(\d+)$/, (res, ctx) => { const season = +res[1]; if (!Number.isNaN(season)) { if (!ctx.result.season?.number || ctx.result.season.number === season) { ctx.update2("season", "number", season); return true; } } return false; } ], [ /(1st|2nd|3rd|[456789]th) Season$/, (res, ctx) => { const season = Number.parseInt(res[1]); if (!Number.isNaN(season)) { if (!ctx.result.season?.number || ctx.result.season.number === season) { ctx.update2("season", "number", season); return true; } } return false; } ], [ /(?:-\s+)(Third) Season$/, (res, ctx) => { const season = { Third: 3 }[res[1]]; if (season && !Number.isNaN(season)) { if (!ctx.result.season?.number || ctx.result.season.number === season) { ctx.update2("season", "number", season); return true; } } return false; } ], [ /第?(\d+)[季期]$/, (res, ctx) => { const season = +res[1]; if (!Number.isNaN(season)) { if (!ctx.result.season?.number || ctx.result.season.number === season) { ctx.update2("season", "number", season); return true; } } return false; } ], [ /第?((?:[零一二三四五六七八九]十)?[零一二三四五六七八九])[季期]$/, (res, ctx) => { const text = res[1]; const map = { \u96F6: 0, \u4E00: 1, \u4E8C: 2, \u4E09: 3, \u56DB: 4, \u4E94: 5, \u516D: 6, \u4E03: 7, \u516B: 8, \u4E5D: 9 }; const base = text.length === 2 && text[0] === "\u5341" ? 1 : text.length === 3 ? map[text[0]] : 0; const offset = map[text[text.length - 1]]; const season = base * 10 + offset; if (!ctx.result.season?.number || ctx.result.season.number === season) { ctx.update2("season", "number", season); return true; } return false; } ], [ /(?:Vol|vol|Volume|volume)\.?\s*(?<vol>\d+)$/, (res, ctx) => { const vol = res.groups?.vol ? +res.groups.vol : NaN; if (!Number.isNaN(vol)) { ctx.update2("volume", "number", vol); return true; } return false; } ], [ /\s+[vV](\d+)$/, (res, ctx) => { const version = +res[1]; if (!Number.isNaN(version)) { ctx.update("version", version); return true; } return false; } ], [ /\s+(?<ep1>\d+)$|\((?<ep2>\d+)\)$/, (res, ctx) => { const text = res.groups?.ep1 ?? res.groups?.ep2; const season = text !== void 0 ? +text : NaN; if (!Number.isNaN(season) && ctx.hasEpisode) { if (!ctx.result.season?.number || ctx.result.season.number === season) { ctx.update2("season", "number", season); return true; } else if ((!ctx.result.year || ctx.result.year === season) && 1949 <= season && season <= 2099) { ctx.update("year", season); return true; } } return false; } ] ]; function parseSuffixTextInlineSeason(ctx, text) { let changed = false; do { changed = false; text = text.trimEnd(); for (const [re, fn] of SuffixSeasonOrEpisodesRes) { const res = re.exec(text); if (res && fn(res, ctx)) { changed = true; text = text.slice(0, text.length - res[0].length).trimEnd(); } } } while (changed); return text; } const AudioChannels = /* @__PURE__ */ new Set(["2.0CH", "2CH", "5.1", "5.1CH"]); const AudioCompoundTerms = /* @__PURE__ */ new Map([ ["DTS5.1", { codec: "DTS", channels: "5.1" }], ["TRUEHD5.1", { codec: "TRUEHD", channels: "5.1" }], ["AACX2", { codec: "AAC", trackCount: 2 }], ["AAC\xD72", { codec: "AAC", trackCount: 2 }], ["AACX3", { codec: "AAC", trackCount: 3 }], ["AAC\xD73", { codec: "AAC", trackCount: 3 }], ["AACX4", { codec: "AAC", trackCount: 4 }], ["AAC\xD74", { codec: "AAC", trackCount: 4 }], ["FLACX2", { codec: "FLAC", trackCount: 2 }], ["FLAC\xD72", { codec: "FLAC", trackCount: 2 }], ["FLACX3", { codec: "FLAC", trackCount: 3 }], ["FLAC\xD73", { codec: "FLAC", trackCount: 3 }], ["FLACX4", { codec: "FLAC", trackCount: 4 }], ["FLAC\xD74", { codec: "FLAC", trackCount: 4 }] ]); const AudioCodecs = /* @__PURE__ */ new Set([ "DTS", "DTS-ES", "EAC3&AAC", "AAC", "QAAC", "AC3", "EAC3", "E-AC-3", "FLAC", "FLAC/AC3", // TODO: split "LOSSLESS", "MP3", "WAV", "OGG", "VORBIS", "OPUS" ]); const AudioLanguages = /* @__PURE__ */ new Set(["DUALAUDIO", "DUAL AUDIO"]); const VideoBitDepths = /* @__PURE__ */ new Set(["8BIT", "8-BIT", "10BIT", "10BITS", "10-BIT", "10-BITS"]); const VideoCodecs = /* @__PURE__ */ new Set([ "HI10", "HI10P", "HI444", "HI444P", "HI444PP", "H264", "H265", "H.264", "H.265", "X264", "X265", "X.264", "AVC", "HEVC", "HEVC2", "DIVX", "DIVX5", "DIVX6", "XVID" ]); const VideoFormats = /* @__PURE__ */ new Set(["AVI", "RMVB", "WMV", "WMV3", "WMV9"]); const VideoQualities = /* @__PURE__ */ new Set(["HDR", "HQ", "LQ"]); const VideoResolutionTerms = /* @__PURE__ */ new Set(["HD", "SD"]); const VideoCompoundTerms = /* @__PURE__ */ new Map([ ["AVC-8BIT", { codec: "AVC", bitDepth: "8BIT" }], ["HEVC_OPUS", { codec: "HEVC", audioCodec: "OPUS" }], ["HEVC-10BIT", { codec: "HEVC", bitDepth: "10BIT" }], ["HEVC-10BIT-1440P", { codec: "HEVC", bitDepth: "10BIT", resolution: "1440P" }], ["HEVC-10BIT-2160P", { codec: "HEVC", bitDepth: "10BIT", resolution: "2160P" }], ["HEVC_10BIT", { codec: "HEVC", bitDepth: "10BIT" }], ["HEVC-8BIT", { codec: "HEVC", bitDepth: "8BIT" }], ["HEVC_8BIT", { codec: "HEVC", bitDepth: "8BIT" }] ]); const VideoFrameRates = /* @__PURE__ */ new Set(["23.976FPS", "24FPS", "29.97FPS", "30FPS", "60FPS", "120FPS"]); const VideoResolutions = /* @__PURE__ */ new Set(["2K", "4K"]); const Source = /* @__PURE__ */ new Set([ "BD", "BDRIP", "BLURAY", "BLU-RAY", "BDREMUX", "UHDBDRIP", "DVD", "DVD5", "DVD9", "DVD-R2J", "DVDRIP", "DVD-RIP", "R2DVD", "R2J", "R2JDVD", "R2JDVDRIP", "HDTV", "HDTVRIP", "TVRIP", "TV-RIP", "WEB", "WEBCAST", "WEBDL", "WEB-DL", "WEBRIP", "WEB-RIP", "WEB-MKV", "MASTERRIP", "DISC1", // TODO: volume [雪飄工作室][アイカツプラネット!/Aikatsu_Planet!/偶像活動星球][BDrip][Disc1](檢索:偶活/愛活) "DISC2", "DISC3", "DISC4", "DISC5", "DISC6", "DISC7", "DISC8", "DISC9" ]); const Platfroms = /* @__PURE__ */ new Set([ "Baha", "Bili", "Bilibili", "BiliBili", "B-Global", "ABEMA", "CR", "AT-X", "AT-X\u7248", "ViuTV", "AMZN", "ADN", "Sentai", "Netflix", "NF" ]); const Variants = /* @__PURE__ */ new Set([ "\u65E5\u914D\u7248", "\u4E2D\u914D\u7248", "\u65E5\u6587\u914D\u97F3", "\u4E2D\u6587\u914D\u97F3", "Chinese Audio", "Japanese Audio", "JPN Audio", "Japanese Dub", "JP Dub", "English Audio", "English Dub" ]); const SubtitleFormatTerms = /* @__PURE__ */ new Map([ ["ASS", "ASS"], ["ASSX2", "ASSx2"], ["ASSX3", "ASSx3"], ["ASSX4", "ASSx4"], ["HARDSUB", "HARDSUB"], ["HARDSUBS", "HARDSUB"], ["SOFTSUB", "SOFTSUB"], ["SOFTSUBS", "SOFTSUB"], ["SUB", "SUB"], ["SUBBED", "SUB"], ["SUBTITLED", "SUB"], ["SRT", "SRT"], ["SRTX2", "SRTx2"], ["SRTX3", "SRTx3"], ["SRTX4", "SRTx4"] ]); const SubtitleEncodingTerms = /* @__PURE__ */ new Map([ ["GB&BIG5", { encodings: ["GB", "BIG5"] }], ["BIG5&GB", { encodings: ["BIG5", "GB"] }], ["\u5916\u6302GB/BIG5", { format: "\u5916\u6302", encodings: ["GB", "BIG5"] }], ["GB/BIG5", { encodings: ["GB", "BIG5"] }], ["GB", { encoding: "GB" }], ["BIG5", { encoding: "BIG5" }] ]); const PlatformLanguageTerms = /* @__PURE__ */ new Map([ ["ViuTV\u7CB5\u8A9E", ["ViuTV", "\u7CB5\u8A9E"]], ["TVB\u7CB5\u8A9E", ["TVB", "\u7CB5\u8A9E"]] ]); const LanguageSubtitleFormatTerms = /* @__PURE__ */ new Map([ ["\u4EE3\u7406\u5546\u7CB5\u8A9E", { language: "\u7CB5\u8A9E" }], ["\u7CB5\u65E5\u96D9\u8A9E+\u5167\u5C01\u7E41\u9AD4\u4E2D\u6587\u5B57\u5E55", { language: "\u7E41\u9AD4\u4E2D\u6587", format: "\u5167\u5C01\u5B57\u5E55" }], ["\u7CB5\u8A9E+\u7121\u5C0D\u767D\u5B57\u5E55", { language: "\u7CB5\u8A9E+\u7121\u5C0D\u767D" }] ]); const SubtitleLanguageTerms = /* @__PURE__ */ new Map([ ["CN", "CN"], ["CHS", "CHS"], ["CHT", "CHT"], ["YUE", "YUE"], ["JPN", "JPN"], ["JP", "JP"], ["\u7B80\u4F53", "\u7B80\u4F53"], ["\u7B80/\u7E41\xB7\u65E5", "\u7B80/\u7E41\xB7\u65E5"], ["\u7E41/\u9AD4", "\u7E41/\u9AD4"], ["\u7B80\u7E41", "\u7B80\u7E41"], ["\u56FD\u8BED\u4E2D\u5B57", "\u56FD\u8BED\u4E2D\u5B57"], ["\u7E41\u9AD4", "\u7E41\u9AD4"], ["\u4E2D\u65E5\u53CC\u8BED", "\u4E2D\u65E5\u53CC\u8BED"], ["\u7E41\u65E5\u53CC\u8BED", "\u7E41\u65E5\u53CC\u8BED"], ["\u7B80\u65E5\u53CC\u8BED", "\u7B80\u65E5\u53CC\u8BED"], ["\u7E41\u65E5\u96D9\u8A9E", "\u7E41\u65E5\u96D9\u8A9E"], ["HOY\u7CB5\u8A9E", "HOY\u7CB5\u8A9E"], ["\u5916\u6302CHS/CHT", "CHS/CHT"], ["\u5916\u6302\u7E41\u7B80\u65E5\u5B57\u5E55", "\u7E41\u7B80\u65E5"] ]); const SubtitleLanguagePrefixes = [ "\u7B80\u7E41\u65E5\u53CC\u8BED", "\u7B80\u7E41\u65E5\u8BED", "\u7B80\u7E41\u82F1\u65E5", "\u7B80\u7E41\u65E5\u82F1", "\u7B80\u7E41\u65E5", "\u7B80\u65E5\u53CC\u8BED", "\u7B80/\u7E41", "\u7B80\u7E41\u82F1", "\u7B80\u7E41\u6CF0", "\u7E41\u7B80\u65E5", "\u4E2D\u65E5\u82F1", "\u7B80\u65E5", "\u7B80\u7E41", "\u7C21\u7E41", "\u7B80\u82F1", "\u7E41\u9AD4", "\u7E41\u4F53", "\u7E41\u65E5", "\u7E41\u82F1", "\u82F1\u6587", "\u7B80\u4F53", "\u7B80", "\u7E41", "\u82F1" ]; const SubtitleFormatSuffixTerms = /* @__PURE__ */ new Map([ ["\u5185\u5D4C\u5B57\u5E55", "\u5185\u5D4C\u5B57\u5E55"], ["\u5185\u5D4C", "\u5185\u5D4C"], ["\u5167\u5D4C", "\u5167\u5D4C"], ["\u5185\u5C01\u5B57\u5E55", "\u5185\u5C01\u5B57\u5E55"], ["\u5185\u5C01", "\u5185\u5C01"], ["\u5167\u5C01", "\u5167\u5C01"], ["\u5916\u6302\u5B57\u5E55", "\u5916\u6302\u5B57\u5E55"], ["\u5916\u6302", "\u5916\u6302"], ["\u5916\u639B", "\u5916\u639B"], ["\u5185\u6302", "\u5185\u6302"], ["\u5B57\u5E55", void 0] ]); const OtherTags = /* @__PURE__ */ new Set([ // "RAW", "DUB", "DUBBED", "retake", "SNS", // "\u5168\u6B4C\u66F2\u7279\u6548", "\u65E0\u6C34\u5370", "\u542B\u526F\u97F3\u8F68", "\u7279\u5178", "LIVE\u7EAF\u4EAB", "\u65E0\u635F\u91CD\u5236", "\u5E7F\u64AD\u5267_Dream\u2606Arch", // TODO: no hardcode // "\u56FD\u6F2B", "Donghua", // "\u7279\u5225\u7248", "\u5148\u884C\u7248", "\u5148\u884C\u7248\u672C", "\u6B63\u7247\u5148\u884C\u7248", "\u6B63\u5F0F\u7248", "\u6B63\u5F0F\u7248\u672C", "\u653E\u9001\u7248", "\u4FEE\u8BA2\u7248", "\u4FEE\u8A02\u7248", "On-air version", // "\u5E74\u9F61\u9650\u5236\u7248", // "Ani-One", // "\u50C5\u9650\u6E2F\u6FB3\u53F0", "\u50C5\u9650\u6E2F\u6FB3\u53F0\u5730\u5340", "\u50C5\u9650\u6E2F\u6FB3\u81FA\u5730\u5340", "\u4EC5\u9650\u6E2F\u6FB3\u53F0", "\u4EC5\u9650\u6E2F\u6FB3\u53F0\u5730\u533A", // "\u91CD\u64AD", // "End", "END", "TV + Movie Fin", "FIN", "Fin" ]); const SearchPrefix = [ "\u68C0\u7D22:", "\u68C0\u7D22\uFF1A", "\u6AA2\u7D22:", "\u6AA2\u7D22\uFF1A", "\u68C0\u7D22\u7528:", "\u68C0\u7D22\u7528\uFF1A", "\u6AA2\u7D22\u7528:", "\u6AA2\u7D22\u7528\uFF1A" ]; const HiringPrefix = ["\u62DB\u52DF", "\u6025\u52DF", "\u5B57\u5E55\u793E\u62DB\u4EBA", "\u5B57\u5E55\u793E\u62DB\u4EBA"]; const OtherPrefix = ["\u25B6"]; const Ignores = /* @__PURE__ */ new Set(["\u52A1\u5FC5\u67E5\u770Bbt\u7AD9\u7B80\u4ECB", "\u8BF7\u770Bbt\u7AD9\u7B80\u4ECB", "\u6DFB\u52A0\u65E5\u8BED", "\u6DFB\u52A0\u65E5\u8A9E"]); function matchSingleTag(ctx, text) { text = text.trim(); const upper = text.toUpperCase(); { const info = AudioCompoundTerms.get(upper); if (info) { if (info.codec) { ctx.update3("file", "audio", "codec", info.codec); } if (info.channels) { ctx.update3("file", "audio", "channels", info.channels); } if (info.trackCount) { ctx.update3("file", "audio", "trackCount", info.trackCount); } return true; } } if (AudioChannels.has(upper)) { ctx.update3("file", "audio", "channels", text); return true; } if (AudioCodecs.has(upper)) { ctx.update3("file", "audio", "codec", text); return true; } if (AudioLanguages.has(upper)) { ctx.update3("file", "audio", "language", text); return true; } { const info = VideoCompoundTerms.get(upper); if (info) { if (info.codec) { ctx.update3("file", "video", "codec", info.codec); } if (info.bitDepth) { ctx.update3("file", "video", "bitDepth", info.bitDepth); } if (info.resolution) { ctx.update3("file", "video", "resolution", info.resolution); } if (info.audioCodec) { ctx.update3("file", "audio", "codec", info.audioCodec); } return true; } } if (VideoCodecs.has(upper)) { ctx.update3("file", "video", "codec", text); return true; } if (VideoBitDepths.has(upper)) { ctx.update3("file", "video", "bitDepth", text); return true; } if (VideoFormats.has(upper)) { ctx.update3("file", "video", "format", text); return true; } if (VideoQualities.has(upper)) { ctx.update3("file", "video", "quality", text); return true; } if (VideoResolutionTerms.has(upper)) { ctx.update3("file", "video", "resolution", text); return true; } if (VideoFrameRates.has(upper)) { ctx.update3("file", "video", "fps", text); return true; } { const res = /^(AI)(\d{3,4}[Pp])$/i.exec(text); if (res) { ctx.update3("file", "video", "enhancement", res[1]); ctx.update3("file", "video", "resolution", res[2]); return true; } } { const res = /^(\d{3,5}(?:[Pp]|[Xx×]\d{3,5}))@(\d+(?:\.\d+)?FPS)$/i.exec(text); if (res) { ctx.update3("file", "video", "resolution", res[1]); ctx.update3("file", "video", "fps", res[2]); return true; } } { const res = /^(\d{3,4}P)(高帧率)$/i.exec(text); if (res) { ctx.update3("file", "video", "resolution", res[1]); ctx.update3("file", "video", "frameRateMode", res[2]); return true; } } if (/^\d{3,4}P$/i.test(text) || /^\d{3,5}[Xx×]\d{3,5}$/.test(text)) { ctx.update3("file", "video", "resolution", text); return true; } if (VideoResolutions.has(upper)) { ctx.update3("file", "video", "resolution", text); return true; } if (Source.has(upper)) { ctx.update("source", text); return true; } if (Platfroms.has(text)) { ctx.update("platform", text); return true; } if (Types.has(upper)) { ctx.update("type", text); return true; } if (Variants.has(text)) { const variants = [...ctx.result.variants ?? [], text]; ctx.update("variants", variants); return true; } if (FileExtensions.has(upper)) { ctx.update2("file", "extension", text); return true; } if (OtherTags.has(text)) { ctx.tags.push(text.trim()); return true; } if (text.endsWith(".ver")) { ctx.tags.push(text.trim()); return true; } if (text.startsWith("Bloomy_Cafe")) { ctx.tags.push(text.trim()); return true; } if (Ignores.has(text)) { return true; } { const res = /^tmdbid=(.+)$/.exec(text); if (res) { ctx.update("tmdbId", res[1]); return true; } } { const res = /^.+(物语|物語)$/.exec(text); if (res) { ctx.tags.push(text); return true; } } { const appendSubtitleLanguage = (language2) => { const languages = [...ctx.result.subtitle?.languages ?? [], language2]; ctx.update2("subtitle", "languages", languages); }; const updateSubtitleFormat = (format2, overwrite = false) => { if (format2 && (overwrite || !ctx.result.subtitle?.format)) { ctx.update2("subtitle", "format", format2); } }; const updateSubtitleEncoding = (encoding) => { if (encoding && !ctx.result.subtitle?.encoding && !ctx.result.subtitle?.encodings) { ctx.update2("subtitle", "encoding", encoding); } }; const updateSubtitleEncodings = (encodings) => { if (!encodings) { return; } const values = [...ctx.result.subtitle?.encodings ?? [], ...encodings]; ctx.update2("subtitle", "encodings", values); if (ctx.result.subtitle?.encoding) { delete ctx.result.subtitle.encoding; } }; const language = SubtitleLanguageTerms.get(upper) ?? SubtitleLanguageTerms.get(text); if (language) { appendSubtitleLanguage(language); return true; } const format = SubtitleFormatTerms.get(upper); if (format) { updateSubtitleFormat(format); return true; } const encodingInfo = SubtitleEncodingTerms.get(upper) ?? SubtitleEncodingTerms.get(text); if (encodingInfo) { updateSubtitleFormat(encodingInfo.format, true); updateSubtitleEncoding(encodingInfo.encoding); updateSubtitleEncodings(encodingInfo.encodings); return true; } const languageWithFormat = LanguageSubtitleFormatTerms.get(text); if (languageWithFormat) { appendSubtitleLanguage(languageWithFormat.language); updateSubtitleFormat(languageWithFormat.format); return true; } const platformLanguage = PlatformLanguageTerms.get(text); if (platformLanguage) { const [platform, language2] = platformLanguage; ctx.update("platform", platform); appendSubtitleLanguage(language2); return true; } for (const prefix of SubtitleLanguagePrefixes) { if (text.startsWith(prefix)) { const suffix = text.slice(prefix.length); if (suffix === "" || SubtitleFormatSuffixTerms.has(suffix)) { appendSubtitleLanguage(prefix); updateSubtitleFormat(SubtitleFormatSuffixTerms.get(suffix)); return true; } } } } { { const match = /^(\d\d\d\d)年(\d\d?)月新?番$/.exec(text); if (match) { const year = +match[1]; const month = +match[2]; if (1949 <= year && year <= 2099) { ctx.update("year", year); } if (1 <= month && month <= 12) { ctx.update("month", month); } return true; } } { const match = /^★?(\d{1,2})月新?番★?$/.exec(text); if (match) { const month = +match[1]; if (1 <= month && month <= 12) { ctx.update("month", month); } return true; } } { const match = /^(\d\d\d\d)\.(\d?\d)\.(\d?\d)$/.exec(text); if (match) { const year = +match[1]; const month = +match[2]; if (1949 <= year && year <= 2099) { ctx.update("year", year); } if (1 <= month && month <= 12) { ctx.update("month", month); } return true; } } { const match = /^(\d\d\d\d)(SP)$/.exec(text); if (match) { const year = +match[1]; const type = match[2]; if (1949 <= year && year <= 2099) { ctx.update("year", year); } ctx.update("type", type); return true; } } { const match = /^[vV](\d+)$/.exec(text); if (match) { const version = +match[1]; ctx.update("version", version); return true; } } } { for (const prefix of SearchPrefix) { if (text.startsWith(prefix)) { const title = text.slice(prefix.length).trim(); ctx.update( "search", title.split("/").map((t) => t.trim()).filter(Boolean) ); return true; } } for (const prefix of HiringPrefix) { if (text.startsWith(prefix)) { return true; } } for (const prefix of OtherPrefix) { if (text.startsWith(prefix)) { ctx.tags.push(text.trim()); return true; } } } { const res = /^(?:Vol|vol|Volume|volume)\.?\s*(?<vol>\d+)$/.exec(text); if (res) { const vol = res.groups?.vol ? +res.groups.vol : NaN; if (!Number.isNaN(vol)) { ctx.update2("volume", "number", vol); return true; } } } return false; } function matchMultipleTags(ctx, text, TagSeperators = [" ", "_", "&", "+"]) { for (const sep of TagSeperators) { const parts = text.split(sep); if (parts.length <= 1) continue; let matched = true; for (let i = 0; i < parts.length; i += 1) { const part = parts[i]; if (!matchSingleTag(ctx, part)) { matched = false; } } if (matched) { return true; } } return false; } function parseWrappedTag(ctx, token) { if (token.isWrapped) { const text = token.text; if (matchSingleTag(ctx, text)) { return true; } if (matchEpiodes(ctx, text)) { return true; } if (matchMultipleTags(ctx, text)) { return true; } } return false; } function parsePrefixWrappedTags(ctx) { while (ctx.left < ctx.right) { if (parseWrappedTag(ctx, ctx.tokens[ctx.left])) { ctx.left += 1; } else if (ctx.tokens[ctx.left].trim().toString() === "") { ctx.left += 1; } else { break; } } } function parseSuffixWrappedTags(ctx) { while (ctx.left < ctx.right) { if (parseWrappedTag(ctx, ctx.tokens[ctx.right])) { ctx.right -= 1; } else if (ctx.tokens[ctx.right].trim().toString() === "") { ctx.right -= 1; } else { const isIgnoreLast = () => { const token = ctx.tokens[ctx.tokens.length - 1]; if (SearchPrefix.some((prefix) => token.text.startsWith(prefix))) { return true; } if (HiringPrefix.some((prefix) => token.text.startsWith(prefix))) { return true; } if (Ignores.has(token.text)) { return true; } return false; }; if ( // xxx [未知标签] ctx.left + 2 < ctx.right && ctx.right === ctx.tokens.length - 1 || // xxx [未知标签](检索用) ctx.right === ctx.tokens.length - 2 && isIgnoreLast() ) { ctx.tags.push(ctx.tokens[ctx.right].text.trim()); ctx.right -= 1; } else { break; } } } } function parsePrefixTextTags(ctx) { if (ctx.left > ctx.right) return false; const token = ctx.tokens[ctx.left]; const trimmed = parsePrefixTextInlineTags(ctx, token.text); if (trimmed !== token.text) { if (trimmed) { ctx.tokens[ctx.left] = new Token(trimmed, token.left, token.right); } else { ctx.left += 1; } return true; } return false; } function parsePrefixTextInlineTags(ctx, text) { text = text.trim(); { const match = /^★?(\d\d?)月新?番★?/.exec(text); if (match) { const matched = match[0]; const month = +match[1]; ctx.update("month", month); text = text.slice(matched.length); } } { const match = /^★?(剧场版|劇場版)★?/.exec(text); if (match) { const matched = match[0]; const type = match[1]; ctx.update("type", type); text = text.slice(matched.length); } } { const match = /^★?(老番)★?/.exec(text); if (match) { const matched = match[0]; text = text.slice(matched.length); } } return text; } function parseSuffixTextInlineTags(ctx, text) { let changed = 0; const tokens = tokenize(text); while (tokens.length > 1) { const token = tokens[tokens.length - 1].trim(); if (parseWrappedTag(ctx, token)) { changed += 1; tokens.pop(); } else { break; } } if (changed) { return tokens.map((t) => t.toString()).join(""); } return text; } function splitMultipleTitles(ctx, options) { const { space = true, separators = ["/", "-"] } = options ?? {}; const rest = ctx.tokens.slice(ctx.left, ctx.right + 1); if (rest.length === 0) return []; if (rest.length > 1 && rest.every((t) => t.isWrapped)) { return rest.map((r) => r.text.trim()); } const fullText = rest.length === 1 ? rest[0].text.trim() : rest.map((t) => t.toString()).join("").trim(); if (!fullText) return []; for (const separator of separators) { const parts = space ? fullText.split(` ${separator} `) : fullText.split(separator); const result = []; if (fullText.startsWith(separator) && parts.length > 0) { parts[0] += separator; } if (fullText.endsWith(separator) && parts.length > 0) { parts[parts.length - 1] += separator; } for (let i = 0; i < parts.length; i += 1) { if (!space && separator === "/" && i + 1 < parts.length) { if ( // Fate/Grand Order 命运/冠位指定 parts[i].toUpperCase().endsWith("FATE") || parts[i].toUpperCase().endsWith("\u547D\u8FD0") || parts[i].toUpperCase().endsWith("\u547D\u904B") || // Ramma 1/2 22/7 /\d$/.test(parts[i]) && /^\d/.test(parts[i + 1]) ) { result.push(parts[i] + separator + parts[i + 1]); i += 1; continue; } } result.push(parts[i]); } if (result.length > 1) { return result.map((t) => t.trim()); } } return [fullText.trim()]; } function parseSingleTitleText(ctx, text) { text = parseSuffixTextInlineTags(ctx, text).trimEnd(); text = parseSuffixTextInlineSeason(ctx, text).trimEnd(); text = parseSuffixTextInlineTags(ctx, text).trimEnd(); return text; } function parseMultipleTitles(ctx, options) { const titles = splitMultipleTitles(ctx, options); if (titles.length === 0) return []; const trimmedTitles = titles.map((t) => parseSingleTitleText(ctx, t)).filter(Boolean); const trimmedTitle = trimmedTitles[0]; ctx.update("title", trimmedTitle); const otherTitles = [...new Set(trimmedTitles)].filter((t) => t !== trimmedTitle); if (otherTitles.length > 0) { ctx.update("titles", otherTitles); } return [trimmedTitle, ...otherTitles]; } const FansubTags = /* @__PURE__ */ new Set(["\u500B\u4EBA\u88FD\u4F5C\u5408\u96C6", "\u4EE3\u53D1", "\u7F8A\u5708\u4E2A\u4EBA\u8BD1\u5236"]); var Fansub = /* @__PURE__ */ ((Fansub2) => { Fansub2["Kirara_Fantasia"] = "Kirara Fantasia"; Fansub2["ANi"] = "ANi"; Fansub2["LoliHouse"] = "LoliHouse"; Fansub2["\u7EFF\u8336\u5B57\u5E55\u7EC4"] = "\u7EFF\u8336\u5B57\u5E55\u7EC4"; Fansub2["\u685C\u90FD\u5B57\u5E55\u7EC4"] = "\u685C\u90FD\u5B57\u5E55\u7EC4"; Fansub2["Prejudice_Studio"] = "Prejudice-Studio"; Fansub2["\u6CB8\u73ED\u4E9A\u9A6C\u5236\u4F5C\u7EC4"] = "\u6CB8\u73ED\u4E9A\u9A6C\u5236\u4F5C\u7EC4"; Fansub2["\u55B5\u840C\u5976\u8336\u5C4B"] = "\u55B5\u840C\u5976\u8336\u5C4B"; Fansub2["\u730E\u6237\u53D1\u5E03\u7EC4"] = "\u730E\u6237\u53D1\u5E03\u7EC4"; Fansub2["\u7231\u604B\u5B57\u5E55\u793E"] = "\u7231\u604B\u5B57\u5E55\u793E"; Fansub2["\u62E8\u96EA\u5BFB\u6625"] = "\u62E8\u96EA\u5BFB\u6625"; Fansub2["\u96EA\u98C4\u5DE5\u4F5C\u5BA4"] = "\u96EA\u98C4\u5DE5\u4F5C\u5BA4(FLsnow)"; Fansub2["\u5E7B\u6A31\u5B57\u5E55\u7EC4"] = "\u5E7B\u6A31\u5B57\u5E55\u7EC4"; Fansub2["GMTeam"] = "GMTeam"; Fansub2["\u4E09\u660E\u6CBB\u6446\u70C2\u7EC4"] = "\u4E09\u660E\u6CBB\u6446\u70C2\u7EC4"; Fansub2["\u661F\u7A7A\u5B57\u5E55\u7EC4"] = "\u661F\u7A7A\u5B57\u5E55\u7EC4"; Fansub2["\u5317\u5B87\u6CBB\u5B57\u5E55\u7EC4"] = "\u5317\u5B87\u6CBB\u5B57\u5E55\u7EC4"; Fansub2["\u6781\u5F71\u5B57\u5E55\u793E"] = "\u6781\u5F71\u5B57\u5E55\u793E"; Fansub2["MingYSub"] = "MingYSub"; Fansub2["\u9ED1