use std::{ collections::{HashMap, HashSet}, path::{Path, PathBuf}, }; use rusqlite::{Connection, params_from_iter}; use serde::{Deserialize, Serialize}; use crate::{ Timer, ZipSource, db::{ DBFILE, Database, DbChild, YomitanProgress, YomitanRow, YomitanWriter, YomitanZipImportResult, }, error::CJDicError, tokenizer::{TokenizeSegment, Tokenizer}, }; #[derive(Serialize, Debug)] pub struct YomitanDictEntry { pub title: String, pub bundle_name: String, pub revision: String, pub lang: String, } #[derive(Serialize, Debug, Clone)] pub struct LoadYomitanZipDirResult { pub new_dicts: Vec, pub to_be_removed_dicts: Vec, } #[derive(Deserialize)] #[serde(untagged)] pub enum SqlParam { Null, Integer(i64), Real(f64), Text(String), Bool(bool), } pub struct AppService { db: Database, tokenizer: Tokenizer, } impl AppService { pub fn new( db_dir: impl AsRef, vibrato_dic: impl AsRef, ) -> Result { let tokenizer = Tokenizer::new(vibrato_dic)?; let db = Database::new(db_dir, tokenizer.clone())?; Ok(Self { db, tokenizer }) } pub fn tokenize(&self, text: String) -> Result, CJDicError> { Ok(self.tokenizer.tokenize(text)) } pub fn execute_sql( &self, sql: String, params: Vec, ) -> Result { let _timer = Timer::new(sql.to_string()); let conn = self.db.conn.lock()?; let converted_params: Vec<_> = params .into_iter() .map(|p| match p { SqlParam::Null => rusqlite::types::Value::Null, SqlParam::Integer(i) => rusqlite::types::Value::Integer(i), SqlParam::Real(n) => rusqlite::types::Value::Real(n), SqlParam::Text(s) => rusqlite::types::Value::Text(s), SqlParam::Bool(b) => rusqlite::types::Value::Integer(b as i64), }) .collect(); let mut stmt = conn.prepare(&sql)?; if stmt.column_count() == 0 { let affected = stmt.execute(params_from_iter(converted_params))?; return Ok(serde_json::Value::Number(affected.into())); } let column_names: Vec = stmt.column_names().iter().map(|s| s.to_string()).collect(); let rows = stmt.query_map(params_from_iter(converted_params), |row| { let mut obj = serde_json::Map::new(); for (i, name) in column_names.iter().enumerate() { let value = match row.get_ref(i)? { rusqlite::types::ValueRef::Null => serde_json::Value::Null, rusqlite::types::ValueRef::Integer(i) => serde_json::Value::Number(i.into()), // Boolean are also rendered as an integer. rusqlite::types::ValueRef::Real(n) => serde_json::Number::from_f64(n) .map(serde_json::Value::Number) .unwrap_or(serde_json::Value::Null), rusqlite::types::ValueRef::Text(t) => { serde_json::Value::String(String::from_utf8_lossy(t).into()) } rusqlite::types::ValueRef::Blob(_) => serde_json::Value::Null, }; obj.insert(name.clone(), value); } return Ok(serde_json::Value::Object(obj)); })?; let result: Result, _> = rows.collect(); Ok(serde_json::Value::Array(result?)) } pub fn search_yomitan( &self, q_term: &str, q_reading: &str, limit: u32, offset: u32, ) -> Result, CJDicError> { Ok(self .db .yomitan()? .search_yomitan(q_term, q_reading, limit, offset)?) } pub fn get_yomitan_writer( &self, progress_callback: impl Fn(YomitanProgress), ) -> Result { let mut writer = YomitanWriter::new(self.db.dir.clone())?; writer.create_schema(&progress_callback)?; Ok(writer) } pub fn list_yomitan_dict(&self) -> Result, CJDicError> { let db_path = self.db.dir.join(DBFILE[DbChild::Yomitan]); let mut out = vec![]; if db_path.exists() { let conn = Connection::open(&db_path)?; let mut stmt = conn.prepare( " SELECT title, bundle_name, revision, lang FROM dictionaries WHERE bundle_name IS NOT NULL", )?; let rows = stmt.query_map([], |r| { Ok(YomitanDictEntry { title: r.get(0)?, bundle_name: r.get(1)?, revision: r.get(2)?, lang: r.get(3)?, }) })?; for r in rows { out.push(r?); } } Ok(out) } pub fn load_yomitan_zip_dir( &self, zip_dir: Vec, asset_dir: &PathBuf, lang: &str, load_callback: LoadCallback, import_callback: ImportCallback, ) -> Result where Z: ZipSource, LoadCallback: Fn(LoadYomitanZipDirResult), ImportCallback: Fn(YomitanProgress), { let mut dir_zip_map: HashMap = HashMap::new(); let mut zip_dir_ord: HashMap = HashMap::new(); for (i, entry) in zip_dir.iter().enumerate() { let filename = entry.file_name(); dir_zip_map.insert(filename.to_string(), entry); zip_dir_ord.insert(filename.to_string(), i); } let mut new_dicts: Vec = vec![]; let mut to_be_removed_dicts: Vec = vec![]; { let mut writer = YomitanWriter::new(self.db.dir.clone())?; writer.create_schema(&import_callback)?; } let db_path = self.db.dir.join(DBFILE[DbChild::Yomitan]); if db_path.exists() { let conn = Connection::open(&db_path)?; let mut stmt = conn.prepare( " SELECT DISTINCT bundle_name FROM dictionaries WHERE lang = ?1 AND asset_count IS NOT NULL ", )?; let mut rows = stmt.query([lang])?; let mut db_zip_list: HashSet = HashSet::new(); while let Some(row) = rows.next()? { let b_name: String = row.get(0)?; db_zip_list.insert(b_name); } for z in dir_zip_map.keys() { if !db_zip_list.contains(&z.to_string()) { new_dicts.push(z.to_string()); } } for z in db_zip_list.iter() { if !dir_zip_map.contains_key(z) { to_be_removed_dicts.push(z.to_string()); } } } else { for z in dir_zip_map.keys() { new_dicts.push(z.to_string()); } } // Import ordering new_dicts.sort_by_key(|s| zip_dir_ord.get(s)); to_be_removed_dicts.sort_by_key(|s| zip_dir_ord.get(s)); load_callback(LoadYomitanZipDirResult { new_dicts: new_dicts.clone(), to_be_removed_dicts: to_be_removed_dicts.clone(), }); { let conn = Connection::open(&db_path)?; for z in to_be_removed_dicts.clone().iter() { conn.execute( "DELETE FROM dictionaries WHERE bundle_name = ?1 AND lang = ?2", [z, lang], )?; } } { let mut writer = YomitanWriter::new(self.db.dir.clone())?; for z in new_dicts.clone().iter() { if let Some(zip_file) = dir_zip_map.get(z) { Self::import_yomitan_zip_file( &mut writer, *zip_file, &asset_dir, lang, &import_callback, )?; } } } { let mut writer = YomitanWriter::new(self.db.dir.clone())?; if new_dicts.len() + to_be_removed_dicts.len() > 0 || !writer.check_search_db()? { writer.make_search_db(self.tokenizer.clone(), &import_callback)?; } } Ok(LoadYomitanZipDirResult { new_dicts, to_be_removed_dicts, }) } pub fn import_yomitan_zip_file( writer: &mut YomitanWriter, // Reuse existing connection zip_file: &Z, asset_dir: &PathBuf, lang: &str, progress_callback: Callback, ) -> Result where Z: ZipSource, Callback: Fn(YomitanProgress), { Ok(writer.import_dictionary_zip_file(zip_file, asset_dir, lang, progress_callback)?) } pub fn remove_yomitan_dictionary( writer: &mut YomitanWriter, bundle_name: &str, lang: &str, ) -> Result<(), CJDicError> { Ok(writer.drop_dictionary(bundle_name, lang)?) } }