1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
use std::ops::{Deref, DerefMut};
use std::path::Path;
use lib::applebooks::ios::{ABIos, ABPlist};
use lib::applebooks::macos::{ABDatabase, ABMacos};
use lib::filter::filters;
use lib::models::annotation::Annotation;
use lib::models::book::Book;
use lib::models::entry::{Entries, Entry};
use crate::cli::app::Result;
/// A container struct for storing and managing [`Entry`]s.
#[derive(Debug, Default)]
pub struct Data(Entries);
impl Data {
/// Builds [`Book`]s and [`Annotation`]s from macOS's Apple Books databases, converts them to
/// [`Entry`]s and appends them to the data model.
///
/// # Arguments
///
/// * `path` - The path to a directory containing macOS's Apple Books databases.
///
/// See [`ABMacos`] for more information on how the databases directory should be structured.
///
/// # Errors
///
/// See [`ABMacos::extract_books()`] and [`ABMacos::extract_annotations()`] for information as
/// these are the only sources of possible errors.
pub fn init_macos(&mut self, path: &Path) -> Result<()> {
let books = ABMacos::extract_books(path)?;
let annotations = ABMacos::extract_annotations(path)?;
log::debug!(
"found {} book(s) in {}",
books.len(),
ABDatabase::Books.to_string()
);
log::debug!(
"found {} annotation(s) in {}",
annotations.len(),
ABDatabase::Annotations.to_string()
);
let entries = Self::build_entries(books, annotations);
self.0.extend(entries);
Ok(())
}
/// Builds [`Book`]s and [`Annotation`]s from iOS's Apple Books plists, converts them to
/// [`Entry`]s and appends them to the data model.
///
/// # Arguments
///
/// * `path` - The path to a directory containing iOS's Apple Books plists.
///
/// See [`ABIos`] for more information on how the plists directory should be structured.
///
/// # Errors
///
/// See [`ABIos::extract_books()`] and [`ABIos::extract_annotations()`] for information as these
/// are the only sources of possible errors.
pub fn init_ios(&mut self, path: &Path) -> Result<()> {
let books = ABIos::extract_books(path)?;
let annotations = ABIos::extract_annotations(path)?;
log::debug!(
"found {} book(s) in {}",
books.len(),
ABPlist::Books.to_string()
);
log::debug!(
"found {} annotation(s) in {}",
annotations.len(),
ABPlist::Annotations.to_string()
);
let entries = Self::build_entries(books, annotations);
self.0.extend(entries);
Ok(())
}
/// Converts [`Book`]s and [`Annotation`]s to [`Entry`]s, then sorts and filters them before
/// adding them to the data model.
fn build_entries(books: Vec<Book>, annotations: Vec<Annotation>) -> Entries {
// `Entry`s are created from `Book`s. Note that `book.metadata.id` is set as the key for
// each entry into the `Data`. This is later used to compare with each `Annotation` to
// determine if the `Annotation` belongs to a `Book` and therefore its `Entry`.
//
// See https://stackoverflow.com/q/69274529/16968574
let mut data: Entries = books
.into_iter()
.map(|book| (book.metadata.id.clone(), Entry::from(book)))
.collect();
// `Annotation`s are pushed onto an `Entry` based on their `book_id`.
for annotation in annotations {
if let Some(entry) = data.get_mut(&annotation.metadata.book_id) {
entry.annotations.push(annotation);
}
}
// Remove `Entry`s that have no `Annotation`s.
filters::contains_no_annotations(&mut data);
let count_books = Self::iter_books_inner(&data).count();
let count_annotations = Self::iter_annotations_inner(&data).count();
log::debug!("created {count_books} Book(s)",);
log::debug!("created {count_annotations} Annotation(s)",);
data
}
/// Returns the number of books within [`Data`].
pub fn count_books(&self) -> usize {
self.iter_books().count()
}
/// Returns the number of annotations within [`Data`].
pub fn count_annotations(&self) -> usize {
self.iter_annotations().count()
}
/// Returns an iterator over all [`Book`]s.
pub fn iter_books(&self) -> impl Iterator<Item = &Book> {
Self::iter_books_inner(&self.0)
}
/// Returns an iterator over all [`Annotation`]s.
pub fn iter_annotations(&self) -> impl Iterator<Item = &Annotation> {
Self::iter_annotations_inner(&self.0)
}
/// Returns an iterator over all [`Annotation`]s given an [`Entries`] type.
fn iter_annotations_inner(entries: &Entries) -> impl Iterator<Item = &Annotation> {
entries.values().flat_map(|entry| &entry.annotations)
}
/// Returns an iterator over all [`Book`]s given an [`Entries`] type.
fn iter_books_inner(entries: &Entries) -> impl Iterator<Item = &Book> {
entries.values().map(|entry| &entry.book)
}
}
impl Deref for Data {
type Target = Entries;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Data {
fn deref_mut(&mut self) -> &mut Entries {
&mut self.0
}
}