anipar
Version:
Anime title Parser built for AnimeGarden.
1,743 lines (1,735 loc) • 56.6 kB
JavaScript
'use strict';
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";