From 31dbe756707089ab5e5c428e7764671c3e88f9c3 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Mon, 29 Sep 2025 00:34:38 -0400 Subject: [PATCH 01/13] working on startup sequence for serialization --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app.rs | 10 +++++++ src/database/scryfall.rs | 61 +++++++++++++++++++++++++++------------- src/startup.rs | 4 +-- src/tui/core.rs | 2 +- 6 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f49f798..589f3a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -503,7 +503,7 @@ dependencies = [ [[package]] name = "decklist" -version = "0.4.0" +version = "0.5.0" dependencies = [ "arboard", "async-std", diff --git a/Cargo.toml b/Cargo.toml index b6a37a4..4832393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "decklist" -version = "0.4.0" +version = "0.5.0" edition = "2021" authors = ["Seth "] license = "Unlicense" diff --git a/src/app.rs b/src/app.rs index 175b489..b8324b1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -77,6 +77,7 @@ pub struct App { pub dl_done: bool, pub load_started: bool, pub load_done: bool, + pub short_done: bool, // shorter JSON w/ single copy and lowest price pub os: SupportedOS, pub directory_exist: bool, pub data_directory_exist: bool, @@ -184,6 +185,7 @@ impl Default for App { dl_done: false, load_started: false, load_done: false, + short_done: false, os: SupportedOS::default(), directory_exist: false, data_directory_exist: false, @@ -448,6 +450,14 @@ impl App { self.redraw = true; } } + // if a Scryfall database is loaded and the hashmap is done, serialize and save as a + // custom database JSON with a single copy of each card with the lowest price so the + // hashmap doesn't have to be filtered each time the program starts + if self.load_done { + // TODO: eventually make another condition for when the short database has been + // loaded instead of a Scryfall file + // + } if self.loading_collection { if let Ok(msg) = self.collection_channel.1.try_recv() { self.debug_string += &msg.debug; diff --git a/src/database/scryfall.rs b/src/database/scryfall.rs index ff558f2..34bbb8e 100644 --- a/src/database/scryfall.rs +++ b/src/database/scryfall.rs @@ -1,11 +1,18 @@ -use std::{collections::HashMap, error::Error, fs, path::PathBuf}; +use std::{ + collections::HashMap, + error::Error, + fs::{self, File}, + io::Write, + path::PathBuf, +}; +use chrono::Local; use diacritics::remove_diacritics; use serde::{Deserialize, Serialize}; /// structure for all Scryfall card data for a unique card // TODO: map to JSON field names manually? or rename? -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub struct ScryfallCard { pub object: ScryfallObject, pub id: String, @@ -130,7 +137,7 @@ impl ScryfallCard { } /// represents different kinds of Scryfall objects -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum ScryfallObject { #[serde(rename = "card")] Card, @@ -139,7 +146,7 @@ pub enum ScryfallObject { } /// different languages for MTG printings -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum Languages { #[serde(rename = "en")] English, @@ -180,7 +187,7 @@ pub enum Languages { } /// different card layout options -#[derive(Deserialize, Clone, PartialEq)] +#[derive(Deserialize, Clone, PartialEq, Serialize)] pub enum CardLayouts { #[serde(rename = "normal")] Normal, @@ -233,7 +240,7 @@ pub enum CardLayouts { } /// scryfall image statuses -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum ImageStatus { #[serde(rename = "highres_scan")] HighRes, @@ -246,7 +253,7 @@ pub enum ImageStatus { } /// struct for all Scryfall image uris -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, Serialize)] pub struct ImageUris { pub small: String, pub normal: String, @@ -257,7 +264,7 @@ pub struct ImageUris { } /// MtG card types -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum CardTypes { Artifact, Creature, @@ -267,7 +274,7 @@ pub enum CardTypes { Land, } -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum MtGColors { #[serde(rename = "W")] White, @@ -282,7 +289,7 @@ pub enum MtGColors { } /// MtG card keywords -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum MtGKeyWords { Trample, Haste, @@ -596,7 +603,7 @@ pub enum MtGKeyWords { } /// card legality options for a specific format -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum Legality { #[serde(rename = "legal")] Legal, @@ -609,7 +616,7 @@ pub enum Legality { } /// contains legal status of card in every Scryfall format -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub struct Legalities { pub standard: Legality, pub future: Legality, @@ -663,7 +670,7 @@ impl Default for Legalities { } /// different game formats -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum GameFormat { #[serde(rename = "paper")] Paper, @@ -678,7 +685,7 @@ pub enum GameFormat { } /// different kinds of finishes recognized by Scryfall -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum ScryfallFinishes { #[serde(rename = "foil")] Foil, @@ -689,7 +696,7 @@ pub enum ScryfallFinishes { } /// Scryfall set classifications -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum ScryfallSetType { #[serde(rename = "core")] Core, @@ -740,7 +747,7 @@ pub enum ScryfallSetType { } /// card rarities -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum MtGRarity { #[serde(rename = "common")] Common, @@ -757,7 +764,7 @@ pub enum MtGRarity { } /// card border colors -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub enum BorderColor { #[serde(rename = "white")] White, @@ -774,7 +781,7 @@ pub enum BorderColor { } /// Scryfall prices struct -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub struct ScryfallPrices { pub usd: Option, // Option, pub usd_foil: Option, // Option, @@ -793,7 +800,7 @@ pub enum PriceType { } /// struct of all of Scryfall's related URIs -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Serialize)] pub struct ScryfallRelated { #[serde(default)] pub gatherer: String, @@ -806,7 +813,7 @@ pub struct ScryfallRelated { } /// struct of Scryfall purchase URIs -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, Serialize)] pub struct ScryfallPurchase { #[serde(default)] pub tcgplayer: String, @@ -899,3 +906,17 @@ pub fn make_safe_name(name: &str, dual: bool) -> String { } safe_name } + +/// saves the Scryfall database in a serial file +pub fn serialize_database( + map: &HashMap, + path: PathBuf, +) -> Result<(), Box> { + let json_string = serde_json::to_string_pretty(map)?; + let mut file_path = path.join(PathBuf::from("decklist_")); + let date_str = Local::now().to_string(); + file_path.push(PathBuf::from(date_str)); + let mut file = File::create(path)?; + file.write_all(json_string.as_bytes())?; + Ok(()) +} diff --git a/src/startup.rs b/src/startup.rs index 16453ff..476327f 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -263,7 +263,7 @@ async fn scryfall_bulk_request( // first request gets URI for latest data let resp: ScryfallResponse = scryfall_agent .get("https://api.scryfall.com/bulk-data/default-cards") - .header("User-Agent", "decklistv0.3.1") + .header("User-Agent", "decklistv0.5.0") .header("Accept", "*/*") .call()? .body_mut() @@ -278,7 +278,7 @@ async fn scryfall_bulk_request( // second request downloads JSON file to user data directory let download_request = scryfall_agent .get(uri) - .header("User-Agent", "decklistv0.3.1") + .header("User-Agent", "decklistv0.5.0") .header("Accept", "application/file") .call()? .body_mut() diff --git a/src/tui/core.rs b/src/tui/core.rs index 437f72b..a8c9df5 100644 --- a/src/tui/core.rs +++ b/src/tui/core.rs @@ -86,7 +86,7 @@ pub fn ui( // define main/center area for display let version = - Line::from(vec!["| Decklist v0.4.0 |".into()]).style(Style::default().cyan().bold()); + Line::from(vec!["| Decklist v0.5.0 |".into()]).style(Style::default().cyan().bold()); let main_block = Block::default() .title_bottom(version) .title_alignment(Alignment::Center) From 896fd0b8218ad23441f67a5e27c593821b018c02 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Mon, 29 Sep 2025 00:43:09 -0400 Subject: [PATCH 02/13] do mpsc channel next --- src/app.rs | 9 +++++++-- src/database/scryfall.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index b8324b1..de4e3bc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use crate::{ collection::{check_legality, check_missing, FormatLegal}, - database::scryfall::{get_min_price, match_card, min_price_fmt}, + database::scryfall::{get_min_price, match_card, min_price_fmt, serialize_database}, startup::{ config_check, database_check, database_management, directory_check, dl_scryfall_latest, load_database_file, ConfigCheck, DatabaseCheck, DirectoryCheck, @@ -453,10 +453,15 @@ impl App { // if a Scryfall database is loaded and the hashmap is done, serialize and save as a // custom database JSON with a single copy of each card with the lowest price so the // hashmap doesn't have to be filtered each time the program starts + let map = self.dc.database_cards.clone(); + let path = self.dc.database_path.clone(); if self.load_done { // TODO: eventually make another condition for when the short database has been // loaded instead of a Scryfall file - // + thread::spawn(move || { + // TODO: mpsc channel to communicate error messages + let short_write_results = task::block_on(serialize_database(&map, path)); + }); } if self.loading_collection { if let Ok(msg) = self.collection_channel.1.try_recv() { diff --git a/src/database/scryfall.rs b/src/database/scryfall.rs index 34bbb8e..35b62ba 100644 --- a/src/database/scryfall.rs +++ b/src/database/scryfall.rs @@ -908,7 +908,7 @@ pub fn make_safe_name(name: &str, dual: bool) -> String { } /// saves the Scryfall database in a serial file -pub fn serialize_database( +pub async fn serialize_database( map: &HashMap, path: PathBuf, ) -> Result<(), Box> { From 0bae83a5f688e5a2d2156f9df6531b1a3c6aee98 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:49:20 -0400 Subject: [PATCH 03/13] "is a directory" error and high CPU usage, probably messed up thread --- src/app.rs | 36 +++++++++++++++++++++++++++++++++--- src/database/scryfall.rs | 1 + src/tui/core.rs | 5 +++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index de4e3bc..b0f524f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -77,6 +77,7 @@ pub struct App { pub dl_done: bool, pub load_started: bool, pub load_done: bool, + pub short_started: bool, pub short_done: bool, // shorter JSON w/ single copy and lowest price pub os: SupportedOS, pub directory_exist: bool, @@ -101,6 +102,10 @@ pub struct App { std::sync::mpsc::Receiver, ), pub dc: DatabaseCheck, + pub short_channel: ( + std::sync::mpsc::Sender, + std::sync::mpsc::Receiver, + ), pub collection: Option>, pub collection_file_name: Option, pub collection_file: Option, @@ -169,6 +174,7 @@ pub struct App { pub legal_counter: u64, pub missing_counter: u64, pub price_counter: u64, + pub short_counter: u64, } impl Default for App { @@ -185,6 +191,7 @@ impl Default for App { dl_done: false, load_started: false, load_done: false, + short_started: false, short_done: false, os: SupportedOS::default(), directory_exist: false, @@ -200,6 +207,7 @@ impl Default for App { config_channel: std::sync::mpsc::channel(), database_channel: std::sync::mpsc::channel(), dc: DatabaseCheck::default(), + short_channel: std::sync::mpsc::channel(), collection: None, collection_file_name: None, collection_file: None, @@ -247,6 +255,7 @@ impl Default for App { legal_counter: 0, missing_counter: 0, price_counter: 0, + short_counter: 0, } } } @@ -455,14 +464,35 @@ impl App { // hashmap doesn't have to be filtered each time the program starts let map = self.dc.database_cards.clone(); let path = self.dc.database_path.clone(); - if self.load_done { + let debug_channel = self.debug_channel.0.clone(); + if self.load_done && !self.short_started { + // TODO: reset this when loading a new database + self.short_started = true; // TODO: eventually make another condition for when the short database has been // loaded instead of a Scryfall file + let short_channel = self.short_channel.0.clone(); + self.short_counter += 1; thread::spawn(move || { - // TODO: mpsc channel to communicate error messages - let short_write_results = task::block_on(serialize_database(&map, path)); + if let Ok(()) = debug_channel.send("Starting short thread...\n".to_string()) {}; + match task::block_on(serialize_database(&map, path)) { + Ok(()) => short_channel.send( + "Compact decklist database generated successfully.\n".to_string(), + ), + Err(e) => short_channel.send(e.to_string()), + } }); } + if self.short_started && !self.short_done { + if let Ok(s) = self.debug_channel.1.try_recv() { + self.debug_string += &s; + } + // TODO: eventually move this behind a different boolean + if let Ok(s) = self.short_channel.1.try_recv() { + self.debug_string += &s; + // TODO: reset this when reloading database + self.short_done = true; + } + } if self.loading_collection { if let Ok(msg) = self.collection_channel.1.try_recv() { self.debug_string += &msg.debug; diff --git a/src/database/scryfall.rs b/src/database/scryfall.rs index 35b62ba..5736a80 100644 --- a/src/database/scryfall.rs +++ b/src/database/scryfall.rs @@ -916,6 +916,7 @@ pub async fn serialize_database( let mut file_path = path.join(PathBuf::from("decklist_")); let date_str = Local::now().to_string(); file_path.push(PathBuf::from(date_str)); + file_path.push(".json"); let mut file = File::create(path)?; file.write_all(json_string.as_bytes())?; Ok(()) diff --git a/src/tui/core.rs b/src/tui/core.rs index a8c9df5..82640c6 100644 --- a/src/tui/core.rs +++ b/src/tui/core.rs @@ -231,6 +231,11 @@ fn draw_debug_main(app: &mut App, frame: &mut Frame, chunk: Rect, main_block: Bl Span::from(space_padding(10)), Span::from(format!("{}", app.price_counter)).cyan(), ]), + Line::from(vec![ + Span::from("Short Check: ").bold(), + Span::from(space_padding(10)), + Span::from(format!("{}", app.short_counter)).cyan(), + ]), ]); frame.render_widget(main_block, chunk); frame.render_widget(debug_text, sections[0]); From a0494fc93df5be8fc25b38553680be3ced6beab4 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:59:03 -0400 Subject: [PATCH 04/13] shortened database creates successfully --- src/app.rs | 2 +- src/database/scryfall.rs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index b0f524f..3c5b4b9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -465,7 +465,7 @@ impl App { let map = self.dc.database_cards.clone(); let path = self.dc.database_path.clone(); let debug_channel = self.debug_channel.0.clone(); - if self.load_done && !self.short_started { + if self.load_done && !self.short_started && !self.short_done { // TODO: reset this when loading a new database self.short_started = true; // TODO: eventually make another condition for when the short database has been diff --git a/src/database/scryfall.rs b/src/database/scryfall.rs index 5736a80..f173afc 100644 --- a/src/database/scryfall.rs +++ b/src/database/scryfall.rs @@ -913,11 +913,13 @@ pub async fn serialize_database( path: PathBuf, ) -> Result<(), Box> { let json_string = serde_json::to_string_pretty(map)?; - let mut file_path = path.join(PathBuf::from("decklist_")); - let date_str = Local::now().to_string(); - file_path.push(PathBuf::from(date_str)); - file_path.push(".json"); - let mut file = File::create(path)?; + let mut file_path_str = String::from("decklist_"); + let date = Local::now(); + let date_str = format!("{}", date.format("%Y%m%d")); + file_path_str.push_str(&date_str); + file_path_str.push_str(".json"); + let file_path = path.join(PathBuf::from(file_path_str)); + let mut file = File::create(file_path)?; file.write_all(json_string.as_bytes())?; Ok(()) } From 651021b749c95b60f8690ea92d6ac8641199a73c Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:49:11 -0400 Subject: [PATCH 05/13] troubleshooting CPU load --- src/app.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3c5b4b9..2d48ae2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -464,7 +464,6 @@ impl App { // hashmap doesn't have to be filtered each time the program starts let map = self.dc.database_cards.clone(); let path = self.dc.database_path.clone(); - let debug_channel = self.debug_channel.0.clone(); if self.load_done && !self.short_started && !self.short_done { // TODO: reset this when loading a new database self.short_started = true; @@ -472,15 +471,14 @@ impl App { // loaded instead of a Scryfall file let short_channel = self.short_channel.0.clone(); self.short_counter += 1; - thread::spawn(move || { - if let Ok(()) = debug_channel.send("Starting short thread...\n".to_string()) {}; - match task::block_on(serialize_database(&map, path)) { + thread::spawn( + move || match task::block_on(serialize_database(&map, path)) { Ok(()) => short_channel.send( "Compact decklist database generated successfully.\n".to_string(), ), Err(e) => short_channel.send(e.to_string()), - } - }); + }, + ); } if self.short_started && !self.short_done { if let Ok(s) = self.debug_channel.1.try_recv() { From aeef2a5e98ed2d3d1a7384a3bd896697d88c5dee Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:04:55 -0400 Subject: [PATCH 06/13] fixed CPU usage --- src/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2d48ae2..9cadc53 100644 --- a/src/app.rs +++ b/src/app.rs @@ -462,9 +462,9 @@ impl App { // if a Scryfall database is loaded and the hashmap is done, serialize and save as a // custom database JSON with a single copy of each card with the lowest price so the // hashmap doesn't have to be filtered each time the program starts - let map = self.dc.database_cards.clone(); - let path = self.dc.database_path.clone(); if self.load_done && !self.short_started && !self.short_done { + let map = self.dc.database_cards.clone(); + let path = self.dc.database_path.clone(); // TODO: reset this when loading a new database self.short_started = true; // TODO: eventually make another condition for when the short database has been From 132dcac009d253e8ac781f153c74d9b79ba125fd Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:34:45 -0400 Subject: [PATCH 07/13] finds new shorter database on startup, need to actually load properly --- src/startup.rs | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/startup.rs b/src/startup.rs index 476327f..46e3fb4 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -139,7 +139,22 @@ pub async fn database_check(data_path: PathBuf, max_age: u64) -> DatabaseCheck { let database_status; let database_cards = HashMap::new(); let mut filename = String::new(); - if let Some((fname, date)) = find_scryfall_database(data_path.clone()) { + // check for shortened decklist database first, then check for scryfall database + if let Some((fname, date)) = find_decklist_database(data_path.clone()) { + let current_time: DateTime = Local::now(); + let formatted_time = current_time.format("%Y%m%d").to_string(); + let time_num = formatted_time.parse::().unwrap_or(0); + // NOTE: quick protection against subtract with overflow if file was downloaded same day + if date < time_num && (time_num - date) > (max_age * 100) { + need_download = true; + database_status = format!("Decklist database file found, but it is older than {} days. Downloading new file...", max_age); + filename = fname; + } else { + database_status = format!("Recent decklist database found: {}", fname.clone()); + filename = fname; + ready_load = true; + } + } else if let Some((fname, date)) = find_scryfall_database(data_path.clone()) { let current_time: DateTime = Local::now(); let formatted_time = current_time.format("%Y%m%d%H%M%S").to_string(); let time_num = formatted_time.parse::().unwrap_or(0); @@ -221,6 +236,36 @@ fn find_scryfall_database(data_path: PathBuf) -> Option<(String, u64)> { } } +/// finds the latest custom database created by decklist +/// returns the full file path as Some(String) if found +/// returns None if no file exists +fn find_decklist_database(data_path: PathBuf) -> Option<(String, u64)> { + let items = fs::read_dir(data_path) + .expect("database directory should exist if calling find_decklist_database()"); + let mut options = Vec::new(); + let mut dates = Vec::new(); + for item in items { + if let Ok(f) = item { + let f_str = f.file_name().into_string(); + if f_str.is_ok() && f_str.as_ref().unwrap().contains("decklist") { + let f_string = f_str.unwrap(); + options.push(f_string.clone()); + let sections: Vec<&str> = f_string.split("_").collect(); + let subsections: Vec<&str> = sections[1].split('.').collect(); + match subsections[0].trim().parse::() { + Ok(num) => dates.push(num), + Err(_) => dates.push(0), + } + } + } + } + if let Some((index, date)) = dates.iter().enumerate().max_by_key(|&(_, &value)| value) { + Some((options[index].clone(), *date)) + } else { + None + } +} + /// downloads latest OracleCards bulk data from Scryfall pub async fn dl_scryfall_latest(mut dc: DatabaseCheck) -> DatabaseCheck { match scryfall_bulk_request(dc.database_path.clone()).await { From cdd760ec287478496bc821b8460abac447eb1549 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:36:43 -0400 Subject: [PATCH 08/13] WIP --- src/startup.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/startup.rs b/src/startup.rs index 46e3fb4..08b1035 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -130,6 +130,13 @@ impl Default for DatabaseCheck { } } +// indicates if database to be loaded is a Scryfall JSON or a decklist JSON, as they need to be +// parsed differently +pub enum DatabaseType { + Scryfall, + Decklist, +} + /// checks for existence of database file /// if none are found, prompt to download a new file pub async fn database_check(data_path: PathBuf, max_age: u64) -> DatabaseCheck { From 9f6f89b5703a71f2e8952cac9793a6f857189208 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Mon, 20 Oct 2025 06:56:15 -0400 Subject: [PATCH 09/13] decklist JSON is empty??? --- src/database/scryfall.rs | 13 ++++++++++++- src/startup.rs | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/database/scryfall.rs b/src/database/scryfall.rs index f173afc..b6c8587 100644 --- a/src/database/scryfall.rs +++ b/src/database/scryfall.rs @@ -744,6 +744,8 @@ pub enum ScryfallSetType { FromTheVault, #[serde(rename = "spellbook")] Spellbook, + #[serde(rename = "eternal")] + Eternal, } /// card rarities @@ -823,7 +825,7 @@ pub struct ScryfallPurchase { pub cardhoarder: String, } -/// reads provided JSON database file and produces a vector of ScryfallCard objects +/// reads provided JSON database file and produces a hash map of ScryfallCard objects pub fn read_scryfall_database( path: &PathBuf, ) -> Result, Box> { @@ -851,6 +853,15 @@ pub fn read_scryfall_database( Ok(result_map) } +/// reads custom decklist JSON file +pub fn read_decklist_database( + path: &PathBuf, +) -> Result, Box> { + let file_text = fs::read_to_string(path)?; + let map: HashMap = serde_json::from_str(&file_text)?; + Ok(map) +} + // TODO: maybe replace the manual implementations of this elsewhere? /// takes card name and finds matching card in database pub fn match_card( diff --git a/src/startup.rs b/src/startup.rs index 08b1035..006ee06 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -14,7 +14,7 @@ use ureq::Agent; use crate::{ config::DecklistConfig, - database::scryfall::{read_scryfall_database, ScryfallCard}, + database::scryfall::{read_decklist_database, read_scryfall_database, ScryfallCard}, }; /* @@ -114,6 +114,7 @@ pub struct DatabaseCheck { pub filename: String, pub need_dl: bool, pub ready_load: bool, + pub db_type: DatabaseType, } impl Default for DatabaseCheck { @@ -126,12 +127,14 @@ impl Default for DatabaseCheck { filename: String::new(), need_dl: false, ready_load: false, + db_type: DatabaseType::Scryfall, } } } // indicates if database to be loaded is a Scryfall JSON or a decklist JSON, as they need to be // parsed differently +#[derive(Clone)] pub enum DatabaseType { Scryfall, Decklist, @@ -143,6 +146,7 @@ pub async fn database_check(data_path: PathBuf, max_age: u64) -> DatabaseCheck { let mut need_download = false; let mut ready_load = false; let mut database_exists = false; + let mut db_type = DatabaseType::Scryfall; let database_status; let database_cards = HashMap::new(); let mut filename = String::new(); @@ -160,6 +164,7 @@ pub async fn database_check(data_path: PathBuf, max_age: u64) -> DatabaseCheck { database_status = format!("Recent decklist database found: {}", fname.clone()); filename = fname; ready_load = true; + db_type = DatabaseType::Decklist; } } else if let Some((fname, date)) = find_scryfall_database(data_path.clone()) { let current_time: DateTime = Local::now(); @@ -193,6 +198,7 @@ pub async fn database_check(data_path: PathBuf, max_age: u64) -> DatabaseCheck { need_dl: need_download, ready_load, filename, + db_type, } } @@ -201,14 +207,28 @@ pub async fn database_check(data_path: PathBuf, max_age: u64) -> DatabaseCheck { pub async fn load_database_file(mut dc: DatabaseCheck) -> DatabaseCheck { let mut data_path = dc.database_path.clone(); data_path.push(dc.filename.clone()); - match read_scryfall_database(&data_path) { - Ok(cards) => { - dc.database_exists = true; - dc.database_status = format!("Loaded cards from: {}", dc.filename); - dc.database_cards = cards; - dc.ready_load = false; // file loaded successfully, don't need to do again + // determine if loading a Scryfall or Decklist database + match dc.db_type { + DatabaseType::Decklist => match read_decklist_database(&data_path) { + Ok(cards) => { + dc.database_exists = true; + dc.database_status = format!("Loaded cards from: {}", dc.filename); + dc.database_cards = cards; + dc.ready_load = false; + } + Err(e) => dc.database_status = e.to_string(), + }, + DatabaseType::Scryfall => { + match read_scryfall_database(&data_path) { + Ok(cards) => { + dc.database_exists = true; + dc.database_status = format!("Loaded cards from: {}", dc.filename); + dc.database_cards = cards; + dc.ready_load = false; // file loaded successfully, don't need to do again + } + Err(e) => dc.database_status = e.to_string(), + } } - Err(e) => dc.database_status = e.to_string(), } dc } From b99053bf1a04d238882197b901715ebc8e047275 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:03:02 -0500 Subject: [PATCH 10/13] Fixed database overwrite bug when loading database fails. Need to correctly identify database type when the file is found, right now decklist file is treated as a Scryfall database and deserializing fails. --- src/app.rs | 11 +++++++++-- src/startup.rs | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9cadc53..3278dd0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use crate::{ database::scryfall::{get_min_price, match_card, min_price_fmt, serialize_database}, startup::{ config_check, database_check, database_management, directory_check, dl_scryfall_latest, - load_database_file, ConfigCheck, DatabaseCheck, DirectoryCheck, + load_database_file, ConfigCheck, DatabaseCheck, DatabaseType, DirectoryCheck, }, }; use arboard::Clipboard; @@ -462,7 +462,14 @@ impl App { // if a Scryfall database is loaded and the hashmap is done, serialize and save as a // custom database JSON with a single copy of each card with the lowest price so the // hashmap doesn't have to be filtered each time the program starts - if self.load_done && !self.short_started && !self.short_done { + // only do it if loading a Scryfall JSON + if self.load_done + && !self.short_started + && !self.short_done + && self.dc.db_type == DatabaseType::Scryfall + && self.dc.database_cards.len() > 10 + // don't overwrite database if loading fails + { let map = self.dc.database_cards.clone(); let path = self.dc.database_path.clone(); // TODO: reset this when loading a new database diff --git a/src/startup.rs b/src/startup.rs index 006ee06..ccfb998 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -134,7 +134,7 @@ impl Default for DatabaseCheck { // indicates if database to be loaded is a Scryfall JSON or a decklist JSON, as they need to be // parsed differently -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub enum DatabaseType { Scryfall, Decklist, @@ -208,6 +208,7 @@ pub async fn load_database_file(mut dc: DatabaseCheck) -> DatabaseCheck { let mut data_path = dc.database_path.clone(); data_path.push(dc.filename.clone()); // determine if loading a Scryfall or Decklist database + println!("{:?}", dc.db_type); match dc.db_type { DatabaseType::Decklist => match read_decklist_database(&data_path) { Ok(cards) => { From 43d5377695aff0768719c0ca1d7f8a3b848529e4 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:11:21 -0500 Subject: [PATCH 11/13] fixed database loading bug Next, check for $0.00 when finding minimum price and ignore. --- src/app.rs | 1 + src/startup.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 3278dd0..0255f58 100644 --- a/src/app.rs +++ b/src/app.rs @@ -394,6 +394,7 @@ impl App { self.dc.need_dl = dc.need_dl; self.dc.ready_load = dc.ready_load; self.dc.filename = dc.filename; + self.dc.db_type = dc.db_type; if dc.database_exists { self.database_ok = true; } diff --git a/src/startup.rs b/src/startup.rs index ccfb998..d7a97dd 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -208,7 +208,6 @@ pub async fn load_database_file(mut dc: DatabaseCheck) -> DatabaseCheck { let mut data_path = dc.database_path.clone(); data_path.push(dc.filename.clone()); // determine if loading a Scryfall or Decklist database - println!("{:?}", dc.db_type); match dc.db_type { DatabaseType::Decklist => match read_decklist_database(&data_path) { Ok(cards) => { From 7dda0d806b1eae427073dd25f7b10fca591eadcb Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:22:48 -0500 Subject: [PATCH 12/13] decklist database ignores 0.0 prices when picking lowest, and now supports sorting by all currency options --- src/app.rs | 3 ++- src/database/scryfall.rs | 22 +++++++++++++++++++++- src/startup.rs | 6 +++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0255f58..004680b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -443,9 +443,10 @@ impl App { self.dc.database_status = format!("Loading {} ...", self.dc.filename); let database_channel = self.database_channel.0.clone(); let dc_clone = self.dc.clone(); + let currency = self.config.currency.clone(); self.database_counter += 1; thread::spawn(move || { - let database_results = task::block_on(load_database_file(dc_clone)); + let database_results = task::block_on(load_database_file(dc_clone, currency)); if let Ok(()) = database_channel.send(database_results) {}; }); self.dc.ready_load = false; diff --git a/src/database/scryfall.rs b/src/database/scryfall.rs index b6c8587..7676ff4 100644 --- a/src/database/scryfall.rs +++ b/src/database/scryfall.rs @@ -828,6 +828,7 @@ pub struct ScryfallPurchase { /// reads provided JSON database file and produces a hash map of ScryfallCard objects pub fn read_scryfall_database( path: &PathBuf, + currency: PriceType, ) -> Result, Box> { let file_text = fs::read_to_string(path)?; let test: Result, serde_json::Error> = serde_json::from_str(&file_text); @@ -844,7 +845,26 @@ pub fn read_scryfall_database( result_map .entry(safe_name) .and_modify(|existing| { - if card.prices.usd < existing.prices.usd { + let (c_opt, e_opt) = match currency { + PriceType::USD => (card.prices.usd.clone(), existing.prices.usd.clone()), + PriceType::Euro => (card.prices.eur.clone(), existing.prices.eur.clone()), + PriceType::Tix => (card.prices.tix.clone(), existing.prices.tix.clone()), + }; + let mut e_price = 0.0; + let mut c_price = 0.0; + if let Some(e_str) = e_opt { + match e_str.parse() { + Ok(p) => e_price = p, + Err(_) => {} + } + } + if let Some(c_str) = c_opt { + match c_str.parse() { + Ok(p) => c_price = p, + Err(_) => {} + } + } + if c_price > 0.0 && c_price < e_price { *existing = card.clone(); } }) diff --git a/src/startup.rs b/src/startup.rs index d7a97dd..1256ff1 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -14,7 +14,7 @@ use ureq::Agent; use crate::{ config::DecklistConfig, - database::scryfall::{read_decklist_database, read_scryfall_database, ScryfallCard}, + database::scryfall::{read_decklist_database, read_scryfall_database, PriceType, ScryfallCard}, }; /* @@ -204,7 +204,7 @@ pub async fn database_check(data_path: PathBuf, max_age: u64) -> DatabaseCheck { /// attempts to load given database file /// updates status accordingly -pub async fn load_database_file(mut dc: DatabaseCheck) -> DatabaseCheck { +pub async fn load_database_file(mut dc: DatabaseCheck, currency: PriceType) -> DatabaseCheck { let mut data_path = dc.database_path.clone(); data_path.push(dc.filename.clone()); // determine if loading a Scryfall or Decklist database @@ -219,7 +219,7 @@ pub async fn load_database_file(mut dc: DatabaseCheck) -> DatabaseCheck { Err(e) => dc.database_status = e.to_string(), }, DatabaseType::Scryfall => { - match read_scryfall_database(&data_path) { + match read_scryfall_database(&data_path, currency) { Ok(cards) => { dc.database_exists = true; dc.database_status = format!("Loaded cards from: {}", dc.filename); From 8d8aa467f11a7d03b2a18018a2f97ccc6cf83ff6 Mon Sep 17 00:00:00 2001 From: hobosock <76829496+hobosock@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:33:40 -0500 Subject: [PATCH 13/13] prices no longer get stuck on 0 if first entry has null for price --- Cargo.lock | 4 ++-- src/database/scryfall.rs | 3 +-- src/main.rs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 589f3a4..cff5742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1195,9 +1195,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "value-bag", ] diff --git a/src/database/scryfall.rs b/src/database/scryfall.rs index 7676ff4..47b99d0 100644 --- a/src/database/scryfall.rs +++ b/src/database/scryfall.rs @@ -841,7 +841,6 @@ pub fn read_scryfall_database( || card.layout == CardLayouts::ModalDualFaceCard || card.layout == CardLayouts::Adventure; let safe_name = make_safe_name(&card.name, dual); - // TODO: get all currencies in here result_map .entry(safe_name) .and_modify(|existing| { @@ -864,7 +863,7 @@ pub fn read_scryfall_database( Err(_) => {} } } - if c_price > 0.0 && c_price < e_price { + if c_price > 0.0 && (c_price < e_price || e_price == 0.0) { *existing = card.clone(); } }) diff --git a/src/main.rs b/src/main.rs index 1be8d71..0d147d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,11 +7,10 @@ pub mod database; pub mod startup; pub mod tui; +use app::App; use ratatui_explorer::{FileExplorer, Theme}; use tui::core::{init, restore}; -use app::App; - #[tokio::main] async fn main() -> Result<(), io::Error> { let mut terminal = init()?;