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