torrent_name_parser/
metadata.rs

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    ///```
78    /// use torrent_name_parser::Metadata;
79    ///
80    /// if let Ok(m) = Metadata::from("Doctor.Who.(2003).S01E01.avi") {
81    ///   assert_eq!(m.title(), "Doctor Who");
82    ///   assert_eq!(m.season(), Some(1));
83    ///   assert_eq!(m.extension(), Some("avi"));
84    ///   assert_eq!(m.is_show(), true);
85    ///   // Season is not 0 (zero) meaning it is not a Season Special. Eg: Christmas Special
86    ///   assert_eq!(m.is_special(), false);
87
88    /// }
89    ///```
90    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    /// Contains a `Vec` of episode numbers detected.
104    /// # Examples:
105    /// No matches -> `[]`  
106    /// `E01` -> `[1]`  
107    /// `E03e04` -> `[3,4]`  
108    /// `e03E07` -> `[3,4,5,6,7]`  
109    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        // Only look for a last episode if pattern::EPISODE returned a value.
207        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                // Sanity check that last_episode does not contain a value or 0 (Zero)
218                if last_episode.len() == 1 && last_episode.contains('0') {
219                    // Treat a string ending with '0' (zero) as invalid and skip further work
220                } else {
221                    // Populate Vec with each episode number
222                    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}