cadmus_core/
library.rs

1use crate::document::file_kind;
2use crate::helpers::{load_json, save_json, Fingerprint, Fp, IsHidden};
3use crate::metadata::{extract_metadata_from_document, sort, sorter};
4use crate::metadata::{BookQuery, FileInfo, Info, ReaderInfo, SimpleStatus, SortMethod};
5use crate::settings::{ImportSettings, LibraryMode};
6use anyhow::{bail, format_err, Error};
7use chrono::{DateTime, Local};
8use fxhash::{FxBuildHasher, FxHashMap, FxHashSet};
9use indexmap::IndexMap;
10use std::collections::BTreeSet;
11use std::fs::{self, File};
12use std::io::{Error as IoError, ErrorKind};
13use std::path::{Path, PathBuf};
14use std::str::FromStr;
15use std::time::{Duration, SystemTime};
16use tracing::{debug, error, info, warn};
17use walkdir::WalkDir;
18
19pub const METADATA_FILENAME: &str = ".metadata.json";
20pub const FAT32_EPOCH_FILENAME: &str = ".fat32-epoch";
21pub const READING_STATES_DIRNAME: &str = ".reading-states";
22pub const THUMBNAIL_PREVIEWS_DIRNAME: &str = ".thumbnail-previews";
23
24pub struct Library {
25    pub home: PathBuf,
26    pub mode: LibraryMode,
27    pub db: IndexMap<Fp, Info, FxBuildHasher>,
28    pub paths: FxHashMap<PathBuf, Fp>,
29    pub reading_states: FxHashMap<Fp, ReaderInfo>,
30    pub modified_reading_states: FxHashSet<Fp>,
31    pub has_db_changed: bool,
32    pub fat32_epoch: SystemTime,
33    pub sort_method: SortMethod,
34    pub reverse_order: bool,
35    pub show_hidden: bool,
36}
37
38impl Library {
39    pub fn new<P: AsRef<Path>>(home: P, mode: LibraryMode) -> Result<Self, Error> {
40        if let Err(e) = fs::create_dir(&home) {
41            if e.kind() != ErrorKind::AlreadyExists {
42                bail!(e);
43            }
44        }
45
46        let path = home.as_ref().join(METADATA_FILENAME);
47        let mut db;
48        if mode == LibraryMode::Database {
49            match load_json::<IndexMap<Fp, Info, FxBuildHasher>, _>(&path) {
50                Err(e) => {
51                    if e.downcast_ref::<IoError>().map(|e| e.kind()) != Some(ErrorKind::NotFound) {
52                        bail!(e);
53                    } else {
54                        db = IndexMap::with_capacity_and_hasher(0, FxBuildHasher::default());
55                    }
56                }
57                Ok(v) => db = v,
58            }
59        } else {
60            db = IndexMap::with_capacity_and_hasher(0, FxBuildHasher::default());
61        }
62
63        let mut reading_states = FxHashMap::default();
64
65        let path = home.as_ref().join(READING_STATES_DIRNAME);
66        if let Err(e) = fs::create_dir(&path) {
67            if e.kind() != ErrorKind::AlreadyExists {
68                bail!(e);
69            }
70        }
71
72        for entry in fs::read_dir(&path)? {
73            let entry = entry?;
74            let path = entry.path();
75            if let Some(fp) = path
76                .file_stem()
77                .and_then(|v| v.to_str())
78                .and_then(|v| Fp::from_str(v).ok())
79            {
80                if let Ok(reader_info) =
81                    load_json(path).map_err(|e| error!("Can't load reading state: {:#}.", e))
82                {
83                    if mode == LibraryMode::Database {
84                        if let Some(info) = db.get_mut(&fp) {
85                            info.reader = Some(reader_info);
86                        } else {
87                            warn!("Unknown fingerprint: {}.", fp);
88                        }
89                    } else {
90                        reading_states.insert(fp, reader_info);
91                    }
92                }
93            }
94        }
95
96        let path = home.as_ref().join(THUMBNAIL_PREVIEWS_DIRNAME);
97        if !path.exists() {
98            fs::create_dir(&path).ok();
99        }
100
101        let paths = if mode == LibraryMode::Database {
102            db.iter()
103                .map(|(fp, info)| (info.file.path.clone(), *fp))
104                .collect()
105        } else {
106            FxHashMap::default()
107        };
108
109        let path = home.as_ref().join(FAT32_EPOCH_FILENAME);
110        if !path.exists() {
111            let file = File::create(&path)?;
112            file.set_modified(std::time::UNIX_EPOCH + Duration::from_secs(315_532_800))?;
113        }
114
115        let fat32_epoch = path.metadata()?.modified()?;
116
117        let sort_method = SortMethod::Opened;
118
119        Ok(Library {
120            home: home.as_ref().to_path_buf(),
121            mode,
122            db,
123            paths,
124            reading_states,
125            modified_reading_states: FxHashSet::default(),
126            has_db_changed: false,
127            fat32_epoch,
128            sort_method,
129            reverse_order: sort_method.reverse_order(),
130            show_hidden: false,
131        })
132    }
133
134    pub fn list<P: AsRef<Path>>(
135        &self,
136        prefix: P,
137        query: Option<&BookQuery>,
138        skip_files: bool,
139    ) -> (Vec<Info>, BTreeSet<PathBuf>) {
140        let mut dirs = BTreeSet::new();
141        let mut files = Vec::new();
142
143        match self.mode {
144            LibraryMode::Database => {
145                let relat_prefix = prefix
146                    .as_ref()
147                    .strip_prefix(&self.home)
148                    .unwrap_or_else(|_| prefix.as_ref());
149                for (_, info) in self.db.iter() {
150                    if let Ok(relat) = info.file.path.strip_prefix(relat_prefix) {
151                        let mut compos = relat.components();
152                        let mut first = compos.next();
153                        // `first` is a file.
154                        if compos.next().is_none() {
155                            first = None;
156                        }
157                        if let Some(child) = first {
158                            dirs.insert(prefix.as_ref().join(child.as_os_str()));
159                        }
160                        if skip_files {
161                            continue;
162                        }
163                        if query.map_or(true, |q| q.is_match(info)) {
164                            files.push(info.clone());
165                        }
166                    }
167                }
168            }
169            LibraryMode::Filesystem => {
170                if !prefix.as_ref().is_dir() {
171                    return (files, dirs);
172                }
173
174                let max_depth = if query.is_some() { usize::MAX } else { 1 };
175
176                for entry in WalkDir::new(prefix.as_ref())
177                    .min_depth(1)
178                    .max_depth(max_depth)
179                    .into_iter()
180                    .filter_entry(|e| self.show_hidden || !e.is_hidden())
181                {
182                    if entry.is_err() {
183                        continue;
184                    }
185                    let entry = entry.unwrap();
186                    let path = entry.path();
187
188                    if path.is_dir() {
189                        if entry.depth() == 1 {
190                            dirs.insert(path.to_path_buf());
191                        }
192                    } else {
193                        let relat = path.strip_prefix(&self.home).unwrap_or(path);
194                        if skip_files
195                            || query.map_or(false, |q| {
196                                relat.to_str().map_or(true, |s| !q.is_simple_match(s))
197                            })
198                        {
199                            continue;
200                        }
201
202                        let kind = file_kind(&path).unwrap_or_default();
203                        let md = entry.metadata().unwrap();
204                        let size = md.len();
205                        let fp = md.fingerprint(self.fat32_epoch).unwrap();
206                        let file = FileInfo {
207                            path: relat.to_path_buf(),
208                            kind,
209                            size,
210                        };
211                        let secs = (*fp >> 32) as i64;
212                        let nsecs = ((*fp & ((1 << 32) - 1)) % 1_000_000_000) as u32;
213                        let added = DateTime::from_timestamp(secs, nsecs).unwrap().naive_utc();
214                        let info = Info {
215                            file,
216                            added,
217                            reader: self.reading_states.get(&fp).cloned(),
218                            ..Default::default()
219                        };
220
221                        files.push(info);
222                    }
223                }
224
225                sort(&mut files, self.sort_method, self.reverse_order);
226            }
227        }
228
229        (files, dirs)
230    }
231
232    pub fn import(&mut self, settings: &ImportSettings) {
233        if self.mode == LibraryMode::Filesystem {
234            return;
235        }
236
237        for entry in WalkDir::new(&self.home)
238            .min_depth(1)
239            .into_iter()
240            .filter_entry(|e| !e.is_hidden())
241        {
242            if entry.is_err() {
243                continue;
244            }
245
246            let entry = entry.unwrap();
247            if entry.file_type().is_dir() {
248                continue;
249            }
250
251            let path = entry.path();
252            let relat = path.strip_prefix(&self.home).unwrap_or(path);
253            let md = entry.metadata().unwrap();
254            let fp = md.fingerprint(self.fat32_epoch).unwrap();
255
256            // The fp is know: update the path if it changed.
257            if self.db.contains_key(&fp) {
258                if relat != self.db[&fp].file.path {
259                    debug!(
260                        "Update path for {}: {} → {}.",
261                        fp,
262                        self.db[&fp].file.path.display(),
263                        relat.display()
264                    );
265                    self.paths.remove(&self.db[&fp].file.path);
266                    self.paths.insert(relat.to_path_buf(), fp);
267                    self.db[&fp].file.path = relat.to_path_buf();
268                    self.has_db_changed = true;
269                }
270            // The path is known: update the fp.
271            } else if let Some(fp2) = self.paths.get(relat).cloned() {
272                debug!(
273                    "Update fingerprint for {}: {} → {}.",
274                    relat.display(),
275                    fp2,
276                    fp
277                );
278                let mut info = self.db.swap_remove(&fp2).unwrap();
279                if settings.sync_metadata && settings.metadata_kinds.contains(&info.file.kind) {
280                    extract_metadata_from_document(&self.home, &mut info);
281                }
282                self.db.insert(fp, info);
283                self.db[&fp].file.size = md.len();
284                self.paths.insert(relat.to_path_buf(), fp);
285                let rp1 = self.reading_state_path(fp2);
286                let rp2 = self.reading_state_path(fp);
287                fs::rename(rp1, rp2).ok();
288                let tpp = self.thumbnail_preview_path(fp2);
289                if tpp.exists() {
290                    fs::remove_file(tpp).ok();
291                }
292                self.has_db_changed = true;
293            } else {
294                let fp1 = self
295                    .fat32_epoch
296                    .checked_sub(Duration::from_secs(1))
297                    .and_then(|epoch| md.fingerprint(epoch).ok())
298                    .unwrap_or(fp);
299                let fp2 = self
300                    .fat32_epoch
301                    .checked_add(Duration::from_secs(1))
302                    .and_then(|epoch| md.fingerprint(epoch).ok())
303                    .unwrap_or(fp);
304
305                let nfp = if fp1 != fp && self.db.contains_key(&fp1) {
306                    Some(fp1)
307                } else if fp2 != fp && self.db.contains_key(&fp2) {
308                    Some(fp2)
309                } else {
310                    None
311                };
312
313                // On a FAT32 file system, the modification time has a two-second precision.
314                // This might be the reason why the modification time of a file can sometimes
315                // drift by one second, when the file is created within an operating system
316                // and moved within another.
317                if let Some(nfp) = nfp {
318                    debug!(
319                        "Update fingerprint for {}: {} → {}.",
320                        self.db[&nfp].file.path.display(),
321                        nfp,
322                        fp
323                    );
324                    let info = self.db.swap_remove(&nfp).unwrap();
325                    self.db.insert(fp, info);
326                    let rp1 = self.reading_state_path(nfp);
327                    let rp2 = self.reading_state_path(fp);
328                    fs::rename(rp1, rp2).ok();
329                    let tp1 = self.thumbnail_preview_path(nfp);
330                    let tp2 = self.thumbnail_preview_path(fp);
331                    fs::rename(tp1, tp2).ok();
332                    if relat != self.db[&fp].file.path {
333                        debug!(
334                            "Update path for {}: {} → {}.",
335                            fp,
336                            self.db[&fp].file.path.display(),
337                            relat.display()
338                        );
339                        self.paths.remove(&self.db[&fp].file.path);
340                        self.paths.insert(relat.to_path_buf(), fp);
341                        self.db[&fp].file.path = relat.to_path_buf();
342                    }
343                // We found a new file: add it to the db.
344                } else {
345                    let kind = file_kind(&path).unwrap_or_default();
346                    if !settings.allowed_kinds.contains(&kind) {
347                        continue;
348                    }
349                    info!("Add new entry: {}, {}.", fp, relat.display());
350                    let size = md.len();
351                    let file = FileInfo {
352                        path: relat.to_path_buf(),
353                        kind,
354                        size,
355                    };
356                    let mut info = Info {
357                        file,
358                        ..Default::default()
359                    };
360                    if settings.metadata_kinds.contains(&info.file.kind) {
361                        extract_metadata_from_document(&self.home, &mut info);
362                    }
363                    self.db.insert(fp, info);
364                    self.paths.insert(relat.to_path_buf(), fp);
365                }
366
367                self.has_db_changed = true;
368            }
369        }
370
371        let home = &self.home;
372        let len = self.db.len();
373
374        self.db.retain(|fp, info| {
375            let path = home.join(&info.file.path);
376            if path.exists() {
377                true
378            } else {
379                info!("Remove entry: {}, {}.", fp, info.file.path.display());
380                false
381            }
382        });
383
384        if self.db.len() != len {
385            self.has_db_changed = true;
386            let db = &self.db;
387            self.paths.retain(|_, fp| db.contains_key(fp));
388            self.modified_reading_states
389                .retain(|fp| db.contains_key(fp));
390
391            let reading_states_dir = home.join(READING_STATES_DIRNAME);
392            let thumbnail_previews_dir = home.join(THUMBNAIL_PREVIEWS_DIRNAME);
393            for entry in fs::read_dir(&reading_states_dir)
394                .unwrap()
395                .chain(fs::read_dir(&thumbnail_previews_dir).unwrap())
396            {
397                if entry.is_err() {
398                    continue;
399                }
400                let entry = entry.unwrap();
401                if let Some(fp) = entry
402                    .path()
403                    .file_stem()
404                    .and_then(|v| v.to_str())
405                    .and_then(|v| Fp::from_str(v).ok())
406                {
407                    if !self.db.contains_key(&fp) {
408                        fs::remove_file(entry.path()).ok();
409                    }
410                }
411            }
412        }
413    }
414
415    pub fn add_document(&mut self, info: Info) {
416        let path = self.home.join(&info.file.path);
417        let md = path.metadata().unwrap();
418        let fp = md.fingerprint(self.fat32_epoch).unwrap();
419
420        if info.reader.is_some() {
421            self.modified_reading_states.insert(fp);
422        }
423
424        if self.mode == LibraryMode::Database {
425            self.paths.insert(info.file.path.clone(), fp);
426            self.db.insert(fp, info);
427            self.has_db_changed = true;
428        } else {
429            if let Some(reader_info) = info.reader {
430                self.reading_states.insert(fp, reader_info);
431            }
432        }
433    }
434
435    pub fn rename<P: AsRef<Path>>(&mut self, path: P, file_name: &str) -> Result<(), Error> {
436        let src = self.home.join(path.as_ref());
437
438        let fp = self
439            .paths
440            .remove(path.as_ref())
441            .or_else(|| {
442                src.metadata()
443                    .ok()
444                    .and_then(|md| md.fingerprint(self.fat32_epoch).ok())
445            })
446            .ok_or_else(|| format_err!("can't get fingerprint of {}", path.as_ref().display()))?;
447
448        let mut dest = src.clone();
449        dest.set_file_name(file_name);
450        fs::rename(&src, &dest)?;
451
452        if self.mode == LibraryMode::Database {
453            let new_path = dest.strip_prefix(&self.home)?;
454            self.paths.insert(new_path.to_path_buf(), fp);
455            if let Some(info) = self.db.get_mut(&fp) {
456                info.file.path = new_path.to_path_buf();
457                self.has_db_changed = true;
458            }
459        }
460
461        Ok(())
462    }
463
464    pub fn remove<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
465        let full_path = self.home.join(path.as_ref());
466
467        let fp = self
468            .paths
469            .get(path.as_ref())
470            .cloned()
471            .or_else(|| {
472                full_path
473                    .metadata()
474                    .ok()
475                    .and_then(|md| md.fingerprint(self.fat32_epoch).ok())
476            })
477            .ok_or_else(|| format_err!("can't get fingerprint of {}", path.as_ref().display()))?;
478
479        if full_path.exists() {
480            fs::remove_file(&full_path)?;
481        }
482
483        if let Some(parent) = full_path.parent() {
484            if parent != self.home {
485                fs::remove_dir(parent).ok();
486            }
487        }
488
489        let rsp = self.reading_state_path(fp);
490        if rsp.exists() {
491            fs::remove_file(rsp)?;
492        }
493
494        let tpp = self.thumbnail_preview_path(fp);
495        if tpp.exists() {
496            fs::remove_file(tpp)?;
497        }
498
499        if self.mode == LibraryMode::Database {
500            self.paths.remove(path.as_ref());
501            if self.db.shift_remove(&fp).is_some() {
502                self.has_db_changed = true;
503            }
504        } else {
505            self.reading_states.remove(&fp);
506        }
507
508        self.modified_reading_states.remove(&fp);
509
510        Ok(())
511    }
512
513    pub fn copy_to<P: AsRef<Path>>(&mut self, path: P, other: &mut Library) -> Result<(), Error> {
514        let src = self.home.join(path.as_ref());
515
516        if !src.exists() {
517            return Err(format_err!(
518                "can't copy non-existing file {}",
519                path.as_ref().display()
520            ));
521        }
522
523        let md = src.metadata()?;
524        let fp = self
525            .paths
526            .get(path.as_ref())
527            .cloned()
528            .or_else(|| md.fingerprint(self.fat32_epoch).ok())
529            .ok_or_else(|| format_err!("can't get fingerprint of {}", path.as_ref().display()))?;
530
531        let mut dest = other.home.join(path.as_ref());
532        if let Some(parent) = dest.parent() {
533            fs::create_dir_all(parent)?;
534        }
535
536        if dest.exists() {
537            let prefix = Local::now().format("%Y%m%d_%H%M%S ");
538            let name = dest
539                .file_name()
540                .and_then(|name| name.to_str())
541                .map(|name| prefix.to_string() + name)
542                .ok_or_else(|| format_err!("can't compute new name for {}", dest.display()))?;
543            dest.set_file_name(name);
544        }
545
546        fs::copy(&src, &dest)?;
547        {
548            let fdest = File::open(&dest)?;
549            fdest.set_modified(md.modified()?)?;
550        }
551
552        let rsp_src = self.reading_state_path(fp);
553        if rsp_src.exists() {
554            let rsp_dest = other.reading_state_path(fp);
555            fs::copy(&rsp_src, &rsp_dest)?;
556        }
557
558        let tpp_src = self.thumbnail_preview_path(fp);
559        if tpp_src.exists() {
560            let tpp_dest = other.thumbnail_preview_path(fp);
561            fs::copy(&tpp_src, &tpp_dest)?;
562        }
563
564        if other.mode == LibraryMode::Database {
565            let info = self.db.get(&fp).cloned().or_else(|| {
566                self.reading_states
567                    .get(&fp)
568                    .cloned()
569                    .map(|reader_info| Info {
570                        file: FileInfo {
571                            size: md.len(),
572                            kind: file_kind(&dest).unwrap_or_default(),
573                            ..Default::default()
574                        },
575                        reader: Some(reader_info),
576                        ..Default::default()
577                    })
578            });
579            if let Some(mut info) = info {
580                let dest_path = dest.strip_prefix(&other.home)?;
581                info.file.path = dest_path.to_path_buf();
582                other.db.insert(fp, info);
583                other.paths.insert(dest_path.to_path_buf(), fp);
584                other.has_db_changed = true;
585            }
586        } else {
587            let reader_info = self
588                .reading_states
589                .get(&fp)
590                .cloned()
591                .or_else(|| self.db.get(&fp).cloned().and_then(|info| info.reader));
592            if let Some(reader_info) = reader_info {
593                other.reading_states.insert(fp, reader_info);
594            }
595        }
596
597        other.modified_reading_states.insert(fp);
598
599        Ok(())
600    }
601
602    pub fn move_to<P: AsRef<Path>>(&mut self, path: P, other: &mut Library) -> Result<(), Error> {
603        let src = self.home.join(path.as_ref());
604
605        if !src.exists() {
606            return Err(format_err!(
607                "can't move non-existing file {}",
608                path.as_ref().display()
609            ));
610        }
611
612        let md = src.metadata()?;
613        let fp = self
614            .paths
615            .get(path.as_ref())
616            .cloned()
617            .or_else(|| md.fingerprint(self.fat32_epoch).ok())
618            .ok_or_else(|| format_err!("can't get fingerprint of {}", path.as_ref().display()))?;
619
620        let src = self.home.join(path.as_ref());
621        let mut dest = other.home.join(path.as_ref());
622        if let Some(parent) = dest.parent() {
623            fs::create_dir_all(parent)?;
624        }
625
626        if dest.exists() {
627            let prefix = Local::now().format("%Y%m%d_%H%M%S ");
628            let name = dest
629                .file_name()
630                .and_then(|name| name.to_str())
631                .map(|name| prefix.to_string() + name)
632                .ok_or_else(|| format_err!("can't compute new name for {}", dest.display()))?;
633            dest.set_file_name(name);
634        }
635
636        fs::rename(&src, &dest)?;
637
638        let rsp_src = self.reading_state_path(fp);
639        if rsp_src.exists() {
640            let rsp_dest = other.reading_state_path(fp);
641            fs::rename(&rsp_src, &rsp_dest)?;
642        }
643
644        let tpp_src = self.thumbnail_preview_path(fp);
645        if tpp_src.exists() {
646            let tpp_dest = other.thumbnail_preview_path(fp);
647            fs::rename(&tpp_src, &tpp_dest)?;
648        }
649
650        if other.mode == LibraryMode::Database {
651            let info = self.db.shift_remove(&fp).or_else(|| {
652                self.reading_states.remove(&fp).map(|reader_info| Info {
653                    file: FileInfo {
654                        size: md.len(),
655                        kind: file_kind(&dest).unwrap_or_default(),
656                        ..Default::default()
657                    },
658                    reader: Some(reader_info),
659                    ..Default::default()
660                })
661            });
662            if let Some(mut info) = info {
663                let dest_path = dest.strip_prefix(&other.home)?;
664                info.file.path = dest_path.to_path_buf();
665                other.db.insert(fp, info);
666                self.paths.remove(path.as_ref());
667                other.paths.insert(dest_path.to_path_buf(), fp);
668                self.has_db_changed = true;
669                other.has_db_changed = true;
670            }
671        } else {
672            let reader_info = self
673                .reading_states
674                .remove(&fp)
675                .or_else(|| self.db.shift_remove(&fp).and_then(|info| info.reader));
676            if let Some(reader_info) = reader_info {
677                other.reading_states.insert(fp, reader_info);
678            }
679        }
680
681        if self.modified_reading_states.remove(&fp) {
682            other.modified_reading_states.insert(fp);
683        }
684
685        Ok(())
686    }
687
688    pub fn clean_up(&mut self) {
689        if self.mode == LibraryMode::Database {
690            return;
691        }
692
693        let fps = WalkDir::new(&self.home)
694            .min_depth(1)
695            .into_iter()
696            .filter_map(|entry| entry.ok())
697            .filter_map(|entry| {
698                if entry.file_type().is_dir() {
699                    None
700                } else {
701                    Some(
702                        entry
703                            .metadata()
704                            .unwrap()
705                            .fingerprint(self.fat32_epoch)
706                            .unwrap(),
707                    )
708                }
709            })
710            .collect::<FxHashSet<Fp>>();
711
712        self.reading_states.retain(|fp, _| {
713            if fps.contains(fp) {
714                true
715            } else {
716                info!("Remove reading state for {}.", fp);
717                false
718            }
719        });
720        self.modified_reading_states.retain(|fp| fps.contains(fp));
721
722        let reading_states_dir = self.home.join(READING_STATES_DIRNAME);
723        let thumbnail_previews_dir = self.home.join(THUMBNAIL_PREVIEWS_DIRNAME);
724        for entry in fs::read_dir(&reading_states_dir)
725            .unwrap()
726            .chain(fs::read_dir(&thumbnail_previews_dir).unwrap())
727        {
728            if entry.is_err() {
729                continue;
730            }
731            let entry = entry.unwrap();
732            if let Some(fp) = entry
733                .path()
734                .file_stem()
735                .and_then(|v| v.to_str())
736                .and_then(|v| Fp::from_str(v).ok())
737            {
738                if !fps.contains(&fp) {
739                    fs::remove_file(entry.path()).ok();
740                }
741            }
742        }
743    }
744
745    pub fn sort(&mut self, sort_method: SortMethod, reverse_order: bool) {
746        self.sort_method = sort_method;
747        self.reverse_order = reverse_order;
748
749        if self.mode == LibraryMode::Filesystem {
750            return;
751        }
752
753        let sort_fn = sorter(sort_method);
754
755        if reverse_order {
756            self.db.sort_by(|_, a, _, b| sort_fn(a, b).reverse());
757        } else {
758            self.db.sort_by(|_, a, _, b| sort_fn(a, b));
759        }
760    }
761
762    pub fn apply<F>(&mut self, f: F)
763    where
764        F: Fn(&Path, &mut Info),
765    {
766        if self.mode == LibraryMode::Filesystem {
767            return;
768        }
769
770        for (_, info) in &mut self.db {
771            f(&self.home, info);
772        }
773
774        self.has_db_changed = true;
775    }
776
777    pub fn sync_reader_info<P: AsRef<Path>>(&mut self, path: P, reader: &ReaderInfo) {
778        let fp = self.paths.get(path.as_ref()).cloned().unwrap_or_else(|| {
779            self.home
780                .join(path.as_ref())
781                .metadata()
782                .unwrap()
783                .fingerprint(self.fat32_epoch)
784                .unwrap()
785        });
786        self.modified_reading_states.insert(fp);
787        match self.mode {
788            LibraryMode::Database => {
789                if let Some(info) = self.db.get_mut(&fp) {
790                    info.reader = Some(reader.clone());
791                }
792            }
793            LibraryMode::Filesystem => {
794                self.reading_states.insert(fp, reader.clone());
795            }
796        }
797    }
798
799    pub fn thumbnail_preview<P: AsRef<Path>>(&self, path: P) -> PathBuf {
800        if path.as_ref().starts_with(THUMBNAIL_PREVIEWS_DIRNAME) {
801            self.home.join(path.as_ref())
802        } else {
803            let fp = self.paths.get(path.as_ref()).cloned().unwrap_or_else(|| {
804                self.home
805                    .join(path.as_ref())
806                    .metadata()
807                    .unwrap()
808                    .fingerprint(self.fat32_epoch)
809                    .unwrap()
810            });
811            self.thumbnail_preview_path(fp)
812        }
813    }
814
815    pub fn set_status<P: AsRef<Path>>(&mut self, path: P, status: SimpleStatus) {
816        let fp = self.paths.get(path.as_ref()).cloned().unwrap_or_else(|| {
817            self.home
818                .join(path.as_ref())
819                .metadata()
820                .unwrap()
821                .fingerprint(self.fat32_epoch)
822                .unwrap()
823        });
824        if self.mode == LibraryMode::Database {
825            match status {
826                SimpleStatus::New => {
827                    if let Some(info) = self.db.get_mut(&fp) {
828                        info.reader = None;
829                    }
830                    fs::remove_file(self.reading_state_path(fp)).ok();
831                    self.modified_reading_states.remove(&fp);
832                }
833                SimpleStatus::Reading | SimpleStatus::Finished => {
834                    if let Some(info) = self.db.get_mut(&fp) {
835                        let reader_info = info.reader.get_or_insert_with(ReaderInfo::default);
836                        reader_info.finished = status == SimpleStatus::Finished;
837                        self.modified_reading_states.insert(fp);
838                    }
839                }
840            }
841        } else {
842            match status {
843                SimpleStatus::New => {
844                    self.reading_states.remove(&fp);
845                    fs::remove_file(self.reading_state_path(fp)).ok();
846                    self.modified_reading_states.remove(&fp);
847                }
848                SimpleStatus::Reading | SimpleStatus::Finished => {
849                    let reader_info = self
850                        .reading_states
851                        .entry(fp)
852                        .or_insert_with(ReaderInfo::default);
853                    reader_info.finished = status == SimpleStatus::Finished;
854                    self.modified_reading_states.insert(fp);
855                }
856            }
857        }
858    }
859
860    pub fn reload(&mut self) {
861        if self.mode == LibraryMode::Database {
862            let path = self.home.join(METADATA_FILENAME);
863
864            match load_json(&path) {
865                Err(e) => {
866                    error!("Can't reload database: {:#}.", e);
867                    return;
868                }
869                Ok(v) => {
870                    self.db = v;
871                    self.has_db_changed = false;
872                }
873            }
874        }
875
876        let path = self.home.join(READING_STATES_DIRNAME);
877
878        self.modified_reading_states.clear();
879        if self.mode == LibraryMode::Filesystem {
880            self.reading_states.clear();
881        }
882
883        for entry in fs::read_dir(&path).unwrap() {
884            let entry = entry.unwrap();
885            let path = entry.path();
886            if let Some(fp) = path
887                .file_stem()
888                .and_then(|v| v.to_str())
889                .and_then(|v| Fp::from_str(v).ok())
890            {
891                if let Ok(reader_info) =
892                    load_json(path).map_err(|e| error!("Can't load reading state: {:#}.", e))
893                {
894                    if self.mode == LibraryMode::Database {
895                        if let Some(info) = self.db.get_mut(&fp) {
896                            info.reader = Some(reader_info);
897                        } else {
898                            warn!("Unknown fingerprint: {}.", fp);
899                        }
900                    } else {
901                        self.reading_states.insert(fp, reader_info);
902                    }
903                }
904            }
905        }
906
907        if self.mode == LibraryMode::Database {
908            self.paths = self
909                .db
910                .iter()
911                .map(|(fp, info)| (info.file.path.clone(), *fp))
912                .collect();
913        }
914    }
915
916    pub fn flush(&mut self) {
917        for fp in &self.modified_reading_states {
918            let reader_info = if self.mode == LibraryMode::Database {
919                self.db.get(fp).and_then(|info| info.reader.as_ref())
920            } else {
921                self.reading_states.get(fp)
922            };
923            if let Some(reader_info) = reader_info {
924                save_json(reader_info, self.reading_state_path(*fp))
925                    .map_err(|e| error!("Can't save reading state: {:#}.", e))
926                    .ok();
927            }
928        }
929
930        self.modified_reading_states.clear();
931
932        if self.has_db_changed {
933            save_json(&self.db, self.home.join(METADATA_FILENAME))
934                .map_err(|e| error!("Can't save database: {:#}.", e))
935                .ok();
936            self.has_db_changed = false;
937        }
938    }
939
940    pub fn is_empty(&self) -> Option<bool> {
941        if self.mode == LibraryMode::Database {
942            Some(self.db.is_empty())
943        } else {
944            None
945        }
946    }
947
948    fn reading_state_path(&self, fp: Fp) -> PathBuf {
949        self.home
950            .join(READING_STATES_DIRNAME)
951            .join(format!("{}.json", fp))
952    }
953
954    fn thumbnail_preview_path(&self, fp: Fp) -> PathBuf {
955        self.home
956            .join(THUMBNAIL_PREVIEWS_DIRNAME)
957            .join(format!("{}.png", fp))
958    }
959}