From cabdd7d5be37b66de9c3e0d2cbd7585b3b161012 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Sat, 25 Jan 2025 21:09:29 +0100 Subject: [PATCH] add caching for icons and json, spacing for ui --- .gitignore | 3 +- Cargo.toml | 7 ++++ src/lib.rs | 7 ++-- src/main.rs | 97 ++++++++++++++++++++++++++++++++++++++++++---------- src/steam.rs | 15 +++++++- src/ui.rs | 26 ++++++++++---- src/watch.rs | 40 ++++++++++++++-------- 7 files changed, 149 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 9860f0c..3f6fbb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /target -*.json -/icons Cargo.lock +*.sh diff --git a/Cargo.toml b/Cargo.toml index 2283e56..4ba6ab3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,13 @@ name = "achievement-watcher" version = "0.1.0" edition = "2021" +[profile.release] +strip = true +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" + [dependencies] dirs = "6.0.0" eframe = "0.30.0" diff --git a/src/lib.rs b/src/lib.rs index 4f28207..294746a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,8 @@ pub struct Achievement { pub struct Context { pub api_key: String, pub game_name_cache_path: String, - pub unlocked_icon_path: String, + pub json_cache_path: String, + pub icon_cache_path: String, pub game_name_list: Vec, pub dirs: Vec, } @@ -47,7 +48,7 @@ pub async fn get_achievement_data(client: &Client, app_id: u32, context: &Contex app_id, achievements: Vec::new(), }; - let game_data = steam::get_steam_data(client, &context.api_key, app_id, "en").await; + let game_data = steam::get_steam_data(client, &context.api_key, app_id, "en", &context.json_cache_path).await; let ini_data = steam::get_achievement_data_all_ini(&context.dirs); let ini_game = ini_data.iter().find(|m| m.id == app_id)?; for json_achievement in &game_data.game.available_game_stats.achievements { @@ -97,7 +98,7 @@ pub async fn get_achievement_data_all(client: &Client, context: &Context) -> Vec .await .unwrap(), }; - let game_data = steam::get_steam_data(client, &context.api_key, ini_game.id, "en").await; + let game_data = steam::get_steam_data(client, &context.api_key, ini_game.id, "en", &context.json_cache_path).await; for json_achievement in &game_data.game.available_game_stats.achievements { let mut found_in_ini = false; for ini_achievement in &ini_game.achievements { diff --git a/src/main.rs b/src/main.rs index f1ca072..7d29d32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use achievement_watcher::Context; +use achievement_watcher::{Context, Game}; use reqwest::Client; use std::{env, fs}; @@ -8,16 +8,26 @@ mod watch; #[tokio::main] async fn main() { let client = Client::new(); - let context = setup(); + let context = match setup() { + Some(v) => v, + None => { + eprintln!("ERROR: was not able to create context"); + std::process::exit(1); + } + }; let mut games = achievement_watcher::get_achievement_data_all(&client, &context).await; + + cache_icons(&context, &games).await; + let args: Vec = std::env::args().skip(1).collect(); if args.is_empty() { - ui::ui(games).unwrap(); + if let Err(e) = ui::ui(context, games) { + eprintln!("ERROR: {e}"); + std::process::exit(1); + } } else if args.len() == 1 { match args[0].as_str() { - "-w" => watch::watch_file(&context, &mut games) - .await - .unwrap(), + "-w" => watch::watch_files(&context, &mut games).await.unwrap(), _ => panic!("wrong arg"), } } else { @@ -25,23 +35,22 @@ async fn main() { } } -fn setup() -> Context { - let config_dir = - dirs::config_dir().unwrap().to_str().unwrap().to_owned() + "/achievement-watcher"; - let cache_dir = - dirs::cache_dir().unwrap().to_str().unwrap().to_owned() + "/achievement-watcher"; +fn setup() -> Option { + let config_dir = dirs::config_dir()?.to_str()?.to_owned() + "/achievement-watcher"; + let cache_dir = dirs::cache_dir()?.to_str()?.to_owned() + "/achievement-watcher"; match std::fs::exists(&config_dir) { - Ok(false) => std::fs::create_dir(&config_dir).unwrap(), + Ok(false) => std::fs::create_dir(&config_dir).ok()?, Err(e) => eprintln!("error in setup: config dir: {e}"), Ok(true) => (), } match std::fs::exists(&cache_dir) { - Ok(false) => std::fs::create_dir(&cache_dir).unwrap(), + Ok(false) => std::fs::create_dir(&cache_dir).ok()?, Err(e) => eprintln!("error in setup: cache dir: {e}"), Ok(true) => (), } let game_name_cache_path = format!("{cache_dir}/game_names.txt"); - let unlocked_icon_path = format!("{cache_dir}/unlocked.png"); + let json_cache_path = format!("{cache_dir}/json"); + let icon_cache_path = format!("{cache_dir}/icon"); let game_name_list = achievement_watcher::util::get_game_name_file_cache(&game_name_cache_path); let api_key_path = format!("{config_dir}/api-key.txt"); let api_key = match fs::read_to_string(&api_key_path) { @@ -52,16 +61,68 @@ fn setup() -> Context { } }; let api_key = api_key.trim(); - let wine_dir = env::var("WINEPREFIX").unwrap_or(env::var("HOME").unwrap() + ".wine"); + let wine_dir = env::var("WINEPREFIX").unwrap_or(env::var("HOME").ok()? + ".wine"); let dirs = vec![ wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/CODEX", wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/RUNE", ]; - Context { + Some(Context { api_key: api_key.to_owned(), dirs, game_name_list, game_name_cache_path, - unlocked_icon_path, - } + json_cache_path, + icon_cache_path, + }) +} + +async fn cache_icons(context: &Context, games: &Vec) { + let client = Client::new(); + let mut futures = Vec::new(); + for game in games { + for achievement in &game.achievements { + let client = &client; + let future = async move { + let icon_dir = &format!("{}/{}", &context.icon_cache_path, game.name); + if !std::path::Path::new(icon_dir).exists() { + std::fs::create_dir(icon_dir).unwrap(); + } + let icon_path = format!( + "{}/{}/{}.png", + &context.icon_cache_path, game.name, achievement.name, + ); + if !std::path::Path::new(&icon_path).exists() { + let data = client + .get(&achievement.icon) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + std::fs::write(&icon_path, &data).unwrap(); + println!("written icon path: {icon_path}"); + } + + let icon_path = format!( + "{}/{}/{}gray.png", + context.icon_cache_path, game.name, achievement.name, + ); + if !std::path::Path::new(&icon_path).exists() { + let data = client + .get(&achievement.icongray) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + std::fs::write(&icon_path, &data).unwrap(); + println!("written icon: {icon_path}"); + } + }; + futures.push(future); + } + } + futures::future::join_all(futures).await; } diff --git a/src/steam.rs b/src/steam.rs index f9e731b..f97f25b 100644 --- a/src/steam.rs +++ b/src/steam.rs @@ -2,6 +2,7 @@ use ini::Ini; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::fs; +use std::path::Path; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -90,9 +91,21 @@ impl IniAchievement { } } -pub async fn get_steam_data(client: &Client, key: &str, appid: u32, lang: &str) -> JsonGame { +pub async fn get_steam_data( + client: &Client, + key: &str, + appid: u32, + lang: &str, + json_cache_path: &str, +) -> JsonGame { + let json_file_path = format!("{json_cache_path}/{appid}.json"); + if Path::new(&json_file_path).exists() { + let json = fs::read_to_string(json_file_path).unwrap(); + return serde_json::from_str(&json).unwrap(); + } let url = format!("https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v0002/?key={key}&appid={appid}&l={lang}&format=json"); let json = client.get(url).send().await.unwrap().text().await.unwrap(); + fs::write(json_file_path, &json).unwrap(); serde_json::from_str(&json).unwrap() } diff --git a/src/ui.rs b/src/ui.rs index 26f8505..c0a7934 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,8 @@ -use achievement_watcher::Game; +use achievement_watcher::{Context, Game}; use eframe::egui; +use std::path::Path; -pub fn ui(games: Vec) -> eframe::Result { +pub fn ui(context: Context, games: Vec) -> eframe::Result { env_logger::init(); let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default(), @@ -41,18 +42,29 @@ pub fn ui(games: Vec) -> eframe::Result { ui.add(egui::ProgressBar::new( achieved_count as f32 / game.achievements.len() as f32, )); + ui.add_space(32.); for achievement in &game.achievements { ui.heading(&achievement.name); if let Some(desc) = &achievement.description { ui.label(desc); } - let icon = if achievement.achieved { - &achievement.icon + let icon_path = format!( + "{}/{}/{}{}.png", + context.icon_cache_path, + game.name, + achievement.name, + if achievement.achieved { "" } else { "gray" } + ); + if Path::new(&icon_path).exists() { + ui.add( + egui::Image::new(format!("file://{}", icon_path)) + .fit_to_original_size(1.), + ); } else { - &achievement.icongray - }; - ui.add(egui::Image::new(icon).fit_to_original_size(1.)); + panic!("image not cached"); + } + ui.add_space(32.); } }); }); diff --git a/src/watch.rs b/src/watch.rs index 95c5c55..90f56ab 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -4,7 +4,7 @@ use reqwest::Client; use std::path::Path; use std::sync::mpsc; -pub async fn watch_file(context: &Context, games: &mut Vec) -> Result<()> { +pub async fn watch_files(context: &Context, games: &mut Vec) -> Result<()> { let (tx, rx) = mpsc::channel::>(); let mut watcher = notify::recommended_watcher(tx)?; let client = Client::new(); @@ -15,7 +15,6 @@ pub async fn watch_file(context: &Context, games: &mut Vec) -> Result<()> for res in &rx { match res { Ok(event) => { - // println!("event: {:#?}", event); if let EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write)) = event.kind { @@ -65,23 +64,34 @@ async fn check_for_new_achievement( for i in indexes { let new_achievement = &new.achievements[i]; println!("new achievement: {:#?}", new_achievement); - let jpg_icon: &[u8] = &client - .get(&new_achievement.icon) - .send() - .await - .unwrap() - .bytes() - .await - .unwrap(); - let image: image::DynamicImage = image::load_from_memory(jpg_icon).unwrap(); - image - .save_with_format(&context.unlocked_icon_path, image::ImageFormat::Png) - .unwrap(); + let icon_path = format!( + "{}/{}/{}.png", + context.icon_cache_path, old.name, new_achievement.name + ); + let icon_path = Path::new(&icon_path); + + println!("icon path: {:?}", &icon_path); + if !icon_path.exists() { + let jpg_icon: &[u8] = &client + .get(&new_achievement.icon) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + let image: image::DynamicImage = image::load_from_memory(jpg_icon).unwrap(); + image + .save_with_format(icon_path, image::ImageFormat::Png) + .unwrap(); + } else { + println!("using cached icons"); + } util::send_notification( &new.name, &new_achievement.name, new_achievement.description.as_deref(), - Some(&context.unlocked_icon_path), + Some(icon_path.to_str().unwrap()), ); } } else {