diff --git a/.env b/.env new file mode 100644 index 0000000..c0d95b5 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +UPDATE_INTERVAL=12 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a7f353e..1546507 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -80,6 +95,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + [[package]] name = "console" version = "0.15.7" @@ -109,6 +139,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -328,6 +364,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.4.0" @@ -453,7 +512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -461,6 +520,8 @@ dependencies = [ name = "mtg_seller_bot" version = "0.1.0" dependencies = [ + "chrono", + "dotenv", "indicatif", "reqwest", "serde", @@ -486,6 +547,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -842,6 +912,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -997,6 +1078,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1101,6 +1188,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.1", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 305d0b5..24070a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = "0.4.26" +dotenv = "0.15.0" indicatif = "0.17.5" reqwest = { version = "0.11.18", features = ["json"] } serde = { version = "1.0.178", features = ["derive"]} diff --git a/src/main.rs b/src/main.rs index 79ff14b..06304a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,107 +1,114 @@ +/// This program utilizes several external libraries to perform its functions, such as dotenv, serde, tokio, etc. +use dotenv; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, env, fs::File, fs::OpenOptions, io::BufReader}; use tokio::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; - -/// A struct to represent a Card returned from the API. +/// This structure defines a Card object, consisting of a name and a hashmap of prices. #[derive(Serialize, Deserialize, Debug, Clone)] struct Card { - /// The name of the card. name: String, - /// The prices of the card in various formats. prices: HashMap>, } -/// A struct to represent a Card from the local JSON file. +/// This structure defines a CardFromFile object, consisting of a name, count, and a USD value. #[derive(Serialize, Deserialize, Debug, Clone)] struct CardFromFile { - /// The name of the card. name: String, - /// The count of this card. count: usize, - /// The value of this card in USD. usd_value: Option, } -/// A struct to represent a collection of Cards from the local JSON file. +/// This structure defines a CardFile object, consisting of a vector of CardFromFile objects. #[derive(Serialize, Deserialize, Debug)] struct CardFile { - /// The list of cards. cards: Vec, } -/// The main function. -/// -/// This function reads a local JSON file of cards, sends an API request for each card to get the current price in USD, -/// compares the fetched price with the stored price in the local file, and updates the file if there is any difference. -/// -/// Note: There is a delay of 100ms between each API request as per the API rules. +/// The main function of the program. It retrieves card price information from an external API and updates a local JSON file. +/// It accepts a path to a JSON file as an argument and also uses an environment variable, UPDATE_INTERVAL, to determine the frequency of its update cycle. #[tokio::main] -async fn main() -> Result<(), Box> { - let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("Please provide the path to the JSON file as an argument."); - return Ok(()); - } +async fn main() -> Result<(), Box> { + dotenv::dotenv().ok(); - let file_path = &args[1]; - let file = match File::open(file_path) { - Ok(file) => file, - Err(error) => { - eprintln!("There was a problem opening the file: {:?}", error); - return Err(Box::new(std::io::Error::new( - std::io::ErrorKind::NotFound, - "File not found", - ))); + // Retrieve the update interval from the .env file or return an error if not present. + let update_interval_str = match dotenv::var("UPDATE_INTERVAL") { + Ok(val) => val, + Err(_) => { + eprintln!("UPDATE_INTERVAL is not defined in the .env file"); + return Err("UPDATE_INTERVAL is not defined in the .env file".into()); } - }; - - let reader = BufReader::new(file); - let mut cards_data: CardFile = serde_json::from_reader(reader).unwrap(); + }; + // Attempt to parse the update interval as a u64 or return an error if it fails. + let update_interval = match update_interval_str.parse::() { + Ok(val) => val, + Err(_) => { + eprintln!("UPDATE_INTERVAL is not a valid number"); + return Err("UPDATE_INTERVAL is not a valid number".into()); + } + }; - let pb = ProgressBar::new(cards_data.cards.len() as u64); - let style = ProgressStyle::default_bar() - .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})") - .unwrap(); - let style = style.progress_chars("#>-"); - pb.set_style(style); + let update_interval = std::time::Duration::from_secs(update_interval * 3600); - for card_from_file in &mut cards_data.cards { - let request_url = format!( - "https://api.scryfall.com/cards/named?exact={}", - card_from_file.name - ); - let response = reqwest::get(&request_url).await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?; - - let card: Card = response.json().await - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))?; + let mut interval = tokio::time::interval(update_interval); + loop { + interval.tick().await; - + // Retrieve the file path from the program's arguments or return an error if not present. + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Please provide the path to the JSON file as an argument."); + return Ok(()); + } - if let Some(price) = card.prices.get("usd") { - if let Some(price_str) = price { - if card_from_file.usd_value.as_ref() != Some(price_str) { - card_from_file.usd_value = Some(price_str.clone()); + let file_path = &args[1]; + let file = File::open(file_path)?; + + let reader = BufReader::new(file); + let mut cards_data: CardFile = serde_json::from_reader(reader)?; + + // Setting up a progress bar for visual representation of the card processing progress. + let pb = ProgressBar::new(cards_data.cards.len() as u64); + let style = ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})") + .unwrap(); + let style = style.progress_chars("#>-"); + pb.set_style(style); + + // For each card in the input file, retrieve the current price information and update the local data if necessary. + for card_from_file in &mut cards_data.cards { + let request_url = format!( + "https://api.scryfall.com/cards/named?exact={}", + card_from_file.name + ); + let response = reqwest::get(&request_url).await?; + + let card: Card = response.json().await?; + + if let Some(price) = card.prices.get("usd") { + if let Some(price_str) = price { + if card_from_file.usd_value.as_ref() != Some(price_str) { + card_from_file.usd_value = Some(price_str.clone()); + } } } + + // Increment the progress bar and pause for a brief period. + pb.inc(1); + tokio::time::sleep(Duration::from_millis(100)).await; } - pb.inc(1); - tokio::time::sleep(Duration::from_millis(100)).await; + // Mark the progress bar as completed. + pb.finish_with_message("Completed!"); + + // Write the updated card data back to the input file. + let file = OpenOptions::new() + .write(true) + .truncate(true) + .open(file_path)?; + + serde_json::to_writer_pretty(file, &cards_data)?; + } - - pb.finish_with_message("Completed!"); - - let file = OpenOptions::new() - .write(true) - .truncate(true) - .open(file_path) - .unwrap(); - - serde_json::to_writer_pretty(file, &cards_data).unwrap(); - - Ok(()) } -