1use crate::error::ErrorMatch;
2use crate::pattern;
3use crate::pattern::Pattern;
4use regex::Captures;
5use std::borrow::Cow;
6use std::cmp::{max, min};
7
8use std::{convert::TryFrom, str::FromStr};
9
10#[derive(Clone, Debug, Default, Eq, PartialEq)]
11pub struct Metadata {
12 title: String,
13 season: Option<i32>,
14 episode: Option<i32>,
15 episodes: Vec<i32>,
16 year: Option<i32>,
17 resolution: Option<String>,
18 quality: Option<String>,
19 codec: Option<String>,
20 audio: Option<String>,
21 group: Option<String>,
22 country: Option<String>,
23 extended: bool,
24 hardcoded: bool,
25 proper: bool,
26 repack: bool,
27 widescreen: bool,
28 unrated: bool,
29 three_d: bool,
30 imdb: Option<String>,
31 extension: Option<String>,
32 language: Option<String>,
33}
34
35fn check_pattern_and_extract<'a>(
36 pattern: &Pattern,
37 torrent_name: &'a str,
38 title_start: &mut usize,
39 title_end: &mut usize,
40 extract_value: impl Fn(Captures<'a>) -> Option<&'a str>,
41) -> Option<&'a str> {
42 pattern.captures(torrent_name).and_then(|caps| {
43 if let Some(cap) = caps.get(0) {
44 if pattern.before_title() {
45 *title_start = max(*title_start, cap.end());
46 } else {
47 *title_end = min(*title_end, cap.start());
48 }
49 }
50 extract_value(caps)
51 })
52}
53
54fn check_pattern<'a>(
55 pattern: &Pattern,
56 torrent_name: &'a str,
57 title_start: &mut usize,
58 title_end: &mut usize,
59) -> Option<Captures<'a>> {
60 pattern.captures(torrent_name).map(|caps| {
61 if let Some(cap) = caps.get(0) {
62 if pattern.before_title() {
63 *title_start = max(*title_start, cap.end());
64 } else {
65 *title_end = min(*title_end, cap.start());
66 }
67 }
68 caps
69 })
70}
71
72fn capture_to_string(caps: Option<Captures<'_>>) -> Option<String> {
73 caps.and_then(|c| c.get(0)).map(|m| m.as_str().to_string())
74}
75
76impl Metadata {
77 pub fn from(name: &str) -> Result<Self, ErrorMatch> {
91 Metadata::from_str(name)
92 }
93
94 pub fn title(&self) -> &str {
95 &self.title
96 }
97 pub fn season(&self) -> Option<i32> {
98 self.season
99 }
100 pub fn episode(&self) -> Option<i32> {
101 self.episode
102 }
103 pub fn episodes(&self) -> &Vec<i32> {
110 &self.episodes
111 }
112 pub fn year(&self) -> Option<i32> {
113 self.year
114 }
115 pub fn resolution(&self) -> Option<&str> {
116 self.resolution.as_deref()
117 }
118 pub fn quality(&self) -> Option<&str> {
119 self.quality.as_deref()
120 }
121 pub fn codec(&self) -> Option<&str> {
122 self.codec.as_deref()
123 }
124 pub fn audio(&self) -> Option<&str> {
125 self.audio.as_deref()
126 }
127 pub fn group(&self) -> Option<&str> {
128 self.group.as_deref()
129 }
130 pub fn country(&self) -> Option<&str> {
131 self.country.as_deref()
132 }
133 pub fn imdb_tag(&self) -> Option<&str> {
134 self.imdb.as_deref()
135 }
136 pub fn extended(&self) -> bool {
137 self.extended
138 }
139 pub fn hardcoded(&self) -> bool {
140 self.hardcoded
141 }
142 pub fn proper(&self) -> bool {
143 self.proper
144 }
145 pub fn repack(&self) -> bool {
146 self.repack
147 }
148 pub fn widescreen(&self) -> bool {
149 self.widescreen
150 }
151 pub fn unrated(&self) -> bool {
152 self.unrated
153 }
154 pub fn three_d(&self) -> bool {
155 self.three_d
156 }
157 pub fn extension(&self) -> Option<&str> {
158 self.extension.as_deref()
159 }
160 pub fn is_show(&self) -> bool {
161 self.season.is_some()
162 }
163 pub fn is_special(&self) -> bool {
164 self.season.map(|s| s < 1).unwrap_or(false)
165 }
166 pub fn language(&self) -> Option<&str> {
167 self.language.as_deref()
168 }
169}
170
171impl FromStr for Metadata {
172 type Err = ErrorMatch;
173
174 fn from_str(name: &str) -> Result<Self, Self::Err> {
175 let mut title_start = 0;
176 let mut title_end = name.len();
177 let mut episodes: Vec<i32> = Vec::new();
178 let interim_last_episode;
179
180 let season = check_pattern_and_extract(
181 &pattern::SEASON,
182 name,
183 &mut title_start,
184 &mut title_end,
185 |caps| {
186 caps.name("short")
187 .or_else(|| caps.name("long"))
188 .or_else(|| caps.name("dash"))
189 .or_else(|| caps.name("collection"))
190 .map(|m| m.as_str())
191 },
192 );
193
194 let episode = check_pattern_and_extract(
195 &pattern::EPISODE,
196 name,
197 &mut title_start,
198 &mut title_end,
199 |caps| {
200 caps.name("short")
201 .or_else(|| caps.name("cross"))
202 .or_else(|| caps.name("dash"))
203 .map(|m| m.as_str())
204 },
205 );
206 if let Some(first_episode) = episode {
208 episodes.push(first_episode.parse().unwrap());
209 interim_last_episode = check_pattern_and_extract(
210 &pattern::LAST_EPISODE,
211 name,
212 &mut title_start,
213 &mut title_end,
214 |caps| caps.get(1).map(|m| m.as_str()),
215 );
216 if let Some(last_episode) = interim_last_episode {
217 if last_episode.len() == 1 && last_episode.contains('0') {
219 } else {
221 for number_of_episode in
223 first_episode.parse::<i32>().unwrap() + 1..=last_episode.parse().unwrap()
224 {
225 episodes.push(number_of_episode);
226 }
227 }
228 }
229 }
230 let year = check_pattern_and_extract(
231 &pattern::YEAR,
232 name,
233 &mut title_start,
234 &mut title_end,
235 |caps: Captures<'_>| caps.name("year").map(|m| m.as_str()),
236 );
237
238 let resolution = check_pattern_and_extract(
239 &pattern::RESOLUTION,
240 name,
241 &mut title_start,
242 &mut title_end,
243 |caps| caps.get(0).map(|m| m.as_str()),
244 )
245 .map(String::from);
246 let quality = check_pattern_and_extract(
247 &pattern::QUALITY,
248 name,
249 &mut title_start,
250 &mut title_end,
251 |caps| caps.get(0).map(|m| m.as_str()),
252 )
253 .map(String::from);
254 let codec = check_pattern_and_extract(
255 &pattern::CODEC,
256 name,
257 &mut title_start,
258 &mut title_end,
259 |caps| caps.get(0).map(|m| m.as_str()),
260 )
261 .map(String::from);
262 let audio = check_pattern_and_extract(
263 &pattern::AUDIO,
264 name,
265 &mut title_start,
266 &mut title_end,
267 |caps| caps.get(0).map(|m| m.as_str()),
268 )
269 .map(String::from);
270 let group = check_pattern_and_extract(
271 &pattern::GROUP,
272 name,
273 &mut title_start,
274 &mut title_end,
275 |caps| caps.get(2).map(|m| m.as_str()),
276 )
277 .map(String::from);
278 let imdb = check_pattern_and_extract(
279 &pattern::IMDB,
280 name,
281 &mut title_start,
282 &mut title_end,
283 |caps| caps.get(0).map(|m| m.as_str()),
284 )
285 .map(String::from);
286 let extension = check_pattern_and_extract(
287 &pattern::FILE_EXTENSION,
288 name,
289 &mut title_start,
290 &mut title_end,
291 |caps| caps.get(1).map(|m| m.as_str()),
292 )
293 .map(String::from);
294 let country = check_pattern_and_extract(
295 &pattern::COUNTRY,
296 name,
297 &mut title_start,
298 &mut title_end,
299 |caps| caps.name("country").map(|m| m.as_str()),
300 )
301 .map(String::from);
302 let language = check_pattern_and_extract(
303 &pattern::LANGUAGE,
304 name,
305 &mut title_start,
306 &mut title_end,
307 |caps| caps.get(0).map(|s| s.as_str()),
308 )
309 .map(String::from);
310
311 let extended = check_pattern(&pattern::EXTENDED, name, &mut title_start, &mut title_end);
312 let hardcoded = check_pattern(&pattern::HARDCODED, name, &mut title_start, &mut title_end);
313 let proper = check_pattern(&pattern::PROPER, name, &mut title_start, &mut title_end);
314 let repack = check_pattern(&pattern::REPACK, name, &mut title_start, &mut title_end);
315 let widescreen =
316 check_pattern(&pattern::WIDESCREEN, name, &mut title_start, &mut title_end);
317 let unrated = check_pattern(&pattern::UNRATED, name, &mut title_start, &mut title_end);
318 let three_d = check_pattern(&pattern::THREE_D, name, &mut title_start, &mut title_end);
319
320 let region = check_pattern(&pattern::REGION, name, &mut title_start, &mut title_end);
321 let container = check_pattern(&pattern::CONTAINER, name, &mut title_start, &mut title_end);
322 let garbage = check_pattern(&pattern::GARBAGE, name, &mut title_start, &mut title_end);
323 let website = check_pattern(&pattern::WEBSITE, name, &mut title_start, &mut title_end);
324
325 if title_start >= title_end {
326 return Err(ErrorMatch::new(vec![
327 ("season", season.map(String::from)),
328 ("episode", episode.map(String::from)),
329 ("year", year.map(String::from)),
330 ("extension", extension.map(String::from)),
331 ("resolution", resolution),
332 ("quality", quality),
333 ("codec", codec),
334 ("audio", audio),
335 ("group", group),
336 ("country", country),
337 ("imdb", imdb),
338 ("language", language),
339 ("extended", capture_to_string(extended)),
340 ("proper", capture_to_string(proper)),
341 ("repack", capture_to_string(repack)),
342 ("widescreen", capture_to_string(widescreen)),
343 ("unrated", capture_to_string(unrated)),
344 ("three_d", capture_to_string(three_d)),
345 ("region", capture_to_string(region)),
346 ("container", capture_to_string(container)),
347 ("garbage", capture_to_string(garbage)),
348 ("website", capture_to_string(website)),
349 ]));
350 }
351
352 let mut title = &name[title_start..title_end];
353 if let Some(pos) = title.find('(') {
354 title = title.split_at(pos).0;
355 }
356 title = title.trim_start_matches(" -");
357 title = title.trim_end_matches(" -");
358 let title = match !title.contains(' ') && title.contains('.') {
359 true => Cow::Owned(title.replace('.', " ")),
360 false => Cow::Borrowed(title),
361 };
362 let title = title
363 .replace('_', " ")
364 .replacen('(', "", 1)
365 .replacen("- ", "", 1)
366 .trim()
367 .to_string();
368
369 Ok(Metadata {
370 title,
371 season: season.map(|s| s.parse().unwrap()),
372 episode: episode.map(|s| s.parse().unwrap()),
373 episodes,
374 year: year.map(|s| s.parse().unwrap()),
375 resolution,
376 quality,
377 codec,
378 audio,
379 group,
380 country,
381 extended: extended.is_some(),
382 hardcoded: hardcoded.is_some(),
383 proper: proper.is_some(),
384 repack: repack.is_some(),
385 widescreen: widescreen.is_some(),
386 unrated: unrated.is_some(),
387 three_d: three_d.is_some(),
388 imdb,
389 extension,
390 language,
391 })
392 }
393}
394
395impl TryFrom<&str> for Metadata {
396 type Error = ErrorMatch;
397
398 fn try_from(s: &str) -> Result<Self, Self::Error> {
399 Metadata::from_str(s)
400 }
401}