UNPKG

anipar

Version:

Anime title Parser built for AnimeGarden.

972 lines (964 loc) 23.2 kB
'use strict'; class Context { // Left cursor for consumed tokens left = 0; // Right cursor for consumed tokens right; tokens; options; result; tags; constructor(tokens, options) { this.tokens = tokens; this.options = options; this.right = tokens.length - 1; this.tags = []; this.result = {}; } 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.episodeRange; } validate() { this.result.tags = this.tags; return this.result; } } 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"], ["{", "}"] ]); const RevWrappers = 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.trim())); cur = ""; } left = char; right = Wrappers.get(char); } else { cur += char; } } else if (left && right && char === right) { tokens.push(new Token(cur.trim(), left, right)); cur = ""; left = void 0; right = void 0; } else { cur += char; } cursor += 1; } if (cur) { tokens.push(new Token(cur.trim())); } return tokens.filter((t) => t.text); } const WrappedEpisodeRE = /^(?<ep1>\d+)(?:\.(\d))?(?:[vV](\d+))?$|^第(?<ep2>\d+)[集话話]$/; const WrappedMovieRE = /^Movie [vV](\d+)$/; const EpisodesRange1 = /^(\d+)-(\d+)(?:\s*.*)$/; const EpisodesRange2 = /^全(\d+)集$/; function matchEpiodes(ctx, text) { { const res = WrappedEpisodeRE.exec(text); if (res) { const epText = res.groups?.ep1 || res.groups?.ep2; 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[2] && !Number.isNaN(res[2])) { ctx.update2("episode", "numberSub", +res[2]); } if (res[3] && !Number.isNaN(res[3])) { ctx.update("version", +res[3]); } 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 = EpisodesRange1.exec(text); if (res) { const from = +res[1]; const to = +res[2]; if (!Number.isNaN(from) && !Number.isNaN(to)) { ctx.update2("episodeRange", "from", from); ctx.update2("episodeRange", "to", to); return true; } } } { const res = EpisodesRange2.exec(text); if (res) { const to = +res[1]; if (!Number.isNaN(to)) { ctx.update2("episodeRange", "from", 1); ctx.update2("episodeRange", "to", to); return true; } } } return false; } const SuffixEpisodeRE = [/- (\d+)(?:\.(\d))?$/, /第(\d+)(?:\.(\d))?[集话話]$/]; function parseSuffixEpisodes(ctx) { if (ctx.result.episode || ctx.result.episodes || ctx.result.episodeRange) { return true; } const token = ctx.tokens[ctx.right]; const text = token.text; for (const RE of SuffixEpisodeRE) { const res = RE.exec(text); if (res) { const ep = +res[1]; if (!Number.isNaN(ep)) { ctx.update2("episode", "number", ep); if (res[2] && !Number.isNaN(res[2])) { ctx.update2("episode", "numberSub", +res[2]); } ctx.tokens[ctx.right] = token.slice(0, text.length - res[0].length).trim(); return true; } } } } const SuffixSeasonOrEpisodesRes = [ [ /(?: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; } ], [ /第?(\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; } ], [ / (\d+)$/, (res, ctx) => { const season = +res[1]; if (!Number.isNaN(season) && (ctx.result.episode || ctx.result.episodes || ctx.result.episodeRange)) { if (/Parts? \d+$/.test(res.input)) { return false; } if (!ctx.result.season?.number || ctx.result.season.number === season) { ctx.update2("season", "number", season); return true; } } return false; } ], [ /(?<ep1>\d+)(?:\.(\d))?(?:[vV](\d+))?$|^第(?<ep2>\d+)[集话話]$/, (res, ctx) => { const ep = +(res.groups?.ep1 || res.groups?.ep2); if (!Number.isNaN(ep)) { if (/Parts? \d+$/.test(res.input)) { return false; } ctx.update2("episode", "number", ep); if (res[2] && !Number.isNaN(res[2])) { ctx.update2("episode", "numberSub", +res[2]); } if (res[3] && !Number.isNaN(res[3])) { ctx.update("version", +res[3]); } return true; } return false; } ], [ /(日配版)$/, (res, ctx) => { ctx.update("language", res[0]); return true; } ], [ /(重播)$/, (res, ctx) => { ctx.tags.push(res[0]); return true; } ] ]; const AudioTerm = /* @__PURE__ */ new Set([ // Audio channels "2.0CH", "2CH", "5.1", "5.1CH", "DTS", "DTS-ES", "DTS5.1", "TRUEHD5.1", // Audio codec "AAC", "AACX2", "AAC\xD72", "AACX3", "AAC\xD73", "AACX4", "AAC\xD74", "AC3", "EAC3", "E-AC-3", "FLAC", "FLACX2", "FLAC\xD72", "FLACX3", "FLAC\xD73", "FLACX4", "FLAC\xD74", "LOSSLESS", "MP3", "WAV", "OGG", "VORBIS", // Audio language "DUALAUDIO", "DUAL AUDIO" ]); const VideoTerm = /* @__PURE__ */ new Set([ // Frame rate "23.976FPS", "24FPS", "29.97FPS", "30FPS", "60FPS", "120FPS", // Video codec "8BIT", "8-BIT", "10BIT", "10BITS", "10-BIT", "10-BITS", "HI10", "HI10P", "HI444", "HI444P", "HI444PP", "H264", "H265", "H.264", "H.265", "X264", "X265", "X.264", "AVC", "HEVC", "HEVC2", "HEVC-10BIT", "HEVC_OPUS", "DIVX", "DIVX5", "DIVX6", "XVID", // Video format "AVI", "RMVB", "WMV", "WMV3", "WMV9", // Video quality "HDR", "HQ", "LQ", // Video resolution "HD", "SD" ]); const VideoResolution = /* @__PURE__ */ new Set([ "480P", "720P", "1080P", "2160P", "AI2160p", "1280X720", "1280\xD7720", "1920X816", "1920\xD7816", "1920X1080", "1920\xD71080", "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" ]); const Platfrom = /* @__PURE__ */ new Set(["Baha", "Bilibili", "B-Global", "ABEMA", "CR", "ViuTV", "AMZN", "ADN"]); const Type = /* @__PURE__ */ new Set([ "GEKIJOUBAN", "MOVIE", "OAD", "OAV", "ONA", "OVA", "SPECIAL", "SPECIALS", "TV", "\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", // "SP", // "ED", "ENDING", "NCED", "NCOP", "OP", "OPENING", "PREVIEW", "PV" ]); const Languages = /* @__PURE__ */ new Set([ "CN", "CHS", "CHT", "YUE", "JP", "\u7B80\u4F53", "\u56FD\u8BED\u4E2D\u5B57", "\u7E41\u9AD4", "\u4E2D\u65E5\u53CC\u8BED", "\u7B80\u65E5\u53CC\u8BED", "\u7E41\u65E5\u96D9\u8A9E", "HOY\u7CB5\u8A9E" ]); const Subtitles = /* @__PURE__ */ new Set([ "ASS", "GB", "BIG5", "DUB", "DUBBED", "HARDSUB", "HARDSUBS", "RAW", "SOFTSUB", "SOFTSUBS", "SUB", "SUBBED", "SUBTITLED", "SRT" ]); const LanguagePrefixes = [ "\u7B80\u4F53", "\u7E41\u9AD4", "\u7B80\u65E5", "\u7E41\u65E5", "\u7B80\u7E41\u65E5\u53CC\u8BED", "\u7B80\u7E41\u65E5", "\u7B80\u7E41", "\u7E41\u7B80\u65E5", "\u7B80\u7E41\u65E5\u8BED", "\u7B80\u65E5\u53CC\u8BED" ]; const SubtitlesSufixes = /* @__PURE__ */ new Set(["\u5185\u5D4C", "\u5167\u5D4C", "\u5185\u5C01", "\u5185\u5C01\u5B57\u5E55", "\u5916\u6302", "\u5916\u639B"]); const PlatformLanguage = /* @__PURE__ */ new Map([["ViuTV\u7CB5\u8A9E", ["ViuTV", "\u7CB5\u8A9E"]]]); const LanguageSubtitles = /* @__PURE__ */ new Map([ ["\u7B80\u4F53\u5B57\u5E55", ["\u7B80\u4F53", void 0]], ["\u7E41\u9AD4\u5B57\u5E55", ["\u7E41\u9AD4", void 0]], ["\u7B80\u65E5\u53CC\u8BED\u5B57\u5E55", ["\u7B80\u65E5\u53CC\u8BED", void 0]], ["TVB\u7CB5\u8A9E", ["\u7CB5\u8A9E", void 0]], ["\u4EE3\u7406\u5546\u7CB5\u8A9E", ["\u7CB5\u8A9E", void 0]], ["\u7CB5\u65E5\u96D9\u8A9E+\u5167\u5C01\u7E41\u9AD4\u4E2D\u6587\u5B57\u5E55", ["\u7E41\u9AD4\u4E2D\u6587", "\u5167\u5C01\u5B57\u5E55"]], ["\u7CB5\u8A9E+\u7121\u5C0D\u767D\u5B57\u5E55", [void 0, "\u7121\u5C0D\u767D\u5B57\u5E55"]] ]); const Extension = /* @__PURE__ */ new Set([ "3GP", "AVI", "DIVX", "FLV", "M2TS", "MKV", "MOV", "MP4", "MPG", "OGM", "RM", "RMVB", "TS", "WEBM", "WMV" ]); const Tags = /* @__PURE__ */ new Set(["\u56FD\u6F2B", "\u5148\u884C\u7248", "\u5148\u884C\u7248\u672C", "\u6B63\u5F0F\u7248", "\u6B63\u5F0F\u7248\u672C", "Ani-One"]); const SearchPrefix = ["\u68C0\u7D22\uFF1A", "\u68C0\u7D22\u7528\uFF1A"]; const HiringPrefix = ["\u62DB\u52DF", "\u5B57\u5E55\u793E\u62DB\u4EBA"]; const OtherPrefix = ["\u25B6"]; function matchSingleTag(ctx, text) { const upper = text.toUpperCase(); if (AudioTerm.has(upper)) { ctx.update3("file", "audio", "term", text); return true; } if (VideoTerm.has(upper)) { ctx.update3("file", "video", "term", text); return true; } if (VideoResolution.has(upper)) { ctx.update3("file", "video", "resolution", text); return true; } if (Source.has(upper)) { ctx.update("source", text); return true; } if (Platfrom.has(text)) { ctx.update("platform", text); return true; } if (Type.has(upper)) { ctx.update("type", text); return true; } if (Extension.has(upper)) { ctx.update2("file", "extension", text); return true; } if (Tags.has(text)) { ctx.tags.push(text); return true; } { if (Languages.has(upper)) { ctx.update("language", text); return true; } if (Subtitles.has(upper)) { ctx.update("subtitles", text); return true; } const combined = LanguageSubtitles.get(text); if (combined) { ctx.update("language", combined[0]); ctx.update("subtitles", combined[1]); return true; } const combined2 = PlatformLanguage.get(text); if (combined2) { ctx.update("platform", combined2[0]); ctx.update("language", combined2[1]); return true; } for (const prefix of LanguagePrefixes) { if (text.startsWith(prefix)) { const language = prefix; const subtitles = text.slice(prefix.length); if (SubtitlesSufixes.has(subtitles)) { ctx.update("language", language); ctx.update("subtitles", subtitles); 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\d?)月新?番★?$/.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.tags.push(title); 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); return true; } } } return false; } const TagSeperators = [" ", "_"]; function matchMultipleTags(ctx, text) { 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 parseTag(ctx, cursor) { const token = ctx.tokens[cursor]; 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 parseRightTags(ctx) { while (ctx.left < ctx.right) { if (parseTag(ctx, ctx.right)) { ctx.right -= 1; } else { if (ctx.left + 2 < ctx.right && ctx.right >= ctx.tokens.length - 1) { ctx.tags.push(ctx.tokens[ctx.right].text); ctx.right -= 1; } else { break; } } } { const token = ctx.tokens[ctx.right]; const text = token.text; const sepearators = [" ", "\u2605"]; for (const sep of sepearators) { const parts = text.split(sep); if (parts.length > 1) { let changed = 0; while (parts.length > 1) { const part = parts[parts.length - 1]; if (/^\d+$/.test(part)) { break; } if (matchSingleTag(ctx, part) || matchEpiodes(ctx, part) || matchMultipleTags(ctx, part)) { changed++; parts.pop(); } else { break; } } if (changed > 1) { const trimmed = parts.join(sep); ctx.tokens[ctx.right] = new Token(trimmed, token.left, token.right); } if (changed > 0) { break; } } } } } function parseLeftTags(ctx) { while (ctx.left < ctx.right) { if (parseTag(ctx, ctx.left)) { ctx.left += 1; } else { break; } } { const token = ctx.tokens[ctx.left]; const text = token.text; const match = /^★?(\d\d?)月新?番★?/.exec(text); if (match) { const matched = match[0]; const month = +match[1]; ctx.update("month", month); ctx.tokens[ctx.left] = token.slice(matched.length); if (ctx.tokens[ctx.left].text.length === 0) { ctx.left += 1; } } } { const token = ctx.tokens[ctx.left]; const text = token.text; const match = /^★?(剧场版|劇場版)★?/.exec(text); if (match) { const matched = match[0]; const type = match[1]; ctx.update("type", type); ctx.tokens[ctx.left] = token.slice(matched.length); if (ctx.tokens[ctx.left].text.length === 0) { ctx.left += 1; } } } } function parseSuffixSeasonOrEpisodes(ctx, text) { while (true) { let found = false; if (RevWrappers.has(text[text.length - 1])) { const leftWrapper = RevWrappers.get(text[text.length - 1]); const leftIdx = text.lastIndexOf(leftWrapper); if (leftIdx !== -1) { const maybe = text.slice(leftIdx + 1, text.length - 1); if (maybe.length > 0) { if (matchSingleTag(ctx, maybe) || !ctx.hasEpisode && matchEpiodes(ctx, maybe)) { text = text.slice(0, text.length - maybe.length - 2).trimEnd(); found = true; } } } } for (const [re, fn] of SuffixSeasonOrEpisodesRes) { const res = re.exec(text); if (res && fn(res, ctx)) { text = text.slice(0, text.length - res[0].length).trimEnd(); found = true; break; } } if (!found) break; } return text; } function parseFansub(ctx) { if (ctx.left + 1 > ctx.right) { return false; } const token = ctx.tokens[ctx.left]; if (token.isWrapped) { const text = token.text; let found = false; const seps = ["&", "\xB7"]; for (const sep of seps) { const [name, ...collab] = text.split(sep); if (collab.length > 0) { if (collab.length === 1 && collab[0].endsWith("\u58D3\u5236")) { break; } found = true; ctx.update2("fansub", "name", name); ctx.update2("fansub", "collab", collab); break; } } if (!found) { ctx.update2("fansub", "name", text); } ctx.left += 1; return true; } return false; } function parseTitle(ctx) { parseSuffixEpisodes(ctx); const rest = ctx.tokens.slice(ctx.left, ctx.right + 1); if (rest.length === 0) return false; let found = false; const matchParts = (parts) => { const [title, ...other] = parts; const trimmedTitle = parseSuffixSeasonOrEpisodes(ctx, title); const trimmedOther = other.map((t) => parseSuffixSeasonOrEpisodes(ctx, t)).map((t) => t.trim()).filter((t) => !!t && t !== trimmedTitle); ctx.update("title", trimmedTitle); if (trimmedOther.length > 0) { ctx.update("titles", trimmedOther); } }; if (rest.length > 1 && rest.every((t) => t.isWrapped)) { matchParts(rest.map((t) => t.text)); found = true; } if (!found) { const text = rest.length === 1 ? rest[0].text : rest.map((t) => t.toString()).join(""); const separators = ["/", "\\"]; for (const sep of separators) { const parts = splitText(text, sep).map((t) => t.trim()).filter((t) => !!t); if (parts.length > 1) { matchParts(parts); found = true; break; } } if (!found && ctx.options.fansub === "ANi") { const parts = splitOnce(text, " - ").map((t) => t.trim()).filter((t) => !!t); if (parts.length > 1) { matchParts(parts); found = true; } } if (!found) { const trimmed = parseSuffixSeasonOrEpisodes(ctx, text); ctx.update("title", trimmed); } } return true; } function splitText(text, sep) { const parts = text.split(sep); const ans = []; for (let i = 0; i < parts.length; i++) { if (i === 0) { ans.push(parts[i]); } else { if (sep === "/" && /\d$/.test(parts[i - 1]) && /^\d/.test(parts[i])) { ans.pop(); ans.push(parts[i - 1] + "/" + parts[i]); } else { ans.push(parts[i]); } } } return ans; } function splitOnce(text, separator) { const found = text.indexOf(separator); if (found === -1) { return [text, ""]; } const first = text.slice(0, found); const second = text.slice(found + separator.length); return [first, second]; } function parse(title, options = {}) { const tokens = tokenize(title); if (tokens.length === 0) return void 0; const context = new Context(tokens, options); parseRightTags(context); parseLeftTags(context); parseFansub(context); parseLeftTags(context); if (!parseTitle(context)) { return void 0; } const result = context.validate(); return result ? postprocess(result) : void 0; } function postprocess(result) { if (result.fansub?.name === "ANi") { if (result.title && result.titles && result.titles.length === 1) { const t1 = result.title; const t2 = result.titles[0]; result.title = t2; result.titles = [t1]; } } return result; } exports.parse = parse; //# sourceMappingURL=index.cjs.map