cadmus_core/
metadata.rs

1use crate::document::asciify;
2use crate::document::djvu::DjvuOpener;
3use crate::document::epub::EpubDocument;
4use crate::document::html::HtmlDocument;
5use crate::document::pdf::PdfOpener;
6use crate::document::{Document, SimpleTocEntry, TextLocation};
7use crate::geom::Point;
8use crate::helpers::datetime_format;
9use chrono::{Local, NaiveDateTime};
10use fxhash::FxHashMap;
11use lazy_static::lazy_static;
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use std::cmp::Ordering;
15use std::collections::{BTreeMap, BTreeSet};
16use std::ffi::OsStr;
17use std::fmt;
18use std::fs;
19use std::path::{Path, PathBuf};
20use titlecase::titlecase;
21use tracing::{error, warn};
22
23pub const DEFAULT_CONTRAST_EXPONENT: f32 = 1.0;
24pub const DEFAULT_CONTRAST_GRAY: f32 = 224.0;
25
26pub type Metadata = Vec<Info>;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(default, rename_all = "camelCase")]
30pub struct Info {
31    #[serde(skip_serializing_if = "String::is_empty")]
32    pub title: String,
33    #[serde(skip_serializing_if = "String::is_empty")]
34    pub subtitle: String,
35    #[serde(skip_serializing_if = "String::is_empty")]
36    pub author: String,
37    #[serde(skip_serializing_if = "String::is_empty")]
38    pub year: String,
39    #[serde(skip_serializing_if = "String::is_empty")]
40    pub language: String,
41    #[serde(skip_serializing_if = "String::is_empty")]
42    pub publisher: String,
43    #[serde(skip_serializing_if = "String::is_empty")]
44    pub series: String,
45    #[serde(skip_serializing_if = "String::is_empty")]
46    pub edition: String,
47    #[serde(skip_serializing_if = "String::is_empty")]
48    pub volume: String,
49    #[serde(skip_serializing_if = "String::is_empty")]
50    pub number: String,
51    #[serde(skip_serializing_if = "String::is_empty")]
52    pub identifier: String,
53    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
54    pub categories: BTreeSet<String>,
55    pub file: FileInfo,
56    #[serde(skip_serializing)]
57    pub reader: Option<ReaderInfo>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub reader_info: Option<ReaderInfo>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub toc: Option<Vec<SimpleTocEntry>>,
62    #[serde(with = "datetime_format")]
63    pub added: NaiveDateTime,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(default, rename_all = "camelCase")]
68pub struct FileInfo {
69    pub path: PathBuf,
70    pub kind: String,
71    pub size: u64,
72}
73
74impl Default for FileInfo {
75    fn default() -> Self {
76        FileInfo {
77            path: PathBuf::default(),
78            kind: String::default(),
79            size: u64::default(),
80        }
81    }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(default, rename_all = "camelCase")]
86pub struct Annotation {
87    #[serde(skip_serializing_if = "String::is_empty")]
88    pub note: String,
89    #[serde(skip_serializing_if = "String::is_empty")]
90    pub text: String,
91    pub selection: [TextLocation; 2],
92    #[serde(with = "datetime_format")]
93    pub modified: NaiveDateTime,
94}
95
96impl Default for Annotation {
97    fn default() -> Self {
98        Annotation {
99            note: String::new(),
100            text: String::new(),
101            selection: [TextLocation::Dynamic(0), TextLocation::Dynamic(1)],
102            modified: Local::now().naive_local(),
103        }
104    }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Margin {
109    pub top: f32,
110    pub right: f32,
111    pub bottom: f32,
112    pub left: f32,
113}
114
115impl Margin {
116    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Margin {
117        Margin {
118            top,
119            right,
120            bottom,
121            left,
122        }
123    }
124}
125
126impl Default for Margin {
127    fn default() -> Margin {
128        Margin::new(0.0, 0.0, 0.0, 0.0)
129    }
130}
131
132#[derive(Debug, Copy, Clone, Eq, PartialEq)]
133pub enum PageScheme {
134    Any,
135    EvenOdd,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(untagged)]
140pub enum CroppingMargins {
141    Any(Margin),
142    EvenOdd([Margin; 2]),
143}
144
145impl CroppingMargins {
146    pub fn margin(&self, index: usize) -> &Margin {
147        match *self {
148            CroppingMargins::Any(ref margin) => margin,
149            CroppingMargins::EvenOdd(ref pair) => &pair[index % 2],
150        }
151    }
152
153    pub fn margin_mut(&mut self, index: usize) -> &mut Margin {
154        match *self {
155            CroppingMargins::Any(ref mut margin) => margin,
156            CroppingMargins::EvenOdd(ref mut pair) => &mut pair[index % 2],
157        }
158    }
159
160    pub fn apply(&mut self, index: usize, scheme: PageScheme) {
161        let margin = self.margin(index).clone();
162
163        match scheme {
164            PageScheme::Any => *self = CroppingMargins::Any(margin),
165            PageScheme::EvenOdd => *self = CroppingMargins::EvenOdd([margin.clone(), margin]),
166        }
167    }
168
169    pub fn is_split(&self) -> bool {
170        !matches!(*self, CroppingMargins::Any(..))
171    }
172}
173
174#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
175#[serde(rename_all = "kebab-case")]
176pub enum TextAlign {
177    Justify,
178    Left,
179    Right,
180    Center,
181}
182
183impl TextAlign {
184    pub fn icon_name(&self) -> &str {
185        match self {
186            TextAlign::Justify => "align-justify",
187            TextAlign::Left => "align-left",
188            TextAlign::Right => "align-right",
189            TextAlign::Center => "align-center",
190        }
191    }
192}
193
194impl fmt::Display for TextAlign {
195    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
196        fmt::Debug::fmt(self, f)
197    }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(default, rename_all = "camelCase")]
202pub struct ReaderInfo {
203    #[serde(with = "datetime_format")]
204    pub opened: NaiveDateTime,
205    pub current_page: usize,
206    pub pages_count: usize,
207    pub finished: bool,
208    pub dithered: bool,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub zoom_mode: Option<ZoomMode>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub scroll_mode: Option<ScrollMode>,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub page_offset: Option<Point>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub rotation: Option<i8>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub cropping_margins: Option<CroppingMargins>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub margin_width: Option<i32>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub screen_margin_width: Option<i32>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub font_family: Option<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub font_size: Option<f32>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub text_align: Option<TextAlign>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub line_height: Option<f32>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub contrast_exponent: Option<f32>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub contrast_gray: Option<f32>,
235    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
236    pub page_names: BTreeMap<usize, String>,
237    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
238    pub bookmarks: BTreeSet<usize>,
239    #[serde(skip_serializing_if = "Vec::is_empty")]
240    pub annotations: Vec<Annotation>,
241}
242
243#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
244pub enum ZoomMode {
245    FitToPage,
246    FitToWidth,
247    Custom(f32),
248}
249
250#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
251pub enum ScrollMode {
252    Screen,
253    Page,
254}
255
256impl PartialEq for ZoomMode {
257    fn eq(&self, other: &Self) -> bool {
258        match (self, other) {
259            (ZoomMode::FitToPage, ZoomMode::FitToPage) => true,
260            (ZoomMode::FitToWidth, ZoomMode::FitToWidth) => true,
261            (ZoomMode::Custom(z1), ZoomMode::Custom(z2)) => (z1 - z2).abs() < f32::EPSILON,
262            _ => false,
263        }
264    }
265}
266
267impl Eq for ZoomMode {}
268
269impl ReaderInfo {
270    pub fn progress(&self) -> f32 {
271        (self.current_page / self.pages_count) as f32
272    }
273}
274
275impl Default for ReaderInfo {
276    fn default() -> Self {
277        ReaderInfo {
278            opened: Local::now().naive_local(),
279            current_page: 0,
280            pages_count: 1,
281            finished: false,
282            dithered: false,
283            zoom_mode: None,
284            scroll_mode: None,
285            page_offset: None,
286            rotation: None,
287            cropping_margins: None,
288            margin_width: None,
289            screen_margin_width: None,
290            font_family: None,
291            font_size: None,
292            text_align: None,
293            line_height: None,
294            contrast_exponent: None,
295            contrast_gray: None,
296            page_names: BTreeMap::new(),
297            bookmarks: BTreeSet::new(),
298            annotations: Vec::new(),
299        }
300    }
301}
302
303impl Default for Info {
304    fn default() -> Self {
305        Info {
306            title: String::default(),
307            subtitle: String::default(),
308            author: String::default(),
309            year: String::default(),
310            language: String::default(),
311            publisher: String::default(),
312            series: String::default(),
313            edition: String::default(),
314            volume: String::default(),
315            number: String::default(),
316            identifier: String::default(),
317            categories: BTreeSet::new(),
318            file: FileInfo::default(),
319            added: Local::now().naive_local(),
320            reader: None,
321            reader_info: None,
322            toc: None,
323        }
324    }
325}
326
327#[derive(Debug, Copy, Clone)]
328pub enum Status {
329    New,
330    Reading(f32),
331    Finished,
332}
333
334#[derive(Debug, Copy, Clone, Eq, PartialEq)]
335pub enum SimpleStatus {
336    New,
337    Reading,
338    Finished,
339}
340
341impl fmt::Display for SimpleStatus {
342    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
343        fmt::Debug::fmt(self, f)
344    }
345}
346
347impl Info {
348    pub fn status(&self) -> Status {
349        if let Some(ref r) = self.reader {
350            if r.finished {
351                Status::Finished
352            } else {
353                Status::Reading(r.current_page as f32 / r.pages_count as f32)
354            }
355        } else {
356            Status::New
357        }
358    }
359
360    pub fn simple_status(&self) -> SimpleStatus {
361        if let Some(ref r) = self.reader {
362            if r.finished {
363                SimpleStatus::Finished
364            } else {
365                SimpleStatus::Reading
366            }
367        } else {
368            SimpleStatus::New
369        }
370    }
371
372    pub fn file_stem(&self) -> String {
373        self.file
374            .path
375            .file_stem()
376            .unwrap()
377            .to_string_lossy()
378            .into_owned()
379    }
380
381    pub fn title(&self) -> String {
382        if self.title.is_empty() {
383            return self.file_stem();
384        }
385
386        let mut title = self.title.clone();
387
388        if !self.number.is_empty() && self.series.is_empty() {
389            title = format!("{} #{}", title, self.number);
390        }
391
392        if !self.volume.is_empty() {
393            title = format!("{} — vol. {}", title, self.volume);
394        }
395
396        if !self.subtitle.is_empty() {
397            title = if self.subtitle.chars().next().unwrap().is_alphanumeric()
398                && title.chars().last().unwrap().is_alphanumeric()
399            {
400                format!("{}: {}", title, self.subtitle)
401            } else {
402                format!("{} {}", title, self.subtitle)
403            };
404        }
405
406        if !self.series.is_empty() && !self.number.is_empty() {
407            title = format!("{} ({} #{})", title, self.series, self.number);
408        }
409
410        title
411    }
412
413    // TODO: handle the following case: *Walter M. Miller Jr.*?
414    // NOTE: e.g.: John Le Carré: the space between *Le* and *Carré*
415    // is a non-breaking space
416    pub fn alphabetic_author(&self) -> &str {
417        self.author
418            .split(',')
419            .next()
420            .and_then(|a| a.split(' ').last())
421            .unwrap_or_default()
422    }
423
424    pub fn alphabetic_title(&self) -> &str {
425        let mut start = 0;
426
427        let lang = if self.language.is_empty() || self.language.starts_with("en") {
428            "en"
429        } else if self.language.starts_with("fr") {
430            "fr"
431        } else {
432            &self.language
433        };
434
435        if let Some(m) = TITLE_PREFIXES.get(lang).and_then(|re| re.find(&self.title)) {
436            start = m.end()
437        }
438
439        &self.title[start..]
440    }
441
442    pub fn label(&self) -> String {
443        if !self.author.is_empty() {
444            format!("{} · {}", self.title(), &self.author)
445        } else {
446            self.title()
447        }
448    }
449}
450
451pub fn make_query(text: &str) -> Option<Regex> {
452    let any = Regex::new(r"^(\.*|\s)$").unwrap();
453
454    if any.is_match(text) {
455        return None;
456    }
457
458    let text = text
459        .replace('a', "[aáàâä]")
460        .replace('e', "[eéèêë]")
461        .replace('i', "[iíìîï]")
462        .replace('o', "[oóòôö]")
463        .replace('u', "[uúùûü]")
464        .replace('c', "[cç]")
465        .replace("ae", "(ae|æ)")
466        .replace("oe", "(oe|œ)");
467    Regex::new(&format!("(?i){}", text))
468        .map_err(|e| error!("Can't create query: {:#}.", e))
469        .ok()
470}
471
472#[derive(Debug, Clone, Default)]
473pub struct BookQuery {
474    pub free: Option<Regex>,
475    pub title: Option<Regex>,
476    pub subtitle: Option<Regex>,
477    pub author: Option<Regex>,
478    pub year: Option<Regex>,
479    pub language: Option<Regex>,
480    pub publisher: Option<Regex>,
481    pub series: Option<Regex>,
482    pub edition: Option<Regex>,
483    pub volume: Option<Regex>,
484    pub number: Option<Regex>,
485    pub reading: Option<bool>,
486    pub new: Option<bool>,
487    pub finished: Option<bool>,
488    pub annotations: Option<bool>,
489    pub bookmarks: Option<bool>,
490    pub opened_after: Option<(bool, NaiveDateTime)>,
491    pub added_after: Option<(bool, NaiveDateTime)>,
492}
493
494impl BookQuery {
495    pub fn new(text: &str) -> Option<BookQuery> {
496        let mut buf = Vec::new();
497        let mut query = BookQuery::default();
498        for word in text.rsplit(' ') {
499            let mut chars = word.chars().peekable();
500            match chars.next() {
501                Some('\'') => {
502                    let mut invert = false;
503                    if chars.peek() == Some(&'!') {
504                        invert = true;
505                        chars.next();
506                    }
507                    match chars.next() {
508                        Some('t') => {
509                            buf.reverse();
510                            query.title = make_query(&buf.join(" "));
511                            buf.clear();
512                        }
513                        Some('u') => {
514                            buf.reverse();
515                            query.subtitle = make_query(&buf.join(" "));
516                            buf.clear();
517                        }
518                        Some('a') => {
519                            buf.reverse();
520                            query.author = make_query(&buf.join(" "));
521                            buf.clear();
522                        }
523                        Some('y') => {
524                            buf.reverse();
525                            query.year = make_query(&buf.join(" "));
526                            buf.clear();
527                        }
528                        Some('l') => {
529                            buf.reverse();
530                            query.language = make_query(&buf.join(" "));
531                            buf.clear();
532                        }
533                        Some('p') => {
534                            buf.reverse();
535                            query.publisher = make_query(&buf.join(" "));
536                            buf.clear();
537                        }
538                        Some('s') => {
539                            buf.reverse();
540                            query.series = make_query(&buf.join(" "));
541                            buf.clear();
542                        }
543                        Some('e') => {
544                            buf.reverse();
545                            query.edition = make_query(&buf.join(" "));
546                            buf.clear();
547                        }
548                        Some('v') => {
549                            buf.reverse();
550                            query.volume = make_query(&buf.join(" "));
551                            buf.clear();
552                        }
553                        Some('n') => {
554                            buf.reverse();
555                            query.number = make_query(&buf.join(" "));
556                            buf.clear();
557                        }
558                        Some('R') => query.reading = Some(!invert),
559                        Some('N') => query.new = Some(!invert),
560                        Some('F') => query.finished = Some(!invert),
561                        Some('A') => query.annotations = Some(!invert),
562                        Some('B') => query.bookmarks = Some(!invert),
563                        Some('O') => {
564                            buf.reverse();
565                            query.opened_after = NaiveDateTime::parse_from_str(
566                                &buf.join(" "),
567                                datetime_format::FORMAT,
568                            )
569                            .ok()
570                            .map(|opened| (!invert, opened));
571                            buf.clear();
572                        }
573                        Some('D') => {
574                            buf.reverse();
575                            query.added_after = NaiveDateTime::parse_from_str(
576                                &buf.join(" "),
577                                datetime_format::FORMAT,
578                            )
579                            .ok()
580                            .map(|added| (!invert, added));
581                            buf.clear();
582                        }
583                        Some('\'') => buf.push(&word[1..]),
584                        _ => (),
585                    }
586                }
587                _ => buf.push(word),
588            }
589        }
590        buf.reverse();
591        query.free = make_query(&buf.join(" "));
592        if query.free.is_none()
593            && query.title.is_none()
594            && query.subtitle.is_none()
595            && query.author.is_none()
596            && query.year.is_none()
597            && query.language.is_none()
598            && query.publisher.is_none()
599            && query.series.is_none()
600            && query.edition.is_none()
601            && query.volume.is_none()
602            && query.number.is_none()
603            && query.reading.is_none()
604            && query.new.is_none()
605            && query.finished.is_none()
606            && query.annotations.is_none()
607            && query.bookmarks.is_none()
608            && query.opened_after.is_none()
609            && query.added_after.is_none()
610        {
611            None
612        } else {
613            Some(query)
614        }
615    }
616
617    #[inline]
618    pub fn is_match(&self, info: &Info) -> bool {
619        self.free.as_ref().map(|re| {
620            re.is_match(&info.title)
621                || re.is_match(&info.subtitle)
622                || re.is_match(&info.author)
623                || re.is_match(&info.series)
624                || info.file.path.to_str().map_or(false, |s| re.is_match(s))
625        }) != Some(false)
626            && self.title.as_ref().map(|re| re.is_match(&info.title)) != Some(false)
627            && self.subtitle.as_ref().map(|re| re.is_match(&info.subtitle)) != Some(false)
628            && self.author.as_ref().map(|re| re.is_match(&info.author)) != Some(false)
629            && self.year.as_ref().map(|re| re.is_match(&info.year)) != Some(false)
630            && self.language.as_ref().map(|re| re.is_match(&info.language)) != Some(false)
631            && self
632                .publisher
633                .as_ref()
634                .map(|re| re.is_match(&info.publisher))
635                != Some(false)
636            && self.series.as_ref().map(|re| re.is_match(&info.series)) != Some(false)
637            && self.edition.as_ref().map(|re| re.is_match(&info.edition)) != Some(false)
638            && self.volume.as_ref().map(|re| re.is_match(&info.volume)) != Some(false)
639            && self.number.as_ref().map(|re| re.is_match(&info.number)) != Some(false)
640            && self
641                .reading
642                .as_ref()
643                .map(|eq| info.simple_status().eq(&SimpleStatus::Reading) == *eq)
644                != Some(false)
645            && self
646                .new
647                .as_ref()
648                .map(|eq| info.simple_status().eq(&SimpleStatus::New) == *eq)
649                != Some(false)
650            && self
651                .finished
652                .as_ref()
653                .map(|eq| info.simple_status().eq(&SimpleStatus::Finished) == *eq)
654                != Some(false)
655            && self.annotations.as_ref().map(|eq| {
656                info.reader
657                    .as_ref()
658                    .map_or(false, |r| !r.annotations.is_empty())
659                    == *eq
660            }) != Some(false)
661            && self.bookmarks.as_ref().map(|eq| {
662                info.reader
663                    .as_ref()
664                    .map_or(false, |r| !r.bookmarks.is_empty())
665                    == *eq
666            }) != Some(false)
667            && self.opened_after.as_ref().map(|(eq, opened)| {
668                info.reader.as_ref().map_or(false, |r| r.opened.gt(opened)) == *eq
669            }) != Some(false)
670            && self
671                .added_after
672                .as_ref()
673                .map(|(eq, added)| info.added.gt(added) == *eq)
674                != Some(false)
675    }
676
677    #[inline]
678    pub fn is_simple_match(&self, text: &str) -> bool {
679        self.free.as_ref().map_or(true, |q| q.is_match(text))
680    }
681}
682
683#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
684#[serde(rename_all = "kebab-case")]
685pub enum SortMethod {
686    Opened,
687    Added,
688    Status,
689    Progress,
690    Title,
691    Year,
692    Author,
693    Series,
694    Pages,
695    Size,
696    Kind,
697    FileName,
698    FilePath,
699}
700
701impl SortMethod {
702    pub fn reverse_order(self) -> bool {
703        !matches!(
704            self,
705            SortMethod::Author
706                | SortMethod::Title
707                | SortMethod::Series
708                | SortMethod::Kind
709                | SortMethod::FileName
710                | SortMethod::FilePath
711        )
712    }
713
714    pub fn is_status_related(self) -> bool {
715        matches!(
716            self,
717            SortMethod::Opened | SortMethod::Status | SortMethod::Progress
718        )
719    }
720
721    pub fn label(&self) -> &str {
722        match *self {
723            SortMethod::Opened => "Date Opened",
724            SortMethod::Added => "Date Added",
725            SortMethod::Status => "Status",
726            SortMethod::Progress => "Progress",
727            SortMethod::Author => "Author",
728            SortMethod::Title => "Title",
729            SortMethod::Year => "Year",
730            SortMethod::Series => "Series",
731            SortMethod::Size => "File Size",
732            SortMethod::Kind => "File Type",
733            SortMethod::Pages => "Pages Count",
734            SortMethod::FileName => "File Name",
735            SortMethod::FilePath => "File Path",
736        }
737    }
738
739    pub fn title(self) -> String {
740        format!("Sort by: {}", self.label())
741    }
742}
743
744pub fn sort(md: &mut Metadata, sort_method: SortMethod, reverse_order: bool) {
745    let sort_fn = sorter(sort_method);
746
747    if reverse_order {
748        md.sort_by(|a, b| sort_fn(a, b).reverse());
749    } else {
750        md.sort_by(sort_fn);
751    }
752}
753
754#[inline]
755pub fn sorter(sort_method: SortMethod) -> fn(&Info, &Info) -> Ordering {
756    match sort_method {
757        SortMethod::Opened => sort_opened,
758        SortMethod::Added => sort_added,
759        SortMethod::Status => sort_status,
760        SortMethod::Progress => sort_progress,
761        SortMethod::Author => sort_author,
762        SortMethod::Title => sort_title,
763        SortMethod::Year => sort_year,
764        SortMethod::Series => sort_series,
765        SortMethod::Size => sort_size,
766        SortMethod::Kind => sort_kind,
767        SortMethod::Pages => sort_pages,
768        SortMethod::FileName => sort_filename,
769        SortMethod::FilePath => sort_filepath,
770    }
771}
772
773pub fn sort_opened(i1: &Info, i2: &Info) -> Ordering {
774    i1.reader
775        .as_ref()
776        .map(|r1| r1.opened)
777        .cmp(&i2.reader.as_ref().map(|r2| r2.opened))
778}
779
780pub fn sort_added(i1: &Info, i2: &Info) -> Ordering {
781    i1.added.cmp(&i2.added)
782}
783
784pub fn sort_pages(i1: &Info, i2: &Info) -> Ordering {
785    i1.reader
786        .as_ref()
787        .map(|r1| r1.pages_count)
788        .cmp(&i2.reader.as_ref().map(|r2| r2.pages_count))
789}
790
791// FIXME: 'Z'.cmp('É') equals Ordering::Less
792pub fn sort_author(i1: &Info, i2: &Info) -> Ordering {
793    i1.alphabetic_author().cmp(i2.alphabetic_author())
794}
795
796pub fn sort_title(i1: &Info, i2: &Info) -> Ordering {
797    i1.alphabetic_title().cmp(i2.alphabetic_title())
798}
799
800pub fn sort_status(i1: &Info, i2: &Info) -> Ordering {
801    match (i1.simple_status(), i2.simple_status()) {
802        (SimpleStatus::Reading, SimpleStatus::Reading)
803        | (SimpleStatus::Finished, SimpleStatus::Finished) => sort_opened(i1, i2),
804        (SimpleStatus::New, SimpleStatus::New) => sort_added(i1, i2),
805        (SimpleStatus::New, SimpleStatus::Finished) => Ordering::Greater,
806        (SimpleStatus::Finished, SimpleStatus::New) => Ordering::Less,
807        (SimpleStatus::New, SimpleStatus::Reading) => Ordering::Less,
808        (SimpleStatus::Reading, SimpleStatus::New) => Ordering::Greater,
809        (SimpleStatus::Finished, SimpleStatus::Reading) => Ordering::Less,
810        (SimpleStatus::Reading, SimpleStatus::Finished) => Ordering::Greater,
811    }
812}
813
814// Ordering: Finished < New < Reading.
815pub fn sort_progress(i1: &Info, i2: &Info) -> Ordering {
816    match (i1.status(), i2.status()) {
817        (Status::Finished, Status::Finished) => Ordering::Equal,
818        (Status::New, Status::New) => Ordering::Equal,
819        (Status::New, Status::Finished) => Ordering::Greater,
820        (Status::Finished, Status::New) => Ordering::Less,
821        (Status::New, Status::Reading(..)) => Ordering::Less,
822        (Status::Reading(..), Status::New) => Ordering::Greater,
823        (Status::Finished, Status::Reading(..)) => Ordering::Less,
824        (Status::Reading(..), Status::Finished) => Ordering::Greater,
825        (Status::Reading(p1), Status::Reading(p2)) => {
826            p1.partial_cmp(&p2).unwrap_or(Ordering::Equal)
827        }
828    }
829}
830
831pub fn sort_size(i1: &Info, i2: &Info) -> Ordering {
832    i1.file.size.cmp(&i2.file.size)
833}
834
835pub fn sort_kind(i1: &Info, i2: &Info) -> Ordering {
836    i1.file.kind.cmp(&i2.file.kind)
837}
838
839pub fn sort_year(i1: &Info, i2: &Info) -> Ordering {
840    i1.year.cmp(&i2.year)
841}
842
843pub fn sort_series(i1: &Info, i2: &Info) -> Ordering {
844    i1.series.cmp(&i2.series).then_with(|| {
845        usize::from_str_radix(&i1.number, 10)
846            .ok()
847            .zip(usize::from_str_radix(&i2.number, 10).ok())
848            .map_or_else(|| i1.number.cmp(&i2.number), |(a, b)| a.cmp(&b))
849    })
850}
851
852pub fn sort_filename(i1: &Info, i2: &Info) -> Ordering {
853    i1.file.path.file_name().cmp(&i2.file.path.file_name())
854}
855
856pub fn sort_filepath(i1: &Info, i2: &Info) -> Ordering {
857    i1.file.path.cmp(&i2.file.path)
858}
859
860lazy_static! {
861    pub static ref TITLE_PREFIXES: FxHashMap<&'static str, Regex> = {
862        let mut p = FxHashMap::default();
863        p.insert("en", Regex::new(r"^(The|An?)\s").unwrap());
864        p.insert(
865            "fr",
866            Regex::new(r"^(Les?\s|La\s|L’|Une?\s|Des?\s|Du\s)").unwrap(),
867        );
868        p
869    };
870}
871
872#[inline]
873pub fn extract_metadata_from_document(prefix: &Path, info: &mut Info) {
874    let path = prefix.join(&info.file.path);
875
876    match info.file.kind.as_ref() {
877        "epub" => match EpubDocument::new(&path) {
878            Ok(doc) => {
879                info.title = doc.title().unwrap_or_default();
880                info.author = doc.author().unwrap_or_default();
881                info.year = doc.year().unwrap_or_default();
882                info.publisher = doc.publisher().unwrap_or_default();
883                if let Some((title, index)) = doc.series() {
884                    info.series = title;
885                    info.number = index;
886                }
887                info.language = doc.language().unwrap_or_default();
888                info.categories.append(&mut doc.categories());
889            }
890            Err(e) => error!("Can't open {}: {:#}.", info.file.path.display(), e),
891        },
892        "html" | "htm" => match HtmlDocument::new(&path) {
893            Ok(doc) => {
894                info.title = doc.title().unwrap_or_default();
895                info.author = doc.author().unwrap_or_default();
896                info.language = doc.language().unwrap_or_default();
897            }
898            Err(e) => error!("Can't open {}: {:#}.", info.file.path.display(), e),
899        },
900        "pdf" => match PdfOpener::new().and_then(|o| o.open(path)) {
901            Some(doc) => {
902                info.title = doc.title().unwrap_or_default();
903                info.author = doc.author().unwrap_or_default();
904            }
905            None => error!("Can't open {}.", info.file.path.display()),
906        },
907        "djvu" | "djv" => match DjvuOpener::new().and_then(|o| o.open(path)) {
908            Some(doc) => {
909                info.title = doc.title().unwrap_or_default();
910                info.author = doc.author().unwrap_or_default();
911                info.year = doc.year().unwrap_or_default();
912                info.series = doc.series().unwrap_or_default();
913                info.publisher = doc.publisher().unwrap_or_default();
914            }
915            None => error!("Can't open {}.", info.file.path.display()),
916        },
917        _ => {
918            warn!(
919                "Don't know how to extract metadata from {}.",
920                &info.file.kind
921            );
922        }
923    }
924}
925
926pub fn extract_metadata_from_filename(_prefix: &Path, info: &mut Info) {
927    if let Some(filename) = info.file.path.file_name().and_then(OsStr::to_str) {
928        let mut start_index = 0;
929
930        if filename.starts_with('(') {
931            start_index += 1;
932            if let Some(index) = filename[start_index..].find(')') {
933                info.series = filename[start_index..start_index + index]
934                    .trim_end()
935                    .to_string();
936                start_index += index + 1;
937            }
938        }
939
940        if let Some(index) = filename[start_index..].find("- ") {
941            info.author = filename[start_index..start_index + index]
942                .trim()
943                .to_string();
944            start_index += index + 1;
945        }
946
947        let title_start = start_index;
948
949        if let Some(index) = filename[start_index..].find('_') {
950            info.title = filename[start_index..start_index + index]
951                .trim_start()
952                .to_string();
953            start_index += index + 1;
954        }
955
956        if let Some(index) = filename[start_index..].find('-') {
957            if title_start == start_index {
958                info.title = filename[start_index..start_index + index]
959                    .trim_start()
960                    .to_string();
961            } else {
962                info.subtitle = filename[start_index..start_index + index]
963                    .trim_start()
964                    .to_string();
965            }
966            start_index += index + 1;
967        }
968
969        if let Some(index) = filename[start_index..].find('(') {
970            info.publisher = filename[start_index..start_index + index]
971                .trim_end()
972                .to_string();
973            start_index += index + 1;
974        }
975
976        if let Some(index) = filename[start_index..].find(')') {
977            info.year = filename[start_index..start_index + index].to_string();
978        }
979    }
980}
981
982pub fn consolidate(_prefix: &Path, info: &mut Info) {
983    if info.subtitle.is_empty() {
984        if let Some(index) = info.title.find(':') {
985            let cur_title = info.title.clone();
986            let (title, subtitle) = cur_title.split_at(index);
987            info.title = title.trim_end().to_string();
988            info.subtitle = subtitle[1..].trim_start().to_string();
989        }
990    }
991
992    if info.language.is_empty() || info.language.starts_with("en") {
993        info.title = titlecase(&info.title);
994        info.subtitle = titlecase(&info.subtitle);
995    }
996
997    info.title = info.title.replace('\'', "’");
998    info.subtitle = info.subtitle.replace('\'', "’");
999    info.author = info.author.replace('\'', "’");
1000    if info.year.len() > 4 {
1001        info.year = info.year[..4].to_string();
1002    }
1003    info.series = info.series.replace('\'', "’");
1004    info.publisher = info.publisher.replace('\'', "’");
1005}
1006
1007pub fn rename_from_info(prefix: &Path, info: &mut Info) {
1008    let new_file_name = file_name_from_info(info);
1009    if !new_file_name.is_empty() {
1010        let old_path = prefix.join(&info.file.path);
1011        let new_path = old_path.with_file_name(&new_file_name);
1012        if old_path != new_path {
1013            match fs::rename(&old_path, &new_path) {
1014                Err(e) => error!(
1015                    "Can't rename {} to {}: {:#}.",
1016                    old_path.display(),
1017                    new_path.display(),
1018                    e
1019                ),
1020                Ok(..) => {
1021                    let relat = new_path.strip_prefix(prefix).unwrap_or(&new_path);
1022                    info.file.path = relat.to_path_buf();
1023                }
1024            }
1025        }
1026    }
1027}
1028
1029pub fn file_name_from_info(info: &Info) -> String {
1030    if info.title.is_empty() {
1031        return "".to_string();
1032    }
1033    let mut base = asciify(&info.title);
1034    if !info.subtitle.is_empty() {
1035        base = format!("{} - {}", base, asciify(&info.subtitle));
1036    }
1037    if !info.volume.is_empty() {
1038        base = format!("{} - {}", base, info.volume);
1039    }
1040    if !info.number.is_empty() && info.series.is_empty() {
1041        base = format!("{} - {}", base, info.number);
1042    }
1043    if !info.author.is_empty() {
1044        base = format!("{} - {}", base, asciify(&info.author));
1045    }
1046    base = format!("{}.{}", base, info.file.kind);
1047    base.replace("..", ".")
1048        .replace('/', " ")
1049        .replace('?', "")
1050        .replace('!', "")
1051        .replace(':', "")
1052}