1pub mod ffi;
52
53use std::ffi::CStr;
54
55#[repr(C)]
57pub struct Options {
58 options: ffi::options_t,
59}
60
61impl Options {
62 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 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 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 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 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 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Element {
166 pub category: ElementCategory,
168 pub value: String,
170}
171
172#[repr(C)]
174pub struct Elements {
175 elements: ffi::elements_t,
176}
177
178impl Elements {
179 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 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 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 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 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#[derive(Debug)]
240pub struct Anitomy {
241 anitomy: *mut ffi::anitomy_t,
242}
243
244impl Anitomy {
245 pub unsafe fn new() -> Self {
247 Self {
248 anitomy: ffi::anitomy_new(),
249 }
250 }
251
252 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 pub unsafe fn elements(&self) -> &Elements {
262 &*(ffi::anitomy_elements(self.anitomy) as *const Elements)
263 }
264
265 pub unsafe fn options(&mut self) -> &mut Options {
267 &mut *(ffi::anitomy_options(self.anitomy) as *mut Options)
268 }
269
270 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}