some minor restructuring and wip errors

This commit is contained in:
2025-01-27 21:10:22 +01:00
parent 91851557c6
commit ebab98dbaa
7 changed files with 242 additions and 188 deletions

View File

@@ -22,4 +22,5 @@ reqwest = "0.12.12"
rust-ini = "0.21.1"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.137"
thiserror = "2.0.11"
tokio = { version = "1.43.0", default-features = false, features = ["rt-multi-thread", "macros"] }

View File

@@ -1,10 +1,13 @@
use crate::steam::SteamApp;
use reqwest::Client;
use steam::App;
use std::path::Path;
pub mod steam;
#[cfg(test)]
mod test;
pub mod ui;
pub mod util;
pub mod watch;
#[derive(Debug)]
pub struct Game {
@@ -28,10 +31,10 @@ pub struct Achievement {
pub struct Context {
pub api_key: String,
pub game_name_cache_path: String,
pub json_cache_path: String,
pub icon_cache_path: String,
pub game_name_list: Vec<App>,
pub game_name_cache_path: Box<Path>,
pub json_cache_path: Box<Path>,
pub icon_cache_path: Box<Path>,
pub game_name_list: Vec<SteamApp>,
pub dirs: Vec<String>,
}
@@ -48,7 +51,14 @@ 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", &context.json_cache_path).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 {
@@ -75,7 +85,8 @@ pub async fn get_achievement_data(client: &Client, app_id: u32, context: &Contex
achievement.max_progress = ini_achievement.max_progress;
achievement.current_progress = ini_achievement.current_progress;
achievement.unlock_time = ini_achievement.unlock_time;
}
break;
}
}
game.achievements.push(achievement);
}
@@ -98,50 +109,42 @@ 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", &context.json_cache_path).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;
let mut achievement = Achievement {
achieved: false,
unlock_time: 0,
max_progress: 0,
current_progress: 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,
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(),
unlock_time: ini_achievement.unlock_time,
});
found_in_ini = true;
achievement.achieved = ini_achievement.achieved;
achievement.unlock_time = ini_achievement.unlock_time;
achievement.max_progress = ini_achievement.max_progress;
achievement.current_progress = ini_achievement.current_progress;
break;
}
}
if !found_in_ini {
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(),
});
}
game.achievements.push(achievement);
}
games.push(game);
}

View File

@@ -1,23 +1,19 @@
use achievement_watcher::{Context, Game};
use achievement_watcher::{ui, util, watch};
use reqwest::Client;
use std::{env, fs};
mod ui;
mod watch;
#[tokio::main]
async fn main() {
let client = Client::new();
let context = match setup() {
Some(v) => v,
None => {
eprintln!("ERROR: was not able to create context");
let context = match util::setup() {
Ok(v) => v,
Err(e) => {
eprintln!("ERROR: {e}");
std::process::exit(1);
}
};
let mut games = achievement_watcher::get_achievement_data_all(&client, &context).await;
cache_icons(&context, &games).await;
util::cache_icons(&context, &games).await;
let args: Vec<String> = std::env::args().skip(1).collect();
if args.is_empty() {
@@ -34,109 +30,3 @@ async fn main() {
panic!("wrong arg count");
}
}
fn setup() -> Option<Context> {
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).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).ok()?,
Err(e) => eprintln!("error in setup: cache dir: {e}"),
Ok(true) => (),
}
let game_name_cache_path = format!("{cache_dir}/game_names.txt");
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) {
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").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",
];
Some(Context {
api_key: api_key.to_owned(),
dirs,
game_name_list,
game_name_cache_path,
json_cache_path,
icon_cache_path,
})
}
async fn cache_icons(context: &Context, games: &Vec<Game>) {
let client = Client::new();
let mut futures = Vec::new();
for game in games {
let header = client
.get(format!(
"https://cdn.cloudflare.steamstatic.com/steam/apps/{}/header.jpg",
game.app_id
))
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
fs::write(
&format!("{}/{}/header.jpg", context.icon_cache_path, game.name),
header,
)
.unwrap();
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();
}
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();
}
};
futures.push(future);
}
}
futures::future::join_all(futures).await;
}

View File

