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 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
791pub 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
814pub 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}