use std::sync::OnceLock;
use regex::Regex;
use crate::detail::container::{find_next_token, find_prev_token, mark};
use crate::detail::delimiter::is_space;
use crate::detail::keyword::KeywordKind;
use crate::detail::token::{
is_close_bracket_token, is_dash_token, is_delimiter_token, is_free_token,
is_not_delimiter_token, is_numeric_token, Token, TokenKind,
};
use crate::detail::util::{byte_to_char_offset, equal_ignore_ascii_case, to_int};
use crate::element::{Element, ElementKind};
fn add_element_from_token(tokens: &mut [Token], idx: usize, elements: &mut Vec<Element>) {
mark(tokens, idx, ElementKind::Episode);
if let Some(token) = tokens.get(idx) {
elements.push(Element {
kind: ElementKind::Episode,
value: token.value.clone(),
position: token.position,
});
}
}
fn add_element_with_value(
tokens: &mut [Token],
idx: usize,
value: String,
position: usize,
elements: &mut Vec<Element>,
) {
mark(tokens, idx, ElementKind::Episode);
elements.push(Element {
kind: ElementKind::Episode,
value,
position,
});
}
struct EpisodeTokenMatch {
season_s: Option<(String, usize)>,
season_x: Option<(String, usize)>,
episode: (String, usize),
version: Option<(String, usize)>,
}
fn episode_token_pattern() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
crate::detail::regex_util::compile(
r"^(?:S([0-9]{1,2})|([0-9]{1,2})x)?[E#]?([0-9]{1,4})(?:[vV]([0-9]))?$",
)
})
}
fn match_episode_token(value: &str) -> Option<EpisodeTokenMatch> {
let caps = episode_token_pattern().captures(value)?;
let group = |i: usize| {
caps.get(i).map(|m| {
(
m.as_str().to_string(),
byte_to_char_offset(value, m.start()),
)
})
};
let episode = group(3)?;
Some(EpisodeTokenMatch {
season_s: group(1),
season_x: group(2),
episode,
version: group(4),
})
}
fn is_episode_delimiter(token: &Token) -> bool {
is_delimiter_token(token) && matches!(token.value.chars().next(), Some('-' | '~' | '&' | '+'))
}
fn trailing_fraction(tokens: &[Token], idx: usize) -> Option<(usize, usize)> {
let dot_idx = idx + 1;
let five_idx = idx + 2;
let is_dot = tokens
.get(dot_idx)
.is_some_and(|t| is_delimiter_token(t) && t.value == ".");
let is_five = tokens
.get(five_idx)
.is_some_and(|t| is_free_token(t) && t.value == "5");
(is_dot && is_five).then_some((dot_idx, five_idx))
}
fn starts_with_episode_or_type_keyword(tokens: &[Token], idx: usize) -> bool {
let Some(keyword) = tokens.get(idx).and_then(|t| t.keyword) else {
return false;
};
match keyword.kind {
KeywordKind::Episode => true,
KeywordKind::Type | KeywordKind::EpisodeType => {
tokens.get(idx).is_some_and(|t| t.value != "Movie")
}
_ => false,
}
}
fn apply_episode_match(
tokens: &mut [Token],
idx: usize,
m: &EpisodeTokenMatch,
elements: &mut Vec<Element>,
) {
let Some(base_position) = tokens.get(idx).map(|t| t.position) else {
return;
};
if let Some((season, offset)) = &m.season_s {
elements.push(Element {
kind: ElementKind::Season,
value: season.clone(),
position: base_position + offset,
});
} else if let Some((season, offset)) = &m.season_x {
elements.push(Element {
kind: ElementKind::Season,
value: season.clone(),
position: base_position + offset,
});
}
let (ep_value, ep_offset) = &m.episode;
add_element_with_value(
tokens,
idx,
ep_value.clone(),
base_position + ep_offset,
elements,
);
if let Some((version, offset)) = &m.version {
elements.push(Element {
kind: ElementKind::ReleaseVersion,
value: version.clone(),
position: base_position + offset,
});
}
}
fn parse_episode_token_strategy(tokens: &mut [Token], elements: &mut Vec<Element>) {
let free_indices: Vec<usize> = tokens
.iter()
.enumerate()
.filter(|(_, t)| is_free_token(t))
.map(|(i, _)| i)
.collect();
for idx in free_indices {
if !tokens.get(idx).is_some_and(is_free_token) {
continue;
}
let Some(value) = tokens.get(idx).map(|t| t.value.clone()) else {
continue;
};
let Some(m1) = match_episode_token(&value) else {
continue;
};
let is_numeric = tokens.get(idx).is_some_and(is_numeric_token);
let x1 = !is_numeric;
let x2 = find_prev_token(tokens, idx, is_not_delimiter_token)
.is_some_and(|p| starts_with_episode_or_type_keyword(tokens, p));
let glued_to_title = tokens.get(idx).is_some_and(|t| t.value.len() == 1)
&& find_prev_token(tokens, idx, is_not_delimiter_token)
.and_then(|p| tokens.get(p))
.is_some_and(|t| {
is_free_token(t)
&& !t.is_enclosed
&& !is_numeric_token(t)
&& t.keyword.is_none()
});
let range_next = tokens
.get(idx + 1)
.filter(|t| is_episode_delimiter(t))
.and_then(|_| {
let after_idx = idx + 2;
let after_value = tokens.get(after_idx)?.value.clone();
let m2 = match_episode_token(&after_value)?;
if glued_to_title && after_value.len() == 1 {
return None;
}
(to_int(&m1.episode.0) < to_int(&m2.episode.0)).then_some((after_idx, m2))
});
let x3 = range_next.is_some();
if !(x1 || x2 || x3) {
continue;
}
let mut m1 = m1;
let fraction = trailing_fraction(tokens, idx);
let fraction_on_version = fraction.is_some() && m1.version.is_some();
if fraction.is_some() {
if let Some(version) = m1.version.as_mut() {
version.0.push_str(".5");
} else {
m1.episode.0.push_str(".5");
}
}
apply_episode_match(tokens, idx, &m1, elements);
if let Some((dot_idx, five_idx)) = fraction {
let kind = if fraction_on_version {
ElementKind::ReleaseVersion
} else {
ElementKind::Episode
};
mark(tokens, dot_idx, kind);
mark(tokens, five_idx, kind);
}
if let Some((after_idx, m2)) = range_next {
apply_episode_match(tokens, after_idx, &m2, elements);
}
}
}
fn parse_separated_episodes(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
let candidates: Vec<usize> = tokens
.iter()
.enumerate()
.filter(|(_, t)| is_free_token(t) && is_numeric_token(t))
.map(|(i, _)| i)
.collect();
for idx in candidates {
let search_start = idx + 1;
let Some(sep_idx) = tokens.get(search_start..).and_then(|s| {
s.iter()
.position(|t| matches!(t.value.as_str(), "&" | "~" | "of"))
.map(|i| search_start + i)
}) else {
continue;
};
if tokens
.get(search_start..sep_idx)
.is_some_and(|s| s.iter().any(is_not_delimiter_token))
{
continue;
}
let Some(after_idx) = find_next_token(tokens, sep_idx, is_not_delimiter_token) else {
continue;
};
if !tokens
.get(after_idx)
.is_some_and(|t| is_free_token(t) && is_numeric_token(t))
{
continue;
}
let is_of = tokens.get(sep_idx).is_some_and(|t| t.value == "of");
add_element_from_token(tokens, idx, elements);
if !is_of {
add_element_from_token(tokens, after_idx, elements);
}
return true;
}
false
}
fn parse_fractional_episode(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
let len = tokens.len();
if len < 3 {
return false;
}
for i in 0..=(len - 3) {
let is_number = tokens
.get(i)
.is_some_and(|t| is_free_token(t) && is_numeric_token(t));
let is_dot = tokens
.get(i + 1)
.is_some_and(|t| is_delimiter_token(t) && t.value == ".");
let is_fraction = tokens
.get(i + 2)
.is_some_and(|t| is_free_token(t) && t.value == "5");
if !(is_number && is_dot && is_fraction) {
continue;
}
let Some((number_value, position)) = tokens.get(i).map(|t| (t.value.clone(), t.position))
else {
continue;
};
add_element_with_value(tokens, i, format!("{number_value}.5"), position, elements);
mark(tokens, i + 1, ElementKind::Episode);
mark(tokens, i + 2, ElementKind::Episode);
return true;
}
false
}
fn japanese_episode_counter_pattern() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| crate::detail::regex_util::compile(r"^(?:第)?([0-9]{1,4})話$"))
}
fn parse_japanese_counter(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
for idx in 0..tokens.len() {
if !tokens.get(idx).is_some_and(is_free_token) {
continue;
}
let Some(value) = tokens.get(idx).map(|t| t.value.clone()) else {
continue;
};
let Some(caps) = japanese_episode_counter_pattern().captures(&value) else {
continue;
};
let Some(group1) = caps.get(1) else {
continue;
};
let offset = byte_to_char_offset(&value, group1.start());
let group1 = group1.as_str().to_string();
let position = tokens.get(idx).map_or(0, |t| t.position);
add_element_with_value(tokens, idx, group1, position + offset, elements);
return true;
}
false
}
fn parse_equivalent_number(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
for outer_idx in 0..tokens.len() {
if !tokens
.get(outer_idx)
.is_some_and(|t| is_free_token(t) && is_numeric_token(t))
{
continue;
}
let Some(open_idx) = find_next_token(tokens, outer_idx, is_not_delimiter_token) else {
continue;
};
if !tokens
.get(open_idx)
.is_some_and(|t| t.kind == TokenKind::OpenBracket)
{
continue;
}
let Some(inner_idx) = find_next_token(tokens, open_idx, is_not_delimiter_token) else {
continue;
};
if !tokens
.get(inner_idx)
.is_some_and(|t| is_free_token(t) && is_numeric_token(t))
{
continue;
}
let Some(close_idx) = find_next_token(tokens, inner_idx, is_not_delimiter_token) else {
continue;
};
if !tokens
.get(close_idx)
.is_some_and(|t| t.kind == TokenKind::CloseBracket)
{
continue;
}
add_element_from_token(tokens, outer_idx, elements);
add_element_from_token(tokens, inner_idx, elements);
return true;
}
false
}
fn parse_separated_number(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
let dash_indices: Vec<usize> = tokens
.iter()
.enumerate()
.filter(|(_, t)| is_dash_token(t))
.map(|(i, _)| i)
.collect();
for &dash_idx in dash_indices.iter().rev() {
let start = dash_idx + 1;
let Some(after_idx) = tokens
.get(start..)
.and_then(|s| s.iter().position(is_not_delimiter_token).map(|i| start + i))
else {
continue;
};
if tokens
.get(after_idx)
.is_some_and(|t| is_free_token(t) && is_numeric_token(t))
{
add_element_from_token(tokens, after_idx, elements);
return true;
}
}
false
}
fn parse_isolated_number(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
let len = tokens.len();
if len < 3 {
return false;
}
for i in 0..=(len - 3) {
let is_isolated = tokens
.get(i)
.is_some_and(|t| t.kind == TokenKind::OpenBracket)
&& tokens
.get(i + 2)
.is_some_and(|t| t.kind == TokenKind::CloseBracket);
if !is_isolated {
continue;
}
if tokens
.get(i + 1)
.is_some_and(|t| is_free_token(t) && is_numeric_token(t))
{
add_element_from_token(tokens, i + 1, elements);
return true;
}
}
false
}
fn is_partial_episode(value: &str) -> bool {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| crate::detail::regex_util::compile(r"^[0-9]{1,4}[ABCabc]$"))
.is_match(value)
}
fn parse_partial_episode(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
for idx in 0..tokens.len() {
let Some(value) = tokens
.get(idx)
.filter(|t| is_free_token(t))
.map(|t| t.value.clone())
else {
continue;
};
if !is_partial_episode(&value) {
continue;
}
if idx > 1 && value == "1a" && tokens.get(idx - 2).is_some_and(|t| t.value == "Ver1") {
continue;
}
add_element_from_token(tokens, idx, elements);
return true;
}
false
}
fn parse_first_number(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
let starts_with_episode_number = tokens
.first()
.is_some_and(|t| is_free_token(t) && is_numeric_token(t))
&& {
if tokens.len() <= 2
|| tokens.get(1).is_some_and(is_dash_token)
|| tokens.get(2).is_some_and(is_dash_token)
{
true
} else if tokens.get(1).is_some_and(|t| t.value == ".") {
let starts_with_space = tokens
.get(2)
.is_some_and(|t| t.value.chars().next().is_some_and(is_space));
let is_extension = tokens
.get(2)
.is_some_and(|t| t.element_kind == Some(ElementKind::FileExtension));
starts_with_space || is_extension
} else {
false
}
};
if starts_with_episode_number {
add_element_from_token(tokens, 0, elements);
return true;
}
false
}
fn is_version_number(tokens: &[Token], idx: usize) -> bool {
if !tokens.get(idx).is_some_and(is_numeric_token) || idx == 0 {
return false;
}
tokens
.get(idx - 1)
.is_some_and(|t| is_delimiter_token(t) && t.value == ".")
}
fn is_version_number_reversed(tokens: &[Token], idx: usize) -> bool {
if !tokens.get(idx).is_some_and(is_numeric_token) {
return false;
}
tokens
.get(idx + 1)
.is_some_and(|t| is_delimiter_token(t) && t.value == ".")
}
fn parse_last_number(tokens: &mut [Token], elements: &mut Vec<Element>) -> bool {
let candidates: Vec<usize> = tokens
.iter()
.enumerate()
.filter(|(_, t)| is_free_token(t) && is_numeric_token(t))
.map(|(i, _)| i)
.collect();
for &idx in candidates.iter().rev() {
if tokens.get(idx).is_some_and(|t| t.is_enclosed) {
continue;
}
if tokens.get(idx).is_some_and(|t| t.position == 0) {
continue;
}
let prev = find_prev_token(tokens, idx, is_not_delimiter_token);
let next = find_next_token(tokens, idx, is_not_delimiter_token);
if let Some(p) = prev {
let Some(value) = tokens.get(p).map(|t| t.value.clone()) else {
continue;
};
if equal_ignore_ascii_case(&value, "Cour") || equal_ignore_ascii_case(&value, "Part") {
continue;
}
if equal_ignore_ascii_case(&value, "Movie") || equal_ignore_ascii_case(&value, "No") {
continue;
}
if is_version_number_reversed(tokens, p) {
continue;
}
if tokens.get(p).is_some_and(is_close_bracket_token) && value == "]" {
continue;
}
}
if let Some(n) = next {
if is_version_number(tokens, n) {
continue;
}
}
if let (Some(p), Some(n)) = (prev, next) {
if tokens.get(p).is_some_and(is_free_token) && tokens.get(n).is_some_and(is_free_token)
{
continue;
}
}
add_element_from_token(tokens, idx, elements);
return true;
}
false
}
pub(super) fn parse_episode(tokens: &mut [Token]) -> Vec<Element> {
let mut elements = Vec::new();
parse_episode_token_strategy(tokens, &mut elements);
if !elements.is_empty() {
return elements;
}
if parse_separated_episodes(tokens, &mut elements) {
return elements;
}
if parse_fractional_episode(tokens, &mut elements) {
return elements;
}
if parse_japanese_counter(tokens, &mut elements) {
return elements;
}
if parse_equivalent_number(tokens, &mut elements) {
return elements;
}
if parse_separated_number(tokens, &mut elements) {
return elements;
}
if parse_isolated_number(tokens, &mut elements) {
return elements;
}
if parse_partial_episode(tokens, &mut elements) {
return elements;
}
if parse_first_number(tokens, &mut elements) {
return elements;
}
parse_last_number(tokens, &mut elements);
elements
}