avatarr_parser/
normalize.rs1use crate::episode::regexes;
8
9pub fn validate_before_parsing(title: &str) -> bool {
11 if title.is_empty() {
12 return false;
13 }
14
15 let lower = title.to_ascii_lowercase();
16 if lower.contains("password") && lower.contains("yenc") {
17 return false;
18 }
19
20 if !title.chars().any(|c| c.is_alphanumeric()) {
21 return false;
22 }
23
24 let without_ext = remove_file_extension(title);
25
26 for re in regexes::REJECT_HASHED_REGEXES.iter() {
27 if re.is_match(&without_ext) {
28 return false;
29 }
30 }
31
32 for re in regexes::SEASON_FOLDER_REGEXES.iter() {
33 if re.is_match(&without_ext) {
34 return false;
35 }
36 }
37
38 true
39}
40
41pub fn remove_file_extension(title: &str) -> String {
43 const MEDIA_EXTENSIONS: &[&str] = &[
44 ".3g2", ".3gp", ".3gp2", ".asf", ".avi", ".divx", ".flv", ".m4v", ".mk3d", ".mka", ".mkv",
45 ".mov", ".mp4", ".mpa", ".mpeg", ".mpg", ".ogg", ".ogm", ".ogv", ".qt", ".rm", ".rmvb",
46 ".tp", ".ts", ".vob", ".w64", ".wav", ".webm", ".wmv", ".srt", ".sub", ".idx", ".ssa",
47 ".ass", ".nfo", ".txt", ".par2", ".nzb",
48 ];
49
50 if let Some(m) = regexes::FILE_EXTENSION_REGEX.find(title) {
51 let ext = &title[m.start()..];
52 if MEDIA_EXTENSIONS.iter().any(|e| e.eq_ignore_ascii_case(ext)) {
53 return title[..m.start()].to_string();
54 }
55 }
56 title.to_string()
57}
58
59pub fn preprocess_title(title: &str) -> Option<String> {
61 if !validate_before_parsing(title) {
62 return None;
63 }
64
65 let mut release_title = if regexes::REVERSED_TITLE_REGEX.is_match(title) {
66 let without_ext = remove_file_extension(title);
67 without_ext.chars().rev().collect::<String>()
68 } else {
69 remove_file_extension(title)
70 };
71
72 if release_title.starts_with("@\"") {
74 release_title = release_title.strip_prefix("@\"").unwrap().to_string();
75 if release_title.ends_with('"') {
76 release_title.pop();
77 }
78 }
79
80 release_title = release_title.replace('【', "[").replace('】', "]");
82
83 if let Some(caps) = regexes::PRE_SUB_SPANISH_REGEX.captures(&release_title)
85 && let (Some(t), Some(y), Some(info)) =
86 (caps.name("title"), caps.name("year"), caps.name("info"))
87 {
88 release_title = format!("{} ({}) - {} ", t.as_str(), y.as_str(), info.as_str());
89 }
90
91 let simple_title = regexes::SIMPLE_TITLE_REGEX
93 .replace_all(&release_title, "")
94 .to_string();
95
96 let simple_title = if let Some(m) = regexes::WEBSITE_PREFIX_REGEX.find(&simple_title) {
101 let matched = &simple_title[m.start()..m.end()];
102 let lower = matched.to_ascii_lowercase();
103 if lower.contains("naruto-kun.") {
104 simple_title
105 } else {
106 regexes::WEBSITE_PREFIX_REGEX
107 .replace(&simple_title, "")
108 .to_string()
109 }
110 } else {
111 simple_title
112 };
113 let simple_title = regexes::WEBSITE_POSTFIX_REGEX
114 .replace(&simple_title, "")
115 .to_string();
116
117 let simple_title = regexes::CLEAN_TORRENT_SUFFIX_REGEX
119 .replace(&simple_title, "")
120 .to_string();
121
122 let simple_title = simple_title.replace("[]", "").replace("()", "");
124 let simple_title = simple_title.trim().to_string();
125
126 let simple_title = if let Some(m) = regexes::CLEAN_QUALITY_BRACKETS_REGEX.find(&simple_title) {
128 let bracket_content = &simple_title[m.start()..];
129 let quality = crate::parse_quality_name(bracket_content);
130 if quality.quality != crate::Quality::Unknown {
131 simple_title[..m.start()].to_string()
132 } else {
133 simple_title
134 }
135 } else {
136 simple_title
137 };
138
139 let simple_title = expand_six_digit_airdate(&simple_title);
141
142 Some(simple_title)
143}
144
145fn expand_six_digit_airdate(title: &str) -> String {
146 if let Some(caps) = regexes::SIX_DIGIT_AIR_DATE_REGEX.captures(title)
147 && let (Some(prefix), Some(year), Some(month), Some(day), Some(suffix)) = (
148 caps.name("prefix"),
149 caps.name("airyear"),
150 caps.name("airmonth"),
151 caps.name("airday"),
152 caps.name("suffix"),
153 )
154 {
155 let full_year = format!("20{}", year.as_str());
156 let replacement = format!(
157 "{}{}.{}.{}{}",
158 prefix.as_str(),
159 full_year,
160 month.as_str(),
161 day.as_str(),
162 suffix.as_str()
163 );
164 return format!(
165 "{}{}{}",
166 &title[..caps.get(0).unwrap().start()],
167 replacement,
168 &title[caps.get(0).unwrap().end()..]
169 );
170 }
171 title.to_string()
172}
173
174pub fn digit_boundary_ok(input: &str, start: usize, end: usize) -> bool {
178 let bytes = input.as_bytes();
179 let no_before = start == 0 || !bytes[start - 1].is_ascii_digit();
180 let no_after = end >= bytes.len() || !bytes[end].is_ascii_digit();
181 no_before && no_after
182}
183
184pub fn clean_series_title(raw: &str) -> String {
187 let mut title = regexes::REQUEST_INFO_REGEX.replace(raw, "").to_string();
188 title = title.replace(['.', '_'], " ");
189 title = title.trim().trim_end_matches(['-', '/']).trim().to_string();
190 close_trailing_year_paren(&mut title);
191 title
192}
193
194fn close_trailing_year_paren(title: &mut String) {
197 let mut rchars = title.chars().rev();
198 let last4: Vec<char> = rchars.by_ref().take(4).collect();
199 if last4.len() == 4 && last4.iter().all(|c| c.is_ascii_digit()) && rchars.next() == Some('(') {
200 title.push(')');
201 }
202}