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 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 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 } 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 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 } 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}