@@ -2,7 +2,9 @@ use ini::Ini;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::fs::OpenOptions;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
@@ -60,12 +62,12 @@ pub struct AppList {
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Apps {
pub apps: Vec<App>,
pub apps: Vec<SteamApp>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct App {
pub struct SteamApp {
pub name: String,
pub appid: u32,
}
@@ -96,10 +98,11 @@ pub async fn get_steam_data(
key: &str,
appid: u32,
lang: &str,
json_cache_path: &str,
json_cache_path: &Path,
) -> JsonGame {
let json_file_path = format!("{json_cache_path}/{appid}.json");
if Path::new(&json_file_path).exists() {
let mut json_file_path: PathBuf = json_cache_path.to_path_buf();
json_file_path.push(format!("{appid}.json"));
if json_file_path.exists() {
let json = fs::read_to_string(json_file_path).unwrap();
return serde_json::from_str(&json).unwrap();
}
@@ -111,12 +114,10 @@ pub async fn get_steam_data(
pub async fn get_game_name(
client: &Client,
path: &str,
game_name_list: &Vec<App>,
path: &Path,
game_name_list: &Vec<SteamApp>,
appid: u32,
) -> Option<String> {
use std::fs::OpenOptions;
use std::io::prelude::*;
for app in game_name_list {
if app.appid == appid {
return Some(app.name.clone());

View File

@@ -1,4 +1,4 @@
use achievement_watcher::{Context, Game};
use crate::{Context, Game};
use eframe::egui;
use std::path::Path;
@@ -30,7 +30,8 @@ pub fn ui(context: Context, games: Vec<Game>) -> eframe::Result {
ui.add(
egui::Image::new(format!(
"file://{}/{}/header.jpg",
context.icon_cache_path, game.name
context.icon_cache_path.to_str().unwrap(),
game.name
))
.fit_to_original_size(1.),
);
@@ -51,7 +52,7 @@ pub fn ui(context: Context, games: Vec<Game>) -> eframe::Result {
}
let icon_path = format!(
"{}/{}/{}{}.png",
context.icon_cache_path,
context.icon_cache_path.to_str().unwrap(),
game.name,
achievement.name,
if achievement.achieved { "" } else { "gray" }

View File

@@ -1,12 +1,40 @@
use crate::steam::App;
use std::path::PathBuf;
use crate::{steam::SteamApp, Context, Game};
use thiserror::Error;
use reqwest::Client;
use std::{env, fs};
pub fn get_game_name_file_cache(path: &str) -> Vec<App> {
#[derive(Error, Debug)]
pub enum SetupError {
#[error("failed to get configuration directory")]
GetConfigDir,
#[error("failed to create configuration directory: \"{0}\"")]
CreateConfigDir(std::io::Error, PathBuf),
#[error("failed to get cache directory")]
GetCacheDir,
#[error("failed to create cache directory: \"{1}\", with err: \"{0}\"")]
CreateCacheDir(std::io::Error, PathBuf),
#[error("failed to create cache file: \"{1}\", with err: \"{0}\"")]
CreateCacheFile(std::io::Error, PathBuf),
#[error("api key does not exist: \"{0}\"")]
NoApiKey(PathBuf),
#[error("failed to read api key from path: \"{1}\", with err: \"{0}\"")]
ReadApiKey(std::io::Error, PathBuf),
#[error("no home dir found: \"{0}\"")]
NoHomeDir(std::env::VarError),
}
pub fn get_game_name_file_cache(path: &PathBuf) -> Vec<SteamApp> {
std::fs::read_to_string(path)
.unwrap()
.lines()
.map(|m| {
let a = m.split_once(":").unwrap();
App {
SteamApp {
appid: a.0.parse().unwrap(),
name: a.1.to_owned(),
}
@@ -34,3 +62,128 @@ pub fn send_notification(
.wait()
.unwrap();
}
pub fn setup() -> Result<Context, SetupError> {
let mut config_dir = dirs::config_dir().ok_or(SetupError::GetConfigDir)?;
config_dir.push("achievement-watcher");
if !config_dir.exists() {
fs::create_dir(&config_dir)
.map_err(|e| SetupError::CreateConfigDir(e, config_dir.clone()))?;
}
let mut cache_dir = dirs::cache_dir().ok_or(SetupError::GetCacheDir)?;
cache_dir.push("achievement-watcher");
if !cache_dir.exists() {
fs::create_dir(&cache_dir).map_err(|e| SetupError::CreateCacheDir(e, cache_dir.clone()))?;
}
let mut json_cache_path = cache_dir.clone();
json_cache_path.push("json");
if !json_cache_path.exists() {
fs::create_dir(&json_cache_path)
.map_err(|e| SetupError::CreateCacheDir(e, json_cache_path.clone()))?;
}
let mut icon_cache_path = cache_dir.clone();
icon_cache_path.push("icon");
if !icon_cache_path.exists() {
fs::create_dir(&icon_cache_path)
.map_err(|e| SetupError::CreateCacheDir(e, icon_cache_path.clone()))?;
}
let mut game_name_cache_path = cache_dir.clone();
game_name_cache_path.push("game_names.txt");
if !game_name_cache_path.exists() {
fs::write(&game_name_cache_path, "")
.map_err(|e| SetupError::CreateCacheFile(e, game_name_cache_path.clone()))?;
}
let game_name_list = crate::util::get_game_name_file_cache(&game_name_cache_path);
let mut api_key_path = config_dir.clone();
api_key_path.push("api-key.txt");
if !api_key_path.exists() {
return Err(SetupError::NoApiKey(api_key_path.clone()));
}
let api_key = std::fs::read_to_string(&api_key_path)
.map_err(|e| SetupError::ReadApiKey(e, api_key_path))?;
let api_key = api_key.trim();
let wine_dir = env::var("WINEPREFIX")
.unwrap_or(env::var("HOME").map_err(SetupError::NoHomeDir)? + ".wine");
let dirs = vec![
wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/CODEX",
wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/RUNE",
];
Ok(Context {
api_key: api_key.to_owned(),
dirs,
game_name_list,
game_name_cache_path: game_name_cache_path.into(),
json_cache_path: json_cache_path.into(),
icon_cache_path: icon_cache_path.into(),
})
}
pub async fn cache_icons(context: &Context, games: &Vec<Game>) {
let client = Client::new();
let mut futures = Vec::new();
for game in games {
let mut icon_dir: PathBuf = context.icon_cache_path.to_path_buf();
icon_dir.push(&game.name);
if !icon_dir.exists() {
std::fs::create_dir(&icon_dir).unwrap();
}
let header = client
.get(format!(
"https://cdn.cloudflare.steamstatic.com/steam/apps/{}/header.jpg",
game.app_id
))
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
let mut header_path = icon_dir.clone();
header_path.push("header.jpg");
fs::write(header_path, header).unwrap();
for achievement in &game.achievements {
let client = &client;
let mut icon_path = icon_dir.clone();
let mut icon_path_gray = icon_dir.clone();
let future = async move {
icon_path.push(format!("{}.png", achievement.name));
if !icon_path.exists() {
let data = client
.get(&achievement.icon)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
std::fs::write(&icon_path, &data).unwrap();
}
drop(icon_path);
icon_path_gray.push(format!("{}gray.png", achievement.name));
if !icon_path_gray.exists() {
let data = client
.get(&achievement.icongray)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
std::fs::write(&icon_path_gray, &data).unwrap();
}
};
futures.push(future);
}
}
futures::future::join_all(futures).await;
}

View File

@@ -1,10 +1,11 @@
use achievement_watcher::{util, Achievement, Context, Game};
use crate::{Achievement, Context, Game};
use crate::util;
use notify::{event::AccessKind, Event, EventKind, RecursiveMode, Result, Watcher};
use reqwest::Client;
use std::path::Path;
use std::sync::mpsc;
pub async fn watch_files(context: &Context, games: &mut Vec<Game>) -> Result<()> {
pub async fn watch_files(context: &Context, games: &mut [Game]) -> Result<()> {
let (tx, rx) = mpsc::channel::<Result<Event>>();
let mut watcher = notify::recommended_watcher(tx)?;
let client = Client::new();
@@ -43,11 +44,11 @@ pub async fn watch_files(context: &Context, games: &mut Vec<Game>) -> Result<()>
async fn check_for_new_achievement(
client: &Client,
games: &mut Vec<Game>,
games: &mut [Game],
app_id: u32,
context: &Context,
) {
let new = achievement_watcher::get_achievement_data(&client, app_id, context)
let new = crate::get_achievement_data(client, app_id, context)
.await
.unwrap();
let old = games.iter().find(|m| m.app_id == app_id);
@@ -57,8 +58,11 @@ async fn check_for_new_achievement(
let new_count = new.achievements.iter().filter(|m| m.achieved).count();
let old_count = old.achievements.iter().filter(|m| m.achieved).count();
if new_count <= old_count {
if new_count < old_count {
println!("WATCHER: new count: {}, old count: {}, not updating.", new_count, old_count);
if new_count < old_count {
println!(
"WATCHER: new count: {}, old count: {}, not updating.",
new_count, old_count
);
}
return;
}
@@ -71,7 +75,9 @@ async fn check_for_new_achievement(
let new_achievement = &new.achievements[i];
let icon_path = format!(
"{}/{}/{}.png",
context.icon_cache_path, old.name, new_achievement.name
context.icon_cache_path.to_str().unwrap(),
old.name,
new_achievement.name
);
let icon_path = Path::new(&icon_path);
@@ -103,12 +109,11 @@ async fn check_for_new_achievement(
*old = new;
}
fn compare_achievements(new: &Vec<Achievement>, old: &Vec<Achievement>) -> Vec<usize> {
fn compare_achievements(new: &[Achievement], old: &[Achievement]) -> Vec<usize> {
new.iter()
.map(|n| {
.filter_map(|n| {
old.iter()
.position(|o| !o.achieved && n.achieved && o.name == n.name)
})
.flatten()
.collect()
}