more watcher work, TODO: watch directories instead of files

This commit is contained in:
2025-01-25 02:32:43 +01:00
parent da93898b64
commit 2793ae830a
5 changed files with 176 additions and 89 deletions

View File

@@ -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<App>,
) -> Game {
pub struct Context {
pub api_key: String,
pub game_name_cache_path: String,
pub unlocked_icon_path: String,
pub game_name_list: Vec<App>,
pub dirs: Vec<String>,
}
pub async fn get_achievement_data(client: &Client, app_id: u32, context: &Context) -> Option<Game> {
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<App>,
dirs: Vec<String>,
) -> Vec<Game> {
pub async fn get_achievement_data_all(client: &Client, context: &Context) -> Vec<Game> {
let mut games: Vec<Game> = 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 {

View File

@@ -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<String> = 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<String> = 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,
}
}

View File

@@ -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<String>) -> Vec<IniGame> {
pub fn get_achievement_data_all_ini(dirs: &Vec<String>) -> Vec<IniGame> {
let mut ini_games = Vec::new();
for dir in dirs {
let game_dirs = fs::read_dir(dir).unwrap();

View File

@@ -14,9 +14,9 @@ pub fn get_game_name_file_cache(path: &str) -> Vec<App> {
.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();
}

View File

@@ -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<App>) -> Result<()> {
pub async fn watch_file(
path: &str,
app_id: u32,
context: &Context,
games: &mut Vec<Game>,
) -> Result<()> {
let (tx, rx) = mpsc::channel::<Result<Event>>();
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<Game>,
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<Achievement>, old: &Vec<Achievement>) -> Vec<usize> {
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
}