mod pocketbook; use rusqlite::{named_params, Connection, Result, Transaction, NO_PARAMS}; use std::io::BufReader; use std::{collections::HashMap, fs::File}; use std::{error::Error, io::Read}; use xml::reader::{EventReader, ParserConfig, XmlEvent}; use zip::{read::ZipFile, ZipArchive}; fn get_root_file(mut container: ZipFile) -> Result, Box> { let mut buf = String::new(); container.read_to_string(&mut buf).unwrap(); // Get rid of the BOM mark, if any if buf.starts_with("\u{feff}") { buf = buf.strip_prefix("\u{feff}").unwrap().to_owned(); } let parser = EventReader::new(BufReader::new(buf.as_bytes())); for e in parser { match e { Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "rootfile" => { for attr in attributes { if attr.name.local_name == "full-path" { return Ok(Some(attr.value)); } } } Err(e) => { return Err(Box::new(e)); } _ => {} } } Ok(None) } struct Refine { role: String, file_as: String, } fn get_attribute_file_as(opf: ZipFile) -> Option { let parser = ParserConfig::new() .trim_whitespace(true) .ignore_comments(true) .coalesce_characters(true) .create_reader(opf); let mut is_epub3 = false; let mut creator_ids = Vec::new(); let mut refines_found = false; let mut role_found = false; let mut refine_entries = HashMap::new(); let mut curr_id = String::new(); for e in parser { match e { Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "package" => { for attr in attributes { if attr.name.local_name == "version" { if attr.value.starts_with("3") == true { is_epub3 = true; } } } } Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "creator" => { for attr in attributes { if attr.name.local_name == "file-as" { return Some(attr.value); } if is_epub3 && attr.name.local_name == "id" { creator_ids.push("#".to_owned() + attr.value.as_str()); } } } Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "meta" => { if attributes.iter().any(|attr| { attr.name.local_name == "refines" && creator_ids.contains(&attr.value) }) && attributes .iter() .any(|attr| attr.name.local_name == "property" && attr.value == "file-as") { refines_found = true; curr_id = attributes .iter() .find(|a| a.name.local_name == "refines") .unwrap() .value .clone(); } else if attributes.iter().any(|attr| { attr.name.local_name == "refines" && creator_ids.contains(&attr.value) }) && attributes .iter() .any(|attr| attr.name.local_name == "property" && attr.value == "role") { role_found = true; curr_id = attributes .iter() .find(|a| a.name.local_name == "refines") .unwrap() .value .clone(); } } Ok(XmlEvent::Characters(value)) => { if role_found == true { if value == "aut" { let entry = refine_entries.entry(curr_id.clone()).or_insert(Refine { role: "".to_string(), file_as: "".to_string(), }); entry.role = value; } role_found = false; } else if refines_found == true { let entry = refine_entries.entry(curr_id.clone()).or_insert(Refine { role: "".to_string(), file_as: "".to_string(), }); entry.file_as = value; refines_found = false; } } Ok(XmlEvent::StartElement { .. }) => { if refines_found == true { refines_found = false; } } Err(_e) => { break; } _ => {} } } if refine_entries.len() == 1 { return Some(refine_entries.values().next().unwrap().file_as.clone()); } else if refine_entries.len() >= 2 { return Some( refine_entries .values() .into_iter() .filter(|v| v.role == "aut") .map(|v| v.file_as.clone()) .collect::>() .join(" & "), ); } None } struct Creator { role: String, name: String, } fn get_attribute_creator(opf: ZipFile) -> Option { let parser = ParserConfig::new() .trim_whitespace(true) .ignore_comments(true) .coalesce_characters(true) .create_reader(opf); let mut is_epub3 = false; let mut creator_found = true; let mut creator_ids = Vec::new(); let mut role_found = false; let mut creator_entries = HashMap::new(); let mut epub2_creator_entries = Vec::new(); let mut curr_id = String::new(); for e in parser { match e { Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "package" => { for attr in attributes { if attr.name.local_name == "version" { if attr.value.starts_with("3") == true { is_epub3 = true; } } } } Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "creator" => { creator_found = true; if !is_epub3 { match attributes .iter() .find(|attr| attr.name.local_name == "role") { Some(attr) => { epub2_creator_entries.push(Creator { role: attr.value.clone(), name: "".to_string(), }); } None => { epub2_creator_entries.push(Creator { role: "aut".to_string(), name: "".to_string(), }); } } } for attr in attributes { if is_epub3 && attr.name.local_name == "id" { creator_ids.push("#".to_owned() + attr.value.as_str()); //creator_entries.insert(attr.value.clone(), Creator{role: "".to_string(), name: "".to_string()}); curr_id = "#".to_owned() + attr.value.as_str(); } } } Ok(XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "meta" => { if attributes.iter().any(|attr| { attr.name.local_name == "refines" && creator_ids.contains(&attr.value) }) && attributes .iter() .any(|attr| attr.name.local_name == "property" && attr.value == "role") { role_found = true; curr_id = attributes .iter() .find(|a| a.name.local_name == "refines") .unwrap() .value .clone(); } } Ok(XmlEvent::Characters(value)) => { if creator_found && is_epub3 == false { epub2_creator_entries.last_mut().unwrap().name = value.clone(); } else if creator_found && is_epub3 == true { let entry = creator_entries.entry(curr_id.clone()).or_insert(Creator { role: "".to_string(), name: "".to_string(), }); entry.name = value; creator_found = false; } else if role_found == true { let entry = creator_entries.entry(curr_id.clone()).or_insert(Creator { role: "".to_string(), name: "".to_string(), }); entry.role = value; role_found = false; } } Ok(XmlEvent::StartElement { .. }) => { if creator_found == true { creator_found = false; } } Err(e) => { println!("{}", e); break; } _ => {} } } if !is_epub3 && epub2_creator_entries.len() >= 1 { return Some( epub2_creator_entries .into_iter() .filter(|v| v.role == "aut") .map(|v| v.name.clone()) .collect::>() .join(", "), ); } else if creator_entries.len() >= 1 { return Some( creator_entries .values() .into_iter() .filter(|v| v.role == "aut") .map(|v| v.name.clone()) .collect::>() .join(", "), ); } None } struct BookEntry { id: i32, filepath: String, author: String, firstauthor: String, has_drm: bool, } fn get_epubs_from_database(tx: &Transaction) -> Vec { let mut book_entries = Vec::new(); let mut stmt = tx .prepare( r" SELECT books.id, folders.name, files.filename, books.firstauthor, books.author FROM books_impl books JOIN files ON books.id = files.book_id JOIN folders ON folders.id = files.folder_id WHERE files.storageid = 1 AND books.ext = 'epub' ORDER BY books.id", ) .unwrap(); let mut rows = stmt.query(NO_PARAMS).unwrap(); while let Some(row) = rows.next().unwrap() { let book_id: i32 = row.get(0).unwrap(); let prefix: String = row.get(1).unwrap(); let filename: String = row.get(2).unwrap(); let filepath = format!("{}/{}", prefix, filename); let firstauthor: String = row.get(3).unwrap(); let author: String = row.get(4).unwrap(); let has_drm = match prefix.as_str() { "/mnt/ext1/Digital Editions" => true, _ => false, }; let entry = BookEntry { id: book_id, filepath, firstauthor, author, has_drm, }; book_entries.push(entry); } book_entries } fn remove_ghost_books_from_db(tx: &Transaction) -> usize { let mut stmt = tx .prepare( r" DELETE FROM books_impl WHERE id IN ( SELECT books.id FROM books_impl books LEFT OUTER JOIN files ON books.id = files.book_id WHERE files.filename is NULL )", ) .unwrap(); let num = stmt.execute(NO_PARAMS).unwrap(); tx.execute( r"DELETE FROM books_settings WHERE bookid NOT IN ( SELECT id FROM books_impl )", NO_PARAMS, ) .unwrap(); tx.execute( r"DELETE FROM books_uids WHERE book_id NOT IN ( SELECT id FROM books_impl )", NO_PARAMS, ) .unwrap(); tx.execute( r"DELETE FROM bookshelfs_books WHERE bookid NOT IN ( SELECT id FROM books_impl )", NO_PARAMS, ) .unwrap(); tx.execute( r"DELETE FROM booktogenre WHERE bookid NOT IN ( SELECT id FROM books_impl )", NO_PARAMS, ) .unwrap(); tx.execute( r"DELETE FROM social WHERE bookid NOT IN ( SELECT id FROM books_impl )", NO_PARAMS, ) .unwrap(); num } struct Statistics { authors_fixed: i32, ghost_books_cleaned: usize, drm_skipped: usize, } fn fix_db_entries(tx: &Transaction, book_entries: &Vec) -> Statistics { let mut stat = Statistics { authors_fixed: 0, ghost_books_cleaned: 0, drm_skipped: 0, }; for entry in book_entries { if entry.has_drm { stat.drm_skipped = stat.drm_skipped + 1; continue; } let file = File::open(entry.filepath.as_str()); let file = match file { Err(_) => continue, Ok(file) => file, }; let mut archive = ZipArchive::new(BufReader::new(file)).unwrap(); let container = archive.by_name("META-INF/container.xml").unwrap(); if let Some(opf_file) = get_root_file(container).unwrap() { let opf = archive.by_name(opf_file.as_str()).unwrap(); // firstauthor… if let Some(file_as) = get_attribute_file_as(opf) { if !file_as.split(" & ").all(|s| entry.firstauthor.contains(s)) { let mut stmt = tx .prepare("UPDATE books_impl SET firstauthor = :file_as WHERE id = :book_id") .unwrap(); stmt.execute_named(named_params![":file_as": file_as, ":book_id": entry.id]) .unwrap(); stat.authors_fixed = stat.authors_fixed + 1; } } let opf = archive.by_name(opf_file.as_str()).unwrap(); // author… if let Some(creator) = get_attribute_creator(opf) { if !creator.split(", ").all(|s| entry.author.contains(s)) || creator.len() < entry.author.len() { let mut stmt = tx .prepare("UPDATE books_impl SET author = :creator WHERE id = :book_id") .unwrap(); stmt.execute_named(named_params![":creator": creator, ":book_id": entry.id]) .unwrap(); stat.authors_fixed = stat.authors_fixed + 1; } } } } // ghost books let num = remove_ghost_books_from_db(tx); stat.ghost_books_cleaned = num; stat } fn main() { if cfg!(target_arch = "arm") { let res = pocketbook::dialog( pocketbook::Icon::None, "PocketBook has sometimes problems parsing metadata.\n\ This app tries to fix some of these issues.\n\ (Note: The database file explore-3.db will be altered!)\n\ \n\ Please be patient - this might take a while.\n\ You will see a blank screen during the process.\n\ \n\ Proceed?", &["Cancel", "Yes"], ); if res == 1 { return; } } let mut conn = Connection::open("/mnt/ext1/system/explorer-3/explorer-3.db").unwrap(); conn.execute("PRAGMA foreign_keys = 0", NO_PARAMS).unwrap(); let tx = conn.transaction().unwrap(); let book_entries = get_epubs_from_database(&tx); let stat = fix_db_entries(&tx, &book_entries); tx.commit().unwrap(); if cfg!(target_arch = "arm") { if stat.authors_fixed == 0 { pocketbook::dialog( pocketbook::Icon::Info, "The database seems to be ok.\nNothing had to be fixed.", &["OK"], ); } else { pocketbook::dialog( pocketbook::Icon::Info, &format!( "Authors fixed: {}\n\ Books skipped (DRM): {}\n\ Books cleaned from DB: {}", &stat.authors_fixed, &stat.drm_skipped, &stat.ghost_books_cleaned ), &["OK"], ); } } else { println!( "Authors fixed: {}\n\ Books skipped (DRM): {}\n\ Books cleaned from DB: {}", &stat.authors_fixed, &stat.drm_skipped, &stat.ghost_books_cleaned ); } }