anitomy_sys/
lib.rs

1//! # anitomy-sys
2//! *anitomy-sys* is a low-level Rust binding for [Anitomy](https://github.com/erengy/anitomy) a C++ library for parsing anime
3//! video filenames.
4//!
5//! Makes use of [anitomy-c](https://github.com/Xtansia/anitomy-c) a C ABI wrapper for Anitomy.
6//!
7//! ## Installation
8//! Add this to your `Cargo.toml`:
9//! ```toml
10//! [dependencies]
11//! anitomy-sys = "0.1"
12//! ```
13//!
14//! *anitomy-sys* will compile and statically link *anitomy-c* and *Anitomy* at build time, as such a compatible compiler is required.
15//!
16//! ### Requirements
17//! * A C++14 compatible compiler
18//!   - GCC >= 5
19//!   - Clang >= 3.4 (According to the [Clang CXX status page](https://clang.llvm.org/cxx_status.html))
20//!   - [Visual Studio 2017](https://www.visualstudio.com/downloads/)
21//!     OR [Build Tools for Visual Studio 2017](https://aka.ms/BuildTools)
22//!
23//! ## Example
24//! ```no_run
25//! extern crate anitomy_sys;
26//!
27//! use anitomy_sys::{Anitomy, ElementCategory};
28//! use std::ffi::CString;
29//!
30//! fn main() {
31//!     let mut anitomy = unsafe { Anitomy::new() };
32//!     let filename = CString::new("[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv").expect("no nul chars in filename");
33//!     let success = unsafe { anitomy.parse(&filename) };
34//!     println!("Success? {}", success);
35//!     unsafe {
36//!         let elements = anitomy.elements();
37//!         println!(
38//!             "It is: {} #{} by {}",
39//!             elements.get(ElementCategory::AnimeTitle),
40//!             elements.get(ElementCategory::EpisodeNumber),
41//!             elements.get(ElementCategory::ReleaseGroup)
42//!         );
43//!         (0..elements.count(None))
44//!             .flat_map(|i| elements.at(i))
45//!             .for_each(|e| println!("{:?}: {:?}", e.category, e.value));
46//!     }
47//!     unsafe { anitomy.destroy() };
48//! }
49//! ```
50
51pub mod ffi;
52
53use std::ffi::CStr;
54
55/// The options used by Anitomy to determine how to parse a filename.
56#[repr(C)]
57pub struct Options {
58    options: ffi::options_t,
59}
60
61impl Options {
62    /// Set the allowed delimiters.
63    pub unsafe fn allowed_delimiters<S: AsRef<CStr>>(&mut self, allowed_delimiters: S) {
64        ffi::options_allowed_delimiters(&mut self.options, allowed_delimiters.as_ref().as_ptr())
65    }
66
67    /// Set the strings to ignore.
68    pub unsafe fn ignored_strings<S: AsRef<CStr>>(&mut self, ignored_strings: &[S]) {
69        let array = ffi::string_array_new();
70        ignored_strings
71            .iter()
72            .for_each(|cstr| ffi::string_array_add(array, cstr.as_ref().as_ptr()));
73        ffi::options_ignored_strings(&mut self.options, array);
74        ffi::string_array_free(array);
75    }
76
77    /// Set whether to attempt to parse the episode number.
78    pub unsafe fn parse_episode_number(&mut self, parse_episode_number: bool) {
79        ffi::options_parse_episode_number(&mut self.options, parse_episode_number)
80    }
81
82    /// Set whether to attempt to parse the episode title.
83    pub unsafe fn parse_episode_title(&mut self, parse_episode_title: bool) {
84        ffi::options_parse_episode_title(&mut self.options, parse_episode_title)
85    }
86
87    /// Set whether to attempt to parse the file extension.
88    pub unsafe fn parse_file_extension(&mut self, parse_file_extension: bool) {
89        ffi::options_parse_file_extension(&mut self.options, parse_file_extension)
90    }
91
92    /// Set whether to attempt to parse the release group.
93    pub unsafe fn parse_release_group(&mut self, parse_release_group: bool) {
94        ffi::options_parse_release_group(&mut self.options, parse_release_group)
95    }
96}
97
98/// The category of an [`Element`](::Element).
99#[repr(i32)]
100#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
101pub enum ElementCategory {
102    AnimeSeason = ffi::kElementAnimeSeason,
103    AnimeSeasonPrefix = ffi::kElementAnimeSeasonPrefix,
104    AnimeTitle = ffi::kElementAnimeTitle,
105    AnimeType = ffi::kElementAnimeType,
106    AnimeYear = ffi::kElementAnimeYear,
107    AudioTerm = ffi::kElementAudioTerm,
108    DeviceCompatibility = ffi::kElementDeviceCompatibility,
109    EpisodeNumber = ffi::kElementEpisodeNumber,
110    EpisodeNumberAlt = ffi::kElementEpisodeNumberAlt,
111    EpisodePrefix = ffi::kElementEpisodePrefix,
112    EpisodeTitle = ffi::kElementEpisodeTitle,
113    FileChecksum = ffi::kElementFileChecksum,
114    FileExtension = ffi::kElementFileExtension,
115    FileName = ffi::kElementFileName,
116    Language = ffi::kElementLanguage,
117    Other = ffi::kElementOther,
118    ReleaseGroup = ffi::kElementReleaseGroup,
119    ReleaseInformation = ffi::kElementReleaseInformation,
120    ReleaseVersion = ffi::kElementReleaseVersion,
121    Source = ffi::kElementSource,
122    Subtitles = ffi::kElementSubtitles,
123    VideoResolution = ffi::kElementVideoResolution,
124    VideoTerm = ffi::kElementVideoTerm,
125    VolumeNumber = ffi::kElementVolumeNumber,
126    VolumePrefix = ffi::kElementVolumePrefix,
127    Unknown = ffi::kElementUnknown,
128}
129
130impl From<ffi::element_category_t> for ElementCategory {
131    fn from(val: ffi::element_category_t) -> ElementCategory {
132        match val {
133            ffi::kElementAnimeSeason => ElementCategory::AnimeSeason,
134            ffi::kElementAnimeSeasonPrefix => ElementCategory::AnimeSeasonPrefix,
135            ffi::kElementAnimeTitle => ElementCategory::AnimeTitle,
136            ffi::kElementAnimeType => ElementCategory::AnimeType,
137            ffi::kElementAnimeYear => ElementCategory::AnimeYear,
138            ffi::kElementAudioTerm => ElementCategory::AudioTerm,
139            ffi::kElementDeviceCompatibility => ElementCategory::DeviceCompatibility,
140            ffi::kElementEpisodeNumber => ElementCategory::EpisodeNumber,
141            ffi::kElementEpisodeNumberAlt => ElementCategory::EpisodeNumberAlt,
142            ffi::kElementEpisodePrefix => ElementCategory::EpisodePrefix,
143            ffi::kElementEpisodeTitle => ElementCategory::EpisodeTitle,
144            ffi::kElementFileChecksum => ElementCategory::FileChecksum,
145            ffi::kElementFileExtension => ElementCategory::FileExtension,
146            ffi::kElementFileName => ElementCategory::FileName,
147            ffi::kElementLanguage => ElementCategory::Language,
148            ffi::kElementOther => ElementCategory::Other,
149            ffi::kElementReleaseGroup => ElementCategory::ReleaseGroup,
150            ffi::kElementReleaseInformation => ElementCategory::ReleaseInformation,
151            ffi::kElementReleaseVersion => ElementCategory::ReleaseVersion,
152            ffi::kElementSource => ElementCategory::Source,
153            ffi::kElementSubtitles => ElementCategory::Subtitles,
154            ffi::kElementVideoResolution => ElementCategory::VideoResolution,
155            ffi::kElementVideoTerm => ElementCategory::VideoTerm,
156            ffi::kElementVolumeNumber => ElementCategory::VolumeNumber,
157            ffi::kElementVolumePrefix => ElementCategory::VolumePrefix,
158            _ => ElementCategory::Unknown,
159        }
160    }
161}
162
163/// An element parsed from a filename by Anitomy.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Element {
166    /// The category of the element.
167    pub category: ElementCategory,
168    /// The value of the element.
169    pub value: String,
170}
171
172/// The collection of elements parsed from a filename by Anitomy.
173#[repr(C)]
174pub struct Elements {
175    elements: ffi::elements_t,
176}
177
178impl Elements {
179    /// Determines whether there are no elements of a given category.
180    ///
181    /// Passing `None` will check for any elements at all.
182    pub unsafe fn empty<C: Into<Option<ElementCategory>>>(&self, category: C) -> bool {
183        match category.into() {
184            Some(cat) => {
185                ffi::elements_empty_category(&self.elements, cat as ffi::element_category_t)
186            }
187            None => ffi::elements_empty(&self.elements),
188        }
189    }
190
191    /// Counts the number of elements of a given category.
192    ///
193    /// Passing `None` will count all elements.
194    pub unsafe fn count<C: Into<Option<ElementCategory>>>(&self, category: C) -> usize {
195        match category.into() {
196            Some(cat) => {
197                ffi::elements_count_category(&self.elements, cat as ffi::element_category_t)
198            }
199            None => ffi::elements_count(&self.elements),
200        }
201    }
202
203    /// Get the element at the given position if one exists.
204    pub unsafe fn at(&self, pos: usize) -> Option<Element> {
205        if pos < self.count(None) {
206            let pair = ffi::elements_at(&self.elements, pos);
207            let value = ffi::raw_into_string(pair.value);
208            ffi::string_free(pair.value);
209            Some(Element {
210                category: ElementCategory::from(pair.category),
211                value: value,
212            })
213        } else {
214            None
215        }
216    }
217
218    /// Gets the first element of a category if one exists, otherwise returns an empty string.
219    pub unsafe fn get(&self, category: ElementCategory) -> String {
220        let rawval = ffi::elements_get(&self.elements, category as ffi::element_category_t);
221        let val = ffi::raw_into_string(rawval);
222        ffi::string_free(rawval);
223        val
224    }
225
226    /// Gets all elements of a category.
227    pub unsafe fn get_all(&self, category: ElementCategory) -> Vec<String> {
228        let rawvals = ffi::elements_get_all(&self.elements, category as ffi::element_category_t);
229        let size = ffi::string_array_size(rawvals);
230        let vals = (0..size)
231            .map(|i| ffi::raw_into_string(ffi::string_array_at(rawvals, i)))
232            .collect();
233        ffi::string_array_free(rawvals);
234        vals
235    }
236}
237
238/// An Anitomy parser instance.
239#[derive(Debug)]
240pub struct Anitomy {
241    anitomy: *mut ffi::anitomy_t,
242}
243
244impl Anitomy {
245    /// Construct a new Anitomy instance.
246    pub unsafe fn new() -> Self {
247        Self {
248            anitomy: ffi::anitomy_new(),
249        }
250    }
251
252    /// Parses a filename.
253    ///
254    /// `true` and `false` return values correspond to what Anitomy classifies as succeeding or failing in parsing a filename.
255    /// Such as an [`AnimeTitle`](::ElementCategory::AnimeTitle) element being found.
256    pub unsafe fn parse<S: AsRef<CStr>>(&mut self, filename: S) -> bool {
257        ffi::anitomy_parse(self.anitomy, filename.as_ref().as_ptr())
258    }
259
260    /// Get the [`Elements`](::Elements) container of this Anitomy instance.
261    pub unsafe fn elements(&self) -> &Elements {
262        &*(ffi::anitomy_elements(self.anitomy) as *const Elements)
263    }
264
265    /// Get the [`Options`](::Options) of this Anitomy instance.
266    pub unsafe fn options(&mut self) -> &mut Options {
267        &mut *(ffi::anitomy_options(self.anitomy) as *mut Options)
268    }
269
270    /// Destroy this instance.
271    ///
272    /// This should always be called to free the associated C++ Anitomy instance and resources.
273    pub unsafe fn destroy(&mut self) {
274        ffi::anitomy_destroy(self.anitomy)
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use std::ffi::CString;
282
283    const BLACK_BULLET_FILENAME: &'static str =
284        "[異域字幕組][漆黑的子彈][Black Bullet][11-12][1280x720][繁体].mp4";
285    const TORADORA_FILENAME: &'static str = "[TaigaSubs]_Toradora!_(2008)_-_01v2_-_Tiger_and_Dragon_[1280x720_H.264_FLAC][1234ABCD].mkv";
286
287    #[test]
288    fn anitomy_new_destroy() {
289        unsafe {
290            let mut ani = Anitomy::new();
291            ani.destroy();
292        }
293    }
294
295    #[test]
296    fn anitomy_parse_good_input() {
297        unsafe {
298            let filename = CString::new(BLACK_BULLET_FILENAME).unwrap();
299            let mut ani = Anitomy::new();
300
301            assert!(ani.parse(&filename));
302
303            ani.destroy();
304        }
305    }
306
307    #[test]
308    fn anitomy_parse_bad_input() {
309        unsafe {
310            let filename = CString::new("").unwrap();
311            let mut ani = Anitomy::new();
312
313            assert!(!ani.parse(&filename));
314
315            ani.destroy();
316        }
317    }
318
319    #[test]
320    fn anitomy_elements_empty_good_input() {
321        unsafe {
322            let filename = CString::new(BLACK_BULLET_FILENAME).unwrap();
323            let mut ani = Anitomy::new();
324
325            assert!(ani.parse(&filename));
326            {
327                let elems = ani.elements();
328                assert!(!elems.empty(None));
329                assert!(!elems.empty(ElementCategory::AnimeTitle));
330                assert!(elems.count(None) > 0);
331                assert!(elems.count(ElementCategory::AnimeTitle) == 1);
332            }
333
334            ani.destroy()
335        }
336    }
337
338    #[test]
339    fn anitomy_elements_empty_bad_input() {
340        unsafe {
341            let filename = CString::new("").unwrap();
342            let mut ani = Anitomy::new();
343
344            assert!(!ani.parse(&filename));
345            {
346                let elems = ani.elements();
347                assert!(elems.empty(None));
348                assert!(elems.empty(ElementCategory::AnimeTitle));
349                assert!(elems.count(None) == 0);
350                assert!(elems.count(ElementCategory::AnimeTitle) == 0);
351            }
352
353            ani.destroy()
354        }
355    }
356
357    #[test]
358    fn anitomy_elements_get_good_input() {
359        unsafe {
360            let filename = CString::new(BLACK_BULLET_FILENAME).unwrap();
361            let mut ani = Anitomy::new();
362
363            assert!(ani.parse(&filename));
364            {
365                let elems = ani.elements();
366                assert!(elems.count(ElementCategory::AnimeTitle) == 1);
367                assert_eq!(elems.get(ElementCategory::AnimeTitle), "Black Bullet");
368            }
369
370            ani.destroy()
371        }
372    }
373
374    #[test]
375    fn anitomy_elements_get_bad_input() {
376        unsafe {
377            let filename = CString::new("").unwrap();
378            let mut ani = Anitomy::new();
379
380            assert!(!ani.parse(&filename));
381            {
382                let elems = ani.elements();
383                assert!(elems.count(ElementCategory::AnimeTitle) == 0);
384                assert_eq!(elems.get(ElementCategory::AnimeTitle), "");
385            }
386
387            ani.destroy()
388        }
389    }
390
391    #[test]
392    fn anitomy_elements_get_all_good_input() {
393        unsafe {
394            let filename = CString::new(BLACK_BULLET_FILENAME).unwrap();
395            let mut ani = Anitomy::new();
396
397            assert!(ani.parse(&filename));
398            {
399                let elems = ani.elements();
400                assert!(elems.count(ElementCategory::EpisodeNumber) == 2);
401                assert_eq!(elems.get_all(ElementCategory::EpisodeNumber), ["11", "12"]);
402            }
403
404            ani.destroy()
405        }
406    }
407
408    #[test]
409    fn anitomy_elements_get_all_bad_input() {
410        unsafe {
411            let filename = CString::new("").unwrap();
412            let mut ani = Anitomy::new();
413
414            assert!(!ani.parse(&filename));
415            {
416                let elems = ani.elements();
417                assert!(elems.count(ElementCategory::EpisodeNumber) == 0);
418                assert_eq!(
419                    elems.get_all(ElementCategory::EpisodeNumber),
420                    Vec::<String>::new()
421                );
422            }
423
424            ani.destroy()
425        }
426    }
427
428    #[test]
429    fn anitomy_elements_at() {
430        unsafe {
431            let filename = CString::new(BLACK_BULLET_FILENAME).unwrap();
432            let mut ani = Anitomy::new();
433
434            assert!(ani.parse(&filename));
435            {
436                let elems = ani.elements();
437                let pair = elems.at(0).expect("at least one element");
438                assert_eq!(pair.category, ElementCategory::FileExtension);
439                assert_eq!(pair.value, "mp4");
440            }
441
442            ani.destroy();
443        }
444    }
445
446    #[test]
447    fn anitomy_options_allowed_delimiters() {
448        unsafe {
449            let filename = CString::new(TORADORA_FILENAME).unwrap();
450            let mut ani = Anitomy::new();
451
452            assert!(ani.parse(&filename));
453            {
454                let elems = ani.elements();
455                assert!(elems.count(ElementCategory::AnimeTitle) == 1);
456                assert_eq!(elems.get(ElementCategory::AnimeTitle), "Toradora!");
457            }
458
459            {
460                ani.options().allowed_delimiters(&CString::new("").unwrap());
461            }
462
463            assert!(ani.parse(&filename));
464            {
465                let elems = ani.elements();
466                assert!(elems.count(ElementCategory::AnimeTitle) == 1);
467                assert_eq!(elems.get(ElementCategory::AnimeTitle), "_Toradora!_");
468            }
469
470            ani.destroy();
471        }
472    }
473
474    #[test]
475    fn anitomy_options_ignored_strings() {
476        unsafe {
477            let filename = CString::new(TORADORA_FILENAME).unwrap();
478            let mut ani = Anitomy::new();
479
480            assert!(ani.parse(&filename));
481            {
482                let elems = ani.elements();
483                assert!(elems.count(ElementCategory::EpisodeTitle) == 1);
484                assert_eq!(elems.get(ElementCategory::EpisodeTitle), "Tiger and Dragon");
485            }
486
487            {
488                ani.options()
489                    .ignored_strings(&[CString::new("Dragon").unwrap()]);
490            }
491
492            assert!(ani.parse(&filename));
493            {
494                let elems = ani.elements();
495                assert!(elems.count(ElementCategory::EpisodeTitle) == 1);
496                assert_eq!(elems.get(ElementCategory::EpisodeTitle), "Tiger and");
497            }
498
499            ani.destroy();
500        }
501    }
502
503    #[test]
504    fn anitomy_options_parse_episode_number() {
505        unsafe {
506            let filename = CString::new(TORADORA_FILENAME).unwrap();
507            let mut ani = Anitomy::new();
508
509            assert!(ani.parse(&filename));
510            {
511                let elems = ani.elements();
512                assert!(elems.count(ElementCategory::EpisodeNumber) == 1);
513            }
514
515            {
516                ani.options().parse_episode_number(false);
517            }
518
519            assert!(ani.parse(&filename));
520            {
521                let elems = ani.elements();
522                assert!(elems.count(ElementCategory::EpisodeNumber) == 0);
523            }
524
525            ani.destroy();
526        }
527    }
528
529    #[test]
530    fn anitomy_options_parse_episode_title() {
531        unsafe {
532            let filename = CString::new(TORADORA_FILENAME).unwrap();
533            let mut ani = Anitomy::new();
534
535            assert!(ani.parse(&filename));
536            {
537                let elems = ani.elements();
538                assert!(elems.count(ElementCategory::EpisodeTitle) == 1);
539            }
540
541            {
542                ani.options().parse_episode_title(false);
543            }
544
545            assert!(ani.parse(&filename));
546            {
547                let elems = ani.elements();
548                assert!(elems.count(ElementCategory::EpisodeTitle) == 0);
549            }
550
551            ani.destroy();
552        }
553    }
554
555    #[test]
556    fn anitomy_options_parse_file_extension() {
557        unsafe {
558            let filename = CString::new(TORADORA_FILENAME).unwrap();
559            let mut ani = Anitomy::new();
560
561            assert!(ani.parse(&filename));
562            {
563                let elems = ani.elements();
564                assert!(elems.count(ElementCategory::FileExtension) == 1);
565            }
566
567            {
568                ani.options().parse_file_extension(false);
569            }
570
571            assert!(ani.parse(&filename));
572            {
573                let elems = ani.elements();
574                assert!(elems.count(ElementCategory::FileExtension) == 0);
575            }
576
577            ani.destroy();
578        }
579    }
580
581    #[test]
582    fn anitomy_options_parse_release_group() {
583        unsafe {
584            let filename = CString::new(TORADORA_FILENAME).unwrap();
585            let mut ani = Anitomy::new();
586
587            assert!(ani.parse(&filename));
588            {
589                let elems = ani.elements();
590                assert!(elems.count(ElementCategory::ReleaseGroup) == 1);
591            }
592
593            {
594                ani.options().parse_release_group(false);
595            }
596
597            assert!(ani.parse(&filename));
598            {
599                let elems = ani.elements();
600                assert!(elems.count(ElementCategory::ReleaseGroup) == 0);
601            }
602
603            ani.destroy();
604        }
605    }
606}