use deunicode::deunicode; use rusqlite; use std::fmt; use std::fs; use std::path::PathBuf; use super::deser::ScryfallCard; use super::utils::get_local_cache_folder; use super::utils::{create_cache_folder, get_local_data_folder, SQLITE_FILENAME}; fn get_local_data_sqlite_file() -> PathBuf { let mut folder = get_local_data_folder(); folder.push(SQLITE_FILENAME); folder } pub fn get_all_card_names() -> Vec { let sqlite_file = get_local_data_sqlite_file(); let conn = rusqlite::Connection::open(sqlite_file).unwrap(); let mut stmt = conn.prepare("SELECT name FROM cards;").unwrap(); let mut rows = stmt.query([]).unwrap(); let mut card_names = Vec::new(); while let Some(row) = rows.next().unwrap() { card_names.push(row.get(0).unwrap()); } card_names } pub fn get_all_lowercase_card_names() -> Vec { let sqlite_file = get_local_data_sqlite_file(); let conn = rusqlite::Connection::open(sqlite_file).unwrap(); let mut stmt = conn.prepare("SELECT lowercase_name FROM cards;").unwrap(); let mut rows = stmt.query([]).unwrap(); let mut card_names = Vec::new(); while let Some(row) = rows.next().unwrap() { card_names.push(row.get(0).unwrap()); } card_names } pub fn get_all_mtg_words() -> Vec { let sqlite_file = get_local_data_sqlite_file(); let conn = rusqlite::Connection::open(sqlite_file).unwrap(); let mut stmt = conn.prepare("SELECT word FROM mtg_words;").unwrap(); let mut rows = stmt.query([]).unwrap(); let mut card_names = Vec::new(); while let Some(row) = rows.next().unwrap() { card_names.push(row.get(0).unwrap()); } card_names } // unsure if this should be in this file... impl fmt::Display for DbCard { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.mana_cost { Some(mc) => writeln!(f, "{}\t{}", self.name, mc)?, None => writeln!(f, "{}", self.name)?, } writeln!(f, "{}", self.type_line)?; match &self.oracle_text { Some(ot) => writeln!(f, "{}", ot)?, None => (), } match &self.power_toughness { Some(pt) => writeln!(f, "{}", pt)?, None => (), } match &self.loyalty { Some(l) => writeln!(f, "Starting Loyalty: {}", l)?, None => (), } writeln!(f, "Scryfall URI: {}", self.scryfall_uri)?; Ok(()) } } #[derive(Debug)] pub struct DbCard { pub name: String, pub lowercase_name: String, pub type_line: String, pub oracle_text: Option, pub power_toughness: Option, pub loyalty: Option, pub mana_cost: Option, pub scryfall_uri: String, } pub enum GetNameType { Name, LowercaseName, } pub fn get_card_by_name(name: &str, name_type: GetNameType) -> Option { let sqlite_file = get_local_data_sqlite_file(); let conn = rusqlite::Connection::open(sqlite_file).unwrap(); let sql = match name_type { GetNameType::Name => { "SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri FROM cards WHERE name = (?1)" } GetNameType::LowercaseName => { "SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri FROM cards WHERE lowercase_name = (?1)" } }; let mut stmt = conn.prepare(sql).unwrap(); let mut rows = stmt.query([name]).unwrap(); match rows.next().unwrap() { Some(row) => Some(DbCard { name: row.get(0).unwrap(), lowercase_name: row.get(1).unwrap(), type_line: row.get(2).unwrap(), oracle_text: row.get(3).unwrap(), power_toughness: row.get(4).unwrap(), loyalty: row.get(5).unwrap(), mana_cost: row.get(6).unwrap(), scryfall_uri: row.get(7).unwrap(), }), None => None, } } pub fn find_matching_cards_scryfall_style(search_strings: &Vec) -> Vec { assert!(!search_strings.is_empty()); let sqlite_file = get_local_data_sqlite_file(); let conn = rusqlite::Connection::open(sqlite_file).unwrap(); let mut percentaged_string = Vec::new(); // I know that .clone fixes my problem - I'm not sure why I need to though for mut search_string in search_strings.clone() { search_string.push('%'); search_string.insert(0, '%'); percentaged_string.push(search_string); } let mut sql: String = "SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri FROM cards WHERE".into(); for i in 0..search_strings.len() { sql.push_str(&format!(" lowercase_name LIKE (?{}) AND", i + 1)); } sql.pop(); sql.pop(); sql.pop(); sql.pop(); let mut stmt = conn.prepare(&sql).unwrap(); stmt.query_map(rusqlite::params_from_iter(percentaged_string), |row| { Ok(DbCard { name: row.get(0).unwrap(), lowercase_name: row.get(1).unwrap(), type_line: row.get(2).unwrap(), oracle_text: row.get(3).unwrap(), power_toughness: row.get(4).unwrap(), loyalty: row.get(5).unwrap(), mana_cost: row.get(6).unwrap(), scryfall_uri: row.get(7).unwrap(), }) }) .unwrap() .filter_map(|res| res.ok()) .collect() } pub fn find_matching_cards(name: &str) -> Vec { let sqlite_file = get_local_data_sqlite_file(); let conn = rusqlite::Connection::open(sqlite_file).unwrap(); // There must be something better than this - although I don't think it's possible with a str let mut name = name.to_string(); name.push('%'); name.insert(0, '%'); let mut stmt = conn .prepare( "SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri FROM cards WHERE lowercase_name LIKE (?1)", ) .unwrap(); stmt.query_map([name], |row| { Ok(DbCard { name: row.get(0).unwrap(), lowercase_name: row.get(1).unwrap(), type_line: row.get(2).unwrap(), oracle_text: row.get(3).unwrap(), power_toughness: row.get(4).unwrap(), loyalty: row.get(5).unwrap(), mana_cost: row.get(6).unwrap(), scryfall_uri: row.get(7).unwrap(), }) }) .unwrap() .filter_map(|res| res.ok()) .collect() } const CREATE_CARDS_TABLE_SQL: &str = " CREATE TABLE cards ( name TEXT NOT NULL UNIQUE, lowercase_name TEXT NOT NULL UNIQUE, type_line TEXT, oracle_text TEXT, power_toughness TEXT, loyalty TEXT, mana_cost TEXT, scryfall_uri TEXT NOT NULL UNIQUE )"; const CREATE_MAGIC_WORDS_TABLE_SQL: &str = " CREATE TABLE mtg_words ( word TEXT NOT NULL UNIQUE )"; // Will delete your current db pub fn init_db() -> bool { create_cache_folder(); let sqlite_file = get_local_data_sqlite_file(); println!("sqlite file location: {}", sqlite_file.display()); // TESTING println!("cache folder: {}", get_local_cache_folder().display()); let _res = fs::remove_file(&sqlite_file); // TODO actually check result for whether it was a permissions thing or something let connection = rusqlite::Connection::open(sqlite_file).unwrap(); connection.execute(&CREATE_CARDS_TABLE_SQL, ()).unwrap(); connection .execute(&CREATE_MAGIC_WORDS_TABLE_SQL, ()) .unwrap(); true } pub fn update_db_with_file(file: PathBuf) -> bool { let ac = fs::read_to_string(file).unwrap(); let ac: Vec = serde_json::from_str(&ac).unwrap(); let sqlite_file = get_local_data_sqlite_file(); let mut conn = rusqlite::Connection::open(sqlite_file).unwrap(); let tx = conn.transaction().unwrap(); for card in ac { for word in card.name.split_whitespace() { let word = deunicode(&word.to_lowercase()); let res = tx.execute( "INSERT INTO mtg_words (word) VALUES (?1) ON CONFLICT (word) DO NOTHING;", [word.replace(",", "")], ); } let lowercase_name = deunicode(&card.name.to_lowercase()); let power_toughness = match card.power { Some(p) => Some(format!("{}/{}", p, card.toughness.unwrap())), None => None, }; let oracle_text = match card.oracle_text { Some(ot) => ot, None => "".to_string(), }; let loyalty = match card.loyalty { Some(loy) => Some(loy), None => None, }; let mana_cost = match card.mana_cost { Some(mc) => Some(mc), None => None, }; let res = tx.execute( "INSERT INTO cards (name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", rusqlite::params![card.name, lowercase_name, card.type_line, oracle_text, power_toughness, loyalty, mana_cost, card.scryfall_uri], ); } tx.commit(); true }