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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
use std::{
collections::{BTreeMap, BTreeSet},
path::PathBuf,
process::Command,
string::FromUtf8Error,
};
#[derive(Debug)]
pub enum ModuleError {
FromUtf8Error(FromUtf8Error),
IoError(std::io::Error),
NoValidModule,
MalformedMetadata,
}
/// A module is defined by a name and the path to the module file.
/// The media_type parameter is set when loading modules to speed things up.
#[derive(Debug)]
struct Module {
mod_file: PathBuf,
media_type: String,
}
impl Module {
pub fn new(mod_file: PathBuf, media_type: String) -> Module {
Module {
mod_file,
media_type,
}
}
/// Check if a URL can be handled by the module
fn is_url_valid(&self, url: &str) -> Result<bool, ModuleError> {
match Command::new(&self.mod_file).args(&["check", url]).output() {
Ok(out) => match String::from_utf8(out.stdout) {
Ok(out) => {
if strip(out) == "1" {
Ok(true)
} else {
Ok(false)
}
}
Err(e) => Err(ModuleError::FromUtf8Error(e)),
},
Err(e) => Err(ModuleError::IoError(e)),
}
}
/// Given a URL, return the respective item code
fn derive_code(&self, url: &str) -> Result<String, ModuleError> {
match Command::new(&self.mod_file).args(&["code", url]).output() {
Ok(out) => match String::from_utf8(out.stdout) {
Ok(out) => Ok(out),
Err(e) => Err(ModuleError::FromUtf8Error(e)),
},
Err(e) => Err(ModuleError::IoError(e)),
}
}
/// Given an item code, return the respective URL
fn derive_url(&self, code: &str) -> Result<String, ModuleError> {
match Command::new(&self.mod_file).args(&["url", code]).output() {
Ok(out) => match String::from_utf8(out.stdout) {
Ok(out) => Ok(out),
Err(e) => Err(ModuleError::FromUtf8Error(e)),
},
Err(e) => Err(ModuleError::IoError(e)),
}
}
/// Get the title, authors, and genres of a book
fn get_metadata(&self, code: &str) -> Result<(String, String, String), ModuleError> {
match Command::new(&self.mod_file)
.args(&["metadata", code])
.output()
{
Ok(out) => match String::from_utf8(out.stdout) {
Ok(out) => {
let out = strip(out);
let mut lines = out.lines();
if let Some(title) = lines.next() {
if let Some(authors) = lines.next() {
if let Some(genres) = lines.next() {
return Ok((
title.to_string(),
authors.to_string(),
genres.to_string(),
));
} else {
Err(ModuleError::MalformedMetadata)
}
} else {
Err(ModuleError::MalformedMetadata)
}
} else {
Err(ModuleError::MalformedMetadata)
}
}
Err(e) => Err(ModuleError::FromUtf8Error(e)),
},
Err(e) => Err(ModuleError::IoError(e)),
}
}
/// Download a book given its code. Everything here is handled by the module.
fn download(&self, code: &str, dest_dir: &str) {
let pb = PathBuf::from(&dest_dir);
if !&pb.exists() {
match std::fs::create_dir_all(pb) {
Ok(()) => {
match Command::new(&self.mod_file).args(&["download", code, dest_dir]).output(){
Ok(output) => {
let out = String::from_utf8(output.stdout).expect("Error converting UTF8 output");
println!("{}", out);
}
Err(e) => println!("Error downloading item: {}", e)
}
}
Err(e) => {
println!("Error creating item data directory: {}", e);
}
}
}
}
}
/// Handles everything about modules.
pub struct ModuleHandler {
modules: BTreeMap<String, Module>,
}
impl ModuleHandler {
/// Load available modules from modules_path
pub fn new(modules_dir: &PathBuf) -> ModuleHandler {
let mut modules: BTreeMap<String, Module> = BTreeMap::new();
match std::fs::read_dir(&modules_dir) {
Ok(files) => {
// Look at all files in `modules_dir/`
for file in files {
match file {
Ok(file) => {
let pb: PathBuf = file.path();
// if execution of `module media` is successful
match Command::new(&pb).args(&["media"]).output() {
Ok(out) => {
// Get media type
let media: Option<String> = {
match String::from_utf8(out.stdout) {
Ok(media) => Some(media),
Err(_e) => None,
}
};
// Get module name ( = fie name)
let name: Option<&str> =
pb.as_path().file_name().and_then(std::ffi::OsStr::to_str);
// Add module if got both media type and name
if let (Some(name), Some(media)) = (name, media) {
modules.insert(name.to_string(), Module::new(pb, media));
}
}
Err(_e) => {}
}
}
Err(_e) => {}
}
}
}
Err(_e) => {}
};
ModuleHandler { modules }
}
/// Derives the appropriate module given a URL
pub fn derive_module(&self, url: &str) -> Result<&String, ModuleError> {
for (name, module) in self.modules.iter() {
match module.is_url_valid(url) {
Ok(is_valid) => {
if is_valid {
return Ok(name);
}
}
Err(e) => {
return Err(e);
}
}
}
Err(ModuleError::NoValidModule)
}
/// Given a module and URL, derive the corresponding code
pub fn derive_code(&self, module: &str, url: &str) -> Result<String, ModuleError> {
match self.modules.get(module) {
Some(module) => module.derive_code(url),
None => Err(ModuleError::NoValidModule),
}
}
/// Given a module and code, derive the corresponding URL
pub fn derive_url(&self, module: &str, code: &str) -> Result<String, ModuleError> {
match self.modules.get(module) {
Some(module) => module.derive_url(code),
None => Err(ModuleError::NoValidModule),
}
}
/// Get the media type handled by a module
pub fn get_media_type(&self, module: &str) -> Result<String, ModuleError> {
match self.modules.get(module) {
Some(m) => Ok(m.media_type.clone()),
None => Err(ModuleError::NoValidModule),
}
}
/// Get a set of available modules
pub fn list_modules(&self) -> BTreeSet<String> {
let mut result = BTreeSet::new();
for key in self.modules.keys() {
result.insert(key.clone());
}
result
}
/// Check if a module is available
pub fn has_module(&self, module: &str) -> bool {
self.modules.contains_key(module)
}
/// Given a module and code, get the title, authors, and genres of the corresponding item
pub fn get_metadata(
&self,
module: &str,
code: &str,
) -> Result<(String, String, String), ModuleError> {
match self.modules.get(module) {
Some(module) => module.get_metadata(code),
None => Err(ModuleError::NoValidModule),
}
}
/// Given a module and code, download item to the provided directory
pub fn download(&self, module: &str, code: &str, dest_dir: &PathBuf) -> Result<(), ModuleError> {
let dest_dir = &*dest_dir.clone().into_os_string().into_string().unwrap();
match self.modules.get(module) {
Some(m) => {
m.download(code, dest_dir);
Ok(())
}
None => Err(ModuleError::NoValidModule),
}
}
}
/// Strip tailing newlines and spaces from String
fn strip(mut string: String) -> String {
while string.ends_with("\n") || string.ends_with(" ") {
string.pop();
}
return string;
}