use chrono::{Datelike, Utc};
use epub::doc::EpubDoc;
use glob::glob;
use rayon::prelude::*;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
const CHARS_PER_PAGE: usize = 2000;
pub struct Book {
timestamp: u32,
read: bool,
title: String,
author: String,
series: String,
pages: usize,
path: String,
}
impl Book {
fn read_symbol(&self) -> String {
if self.read {
String::from("*")
} else {
String::from(" ")
}
}
}
pub enum SortBy {
Date,
Read,
Title,
Author,
Pages,
Series,
}
pub fn load_library(lib_db_path: &Path, epub_path: &Path, sort_by: &SortBy, reverse: bool) -> Result<Vec<Book>, &'static str> {
let epub_list = find_epub_files(epub_path);
let mut library = read_library_db(lib_db_path)?;
let mut new_books = epub_list
.par_iter()
.filter(|e| !library.iter().any(|b| &b.path == *e))
.map(|e| create_book_from_epub(e))
.collect::<Vec<Book>>();
library.append(&mut new_books);
library.retain(|e| epub_list.contains(&e.path));
sort_library(&mut library, sort_by, reverse);
Ok(library)
}
fn find_epub_files(epub_path: &Path) -> Vec<String> {
let glob_pattern = format!("{}{}", epub_path.to_str().unwrap(), "/**/*.epub");
glob(&glob_pattern)
.unwrap()
.filter(Result::is_ok)
.map(|p| p.unwrap().display().to_string())
.collect()
}
fn read_library_db(lib_file_path: &Path) -> Result<Vec<Book>, &'static str> {
if !lib_file_path.exists() {
return Ok(Vec::new());
}
let Ok(file) = File::open(lib_file_path) else {
return Err("error: unable to read library DB")
};
let library = BufReader::new(file)
.lines()
.skip(1) .filter(|l| l.is_ok() && !(l.as_ref().unwrap().is_empty() || l.as_ref().unwrap().starts_with('#')))
.map(|l| line_to_book(&l.unwrap()))
.collect();
Ok(library)
}
fn line_to_book(line: &str) -> Book {
let fields: Vec<&str> = line
.split(',')
.map(str::trim)
.collect();
Book {
timestamp: fields[0].parse().unwrap_or(999_999),
read: !fields[1].trim().is_empty(),
title: fields[2].to_string(),
author: fields[3].to_string(),
pages: fields[4].parse().unwrap_or(0),
series: fields[5].to_string(),
path: fields[6].to_string(),
}
}
fn create_book_from_epub(epub_path: &str) -> Book {
let epub_doc = EpubDoc::new(epub_path);
let (title, author, pages) = epub_doc.map_or_else(|_| (
String::from("Unknown title"),
String::from("Unknown author"),
0,
), |mut doc| (
doc.mdata("title").unwrap_or_else(|| String::from("Unknown title")),
doc.mdata("creator").unwrap_or_else(|| String::from("Unknown author")),
count_epub_pages(&mut doc),
));
Book {
timestamp: create_timestamp(&epub_path),
read: false,
title,
author,
series: String::new(),
pages,
path: epub_path.to_string(),
}
}
fn count_epub_pages(epub_doc: &mut EpubDoc<BufReader<File>>) -> usize {
let char_count = epub_doc.spine.clone().iter().fold(0_usize, |acc, r| {
acc + epub_doc
.get_resource_str(r)
.unwrap_or((String::new(), String::new()))
.0
.chars()
.filter(|s| *s != '\n')
.count()
});
char_count / CHARS_PER_PAGE
}
fn create_timestamp(path: &str) -> u32 {
let filename = path.split('/').last().unwrap();
if filename[0..6].parse::<u32>().is_ok() {
return filename[0..6].parse::<u32>().unwrap();
}
let now = Utc::now();
let date_str = format!("{:02}{:02}{:02}", now.year(), now.month(), now.day());
(date_str[2..]).parse().unwrap_or(999_999)
}
fn sort_library(library: &mut [Book], sort_by: &SortBy, reverse: bool) {
match sort_by {
SortBy::Date => library.par_sort_unstable_by_key(|b| b.timestamp),
SortBy::Read => library.par_sort_unstable_by_key(|b| b.read),
SortBy::Title => library.par_sort_unstable_by(|b1, b2| b1.title.cmp(&b2.title)),
SortBy::Author => library.par_sort_unstable_by(|b1, b2| b1.author.cmp(&b2.author)),
SortBy::Pages => library.par_sort_unstable_by_key(|b| b.pages),
SortBy::Series => library.par_sort_unstable_by(|b1, b2| b1.series.cmp(&b2.series)),
}
if reverse {
library.reverse();
}
}
pub fn write_library(library_string: &str, output_path: &Path) -> Result<(), &'static str> {
let Ok(mut output_file) = File::create(output_path) else {
return Err("error: unable to open library DB")
};
if write!(output_file, "{library_string}").is_err() {
return Err("error: unable to write library to DB");
}
Ok(())
}
pub fn library_to_string(library: &[Book]) -> String {
let mut lib_str = String::new();
let col_text = [
String::from("DATE"),
String::from("R"),
String::from("TITLE"),
String::from("AUTHOR"),
String::from("PG"),
String::from("SERIES"),
String::from("PATH"),
];
lib_str.push_str(string_to_csv(&col_text).as_str());
lib_str.push_str(
library
.iter()
.map(book_to_line)
.collect::<String>()
.as_str(),
);
lib_str.trim_end().to_string()
}
fn book_to_line(book: &Book) -> String {
let col_text = [
book.timestamp.to_string(),
book.read_symbol(),
book.title.clone(),
book.author.clone(),
book.pages.to_string(),
book.series.clone(),
book.path.clone(),
];
string_to_csv(&col_text)
}
fn string_to_csv(col_text: &[String]) -> String {
let mut tab_str = col_text
.par_iter()
.map(|s| s.replace(',', ""))
.collect::<Vec<_>>()
.join(",");
tab_str.push('\n');
tab_str
}