From 2793ae830a2fd954c3667f17be6f7b5cdccb71e2 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Sat, 25 Jan 2025 02:32:43 +0100 Subject: [PATCH] more watcher work, TODO: watch directories instead of files --- src/lib.rs | 94 +++++++++++++++++++++++++++++----------------------- src/main.rs | 84 ++++++++++++++++++++++++++-------------------- src/steam.rs | 3 +- src/util.rs | 4 +-- src/watch.rs | 80 +++++++++++++++++++++++++++++++++++++++----- 5 files changed, 176 insertions(+), 89 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 83197b1..717a5e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,61 +26,73 @@ pub struct Achievement { pub unlock_time: u32, } -pub async fn get_achievement_data( - client: &Client, - api_key: &str, - app_id: u32, - game_name_cache_path: &str, - game_name_list: &Vec, -) -> Game { +pub struct Context { + pub api_key: String, + pub game_name_cache_path: String, + pub unlocked_icon_path: String, + pub game_name_list: Vec, + pub dirs: Vec, +} + +pub async fn get_achievement_data(client: &Client, app_id: u32, context: &Context) -> Option { let mut game = Game { - name: steam::get_game_name(client, game_name_cache_path, game_name_list, app_id) - .await - .unwrap(), + name: steam::get_game_name( + client, + &context.game_name_cache_path, + &context.game_name_list, + app_id, + ) + .await + .unwrap(), app_id, achievements: Vec::new(), }; - let game_data = steam::get_steam_data(client, api_key, app_id, "en").await; + let game_data = steam::get_steam_data(client, &context.api_key, app_id, "en").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 { - game.achievements.push(Achievement { - achieved: false, - max_progress: 0, - current_progress: 0, - unlock_time: 0, - description: json_achievement.description.clone(), - name: json_achievement.display_name.clone(), - hidden: if json_achievement.hidden == 1 { - true - } else if json_achievement.hidden == 0 { - false - } else { - panic!("Unexpected hidden value") - }, - icon: json_achievement.icon.clone(), - icongray: json_achievement.icongray.clone(), - }); + for ini_achievement in &ini_game.achievements { + if json_achievement.name == ini_achievement.name { + game.achievements.push(Achievement { + achieved: ini_achievement.achieved, + max_progress: ini_achievement.max_progress, + current_progress: ini_achievement.current_progress, + unlock_time: ini_achievement.unlock_time, + description: json_achievement.description.clone(), + name: json_achievement.display_name.clone(), + hidden: if json_achievement.hidden == 1 { + true + } else if json_achievement.hidden == 0 { + false + } else { + panic!("Unexpected hidden value") + }, + icon: json_achievement.icon.clone(), + icongray: json_achievement.icongray.clone(), + }); + } + } } - game + Some(game) } -pub async fn get_achievement_data_all( - client: &Client, - api_key: &str, - game_name_cache_path: &str, - game_name_list: &Vec, - dirs: Vec, -) -> Vec { +pub async fn get_achievement_data_all(client: &Client, context: &Context) -> Vec { let mut games: Vec = Vec::new(); - let ini_games = steam::get_achievement_data_all_ini(dirs); + let ini_games = steam::get_achievement_data_all_ini(&context.dirs); for ini_game in ini_games { let mut game = Game { app_id: ini_game.id, achievements: Vec::new(), - name: steam::get_game_name(client, game_name_cache_path, game_name_list, ini_game.id) - .await - .unwrap(), + name: steam::get_game_name( + client, + &context.game_name_cache_path, + &context.game_name_list, + ini_game.id, + ) + .await + .unwrap(), }; - let game_data = steam::get_steam_data(client, api_key, ini_game.id, "en").await; + let game_data = steam::get_steam_data(client, &context.api_key, ini_game.id, "en").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 f4d3883..18f2e05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use achievement_watcher::Context; use reqwest::Client; use std::{env, fs}; @@ -7,49 +8,60 @@ mod watch; #[tokio::main] async fn main() { let client = Client::new(); - let config_dir = dirs::config_dir().unwrap().to_str().unwrap().to_owned(); - let cache_dir = dirs::cache_dir().unwrap().to_str().unwrap().to_owned(); - let game_name_cache_path = format!("{cache_dir}/achievement-watcher/game_names.txt"); + let context = setup(); + let mut games = achievement_watcher::get_achievement_data_all(&client, &context).await; + let args: Vec = std::env::args().skip(1).collect(); + if args.is_empty() { + ui::ui(games).unwrap(); + } else if args.len() == 3 { + match args[0].as_str() { + "-w" => watch::watch_file(&args[1], args[2].parse().unwrap(), &context, &mut games) + .await + .unwrap(), + _ => panic!("wrong arg"), + } + } else { + panic!("wrong arg count"); + } +} + +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"; + match std::fs::exists(&config_dir) { + Ok(false) => std::fs::create_dir(&config_dir).unwrap(), + 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(), + 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 game_name_list = achievement_watcher::util::get_game_name_file_cache(&game_name_cache_path); - let api_key = - fs::read_to_string(format!("{config_dir}/achievement-watcher/api-key.txt")).unwrap(); + let api_key_path = format!("{config_dir}/api-key.txt"); + let api_key = match fs::read_to_string(&api_key_path) { + Ok(v) => v, + Err(e) => { + eprintln!("Failed to read api key: {e} from: {api_key_path}"); + std::process::exit(1); + } + }; let api_key = api_key.trim(); let wine_dir = env::var("WINEPREFIX").unwrap_or(env::var("HOME").unwrap() + ".wine"); let dirs = vec![ wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/CODEX", wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/RUNE", ]; - let games = achievement_watcher::get_achievement_data_all( - &client, - api_key, - &game_name_cache_path, - &game_name_list, + Context { + api_key: api_key.to_owned(), dirs, - ) - .await; - // for game in &games { - // for achievement in game.achievements.iter() { - // let image = client - // .get(&achievement.icon) - // .send() - // .await - // .unwrap() - // .bytes() - // .await - // .unwrap(); - // fs::write(format!("icons/{}_{}.jpg", game.name, achievement.name), image).unwrap(); - // } - // break; - // } - let args: Vec = std::env::args().skip(1).collect(); - if args.is_empty() { - ui::ui(games).unwrap(); - } else if args.len() == 3 { - match args[0].as_str() { - // "-w" => watch::watch_file(&args[1], args[2].parse().unwrap()).unwrap(), - _ => todo!(), - } - } else { - panic!("wrong arg count"); + game_name_list, + game_name_cache_path, + unlocked_icon_path, } } diff --git a/src/steam.rs b/src/steam.rs index dba951d..f9e731b 100644 --- a/src/steam.rs +++ b/src/steam.rs @@ -8,6 +8,7 @@ use std::fs; pub struct JsonGame { pub game: JsonGameContent, } + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct JsonGameContent { @@ -128,7 +129,7 @@ pub async fn get_game_name( None } -pub fn get_achievement_data_all_ini(dirs: Vec) -> Vec { +pub fn get_achievement_data_all_ini(dirs: &Vec) -> Vec { let mut ini_games = Vec::new(); for dir in dirs { let game_dirs = fs::read_dir(dir).unwrap(); diff --git a/src/util.rs b/src/util.rs index bc5b82b..67e8a41 100644 --- a/src/util.rs +++ b/src/util.rs @@ -14,9 +14,9 @@ pub fn get_game_name_file_cache(path: &str) -> Vec { .collect() } -fn send_notification(icon_path: &str, achievement_name: &str, achievement_description: &str) { +pub fn send_notification(game_name: &str, achievement_name: &str, achievement_description: Option<&str>, icon_path: Option<&str>) { std::process::Command::new("notify-send") - .args(["-i", icon_path, achievement_name, achievement_description]) + .args(["-a", game_name, "-i", icon_path.unwrap_or(""), achievement_name, achievement_description.unwrap_or("")]) .spawn() .unwrap(); } diff --git a/src/watch.rs b/src/watch.rs index 8d973f8..f0a8c87 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,11 +1,15 @@ -use achievement_watcher::steam; -use notify::{Event, RecursiveMode, Result, Watcher}; +use achievement_watcher::{util, Achievement, Context, Game}; +use notify::{event::AccessKind, Event, EventKind, RecursiveMode, Result, Watcher}; +use reqwest::Client; use std::path::Path; use std::sync::mpsc; -use reqwest::Client; -use achievement_watcher::steam::App; -pub fn watch_file(path: &str, app_id: u32, api_key: &str, game_name_cache_path: &str, game_name_list: Vec) -> Result<()> { +pub async fn watch_file( + path: &str, + app_id: u32, + context: &Context, + games: &mut Vec, +) -> Result<()> { let (tx, rx) = mpsc::channel::>(); let mut watcher = notify::recommended_watcher(tx)?; @@ -15,10 +19,12 @@ pub fn watch_file(path: &str, app_id: u32, api_key: &str, game_name_cache_path: for res in rx { match res { Ok(event) => { - println!("event: {:?}", event); - let ini_game = steam::parse_ini_file(path, app_id); - let json_game = steam::get_steam_data(&client, api_key, app_id, "en"); - let result = achievement_watcher::get_achievement_data(&client, api_key, app_id, game_name_cache_path, &game_name_list); + println!("event: {:#?}", event); + if let EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write)) = + event.kind + { + check_for_new_achievement(&client, games, app_id, context).await; + } } Err(e) => panic!("watch error: {:?}", e), } @@ -26,3 +32,59 @@ pub fn watch_file(path: &str, app_id: u32, api_key: &str, game_name_cache_path: Ok(()) } + +async fn check_for_new_achievement( + client: &Client, + games: &mut Vec, + app_id: u32, + context: &Context, +) { + let new = achievement_watcher::get_achievement_data(&client, app_id, context) + .await + .unwrap(); + let old = games.iter().find(|m| m.app_id == app_id); + if let Some(old) = old { + let indexes = compare_achievements(&new.achievements, &old.achievements); + 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(); + util::send_notification( + &new.name, + &new_achievement.name, + new_achievement.description.as_deref(), + Some(&context.unlocked_icon_path), + ); + } + } else { + println!("\n\nWATCHER: did not find the game"); + } + let u_old = games.iter_mut().find(|m| m.app_id == app_id).unwrap(); + *u_old = new; +} + +fn compare_achievements(new: &Vec, old: &Vec) -> Vec { + let mut indexes = Vec::new(); + for (i, new_a) in new.iter().enumerate() { + if old + .iter() + .find(|m| m.name == new_a.name && !m.achieved && new_a.achieved) + .is_some() + { + println!("\n\nWATCHER: found new achievement {}", new_a.name); + indexes.push(i); + } + } + indexes +}