add gui, watch wip
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
/target
|
||||
*.json
|
||||
/icons
|
||||
Cargo.lock
|
||||
|
||||
1694
Cargo.lock
generated
1694
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,12 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0.0"
|
||||
eframe = "0.30.0"
|
||||
egui_extras = { version = "0.30.0", features = ["default", "all_loaders"] }
|
||||
env_logger = "0.11.6"
|
||||
futures = "0.3.31"
|
||||
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png"] }
|
||||
notify = "8.0.0"
|
||||
reqwest = "0.12.12"
|
||||
rust-ini = "0.21.1"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
|
||||
131
src/lib.rs
Normal file
131
src/lib.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use reqwest::Client;
|
||||
use steam::App;
|
||||
|
||||
pub mod steam;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
pub mod util;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Game {
|
||||
pub app_id: u32,
|
||||
pub name: String,
|
||||
pub achievements: Vec<Achievement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Achievement {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub achieved: bool,
|
||||
pub hidden: bool,
|
||||
pub icon: String,
|
||||
pub icongray: String,
|
||||
pub current_progress: u32,
|
||||
pub max_progress: u32,
|
||||
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 {
|
||||
let mut game = Game {
|
||||
name: steam::get_game_name(client, game_name_cache_path, 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;
|
||||
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(),
|
||||
});
|
||||
}
|
||||
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> {
|
||||
let mut games: Vec<Game> = Vec::new();
|
||||
let ini_games = steam::get_achievement_data_all_ini(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(),
|
||||
};
|
||||
let game_data = steam::get_steam_data(client, 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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
games.push(game);
|
||||
}
|
||||
games
|
||||
}
|
||||
366
src/main.rs
366
src/main.rs
@@ -1,75 +1,8 @@
|
||||
use dirs;
|
||||
use ini::Ini;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::{env, fs};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct JsonGame {
|
||||
game: JsonGameContent,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct JsonGameContent {
|
||||
game_name: String,
|
||||
game_version: String,
|
||||
available_game_stats: JsonAvailableGameStats,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct JsonAvailableGameStats {
|
||||
achievements: Vec<JsonAchievement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct JsonAchievement {
|
||||
name: String,
|
||||
defaultvalue: i32,
|
||||
display_name: String,
|
||||
hidden: i32,
|
||||
description: Option<String>,
|
||||
icon: String,
|
||||
icongray: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IniAchievement {
|
||||
name: String,
|
||||
achieved: bool,
|
||||
current_progress: u32,
|
||||
max_progress: u32,
|
||||
unlock_time: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IniGame {
|
||||
id: u32,
|
||||
achievements: Vec<IniAchievement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Game {
|
||||
app_id: u32,
|
||||
name: String,
|
||||
achievements: Vec<Achievement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Achievement {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
achieved: bool,
|
||||
hidden: bool,
|
||||
icon: String,
|
||||
icongray: String,
|
||||
current_progress: u32,
|
||||
max_progress: u32,
|
||||
unlock_time: u32,
|
||||
}
|
||||
mod ui;
|
||||
mod watch;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -77,7 +10,7 @@ async fn main() {
|
||||
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 game_name_list = get_game_name_file_cache(&game_name_cache_path);
|
||||
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 = api_key.trim();
|
||||
@@ -86,7 +19,7 @@ async fn main() {
|
||||
wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/CODEX",
|
||||
wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/RUNE",
|
||||
];
|
||||
let games = get_achievement_data_all(
|
||||
let games = achievement_watcher::get_achievement_data_all(
|
||||
&client,
|
||||
api_key,
|
||||
&game_name_cache_path,
|
||||
@@ -94,274 +27,29 @@ async fn main() {
|
||||
dirs,
|
||||
)
|
||||
.await;
|
||||
println!("games: {:#?}", games);
|
||||
}
|
||||
|
||||
impl IniGame {
|
||||
fn new(id: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
achievements: Vec::new(),
|
||||
// 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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IniAchievement {
|
||||
fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
achieved: false,
|
||||
current_progress: 0,
|
||||
max_progress: 0,
|
||||
unlock_time: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_steam_data(client: &Client, key: &str, appid: u32, lang: &str) -> JsonGame {
|
||||
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();
|
||||
serde_json::from_str(&json).unwrap()
|
||||
}
|
||||
|
||||
async fn get_achievement_data(
|
||||
client: &Client,
|
||||
api_key: &str,
|
||||
app_id: u32,
|
||||
game_name_cache_path: &str,
|
||||
game_name_list: &Vec<App>,
|
||||
) -> Game {
|
||||
let mut game = Game {
|
||||
name: get_game_name(&client, &game_name_cache_path, &game_name_list, app_id)
|
||||
.await
|
||||
.unwrap(),
|
||||
app_id,
|
||||
achievements: Vec::new(),
|
||||
};
|
||||
let game_data = get_steam_data(&client, api_key, app_id, "en").await;
|
||||
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(),
|
||||
});
|
||||
}
|
||||
game
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut games: Vec<Game> = Vec::new();
|
||||
let ini_games = get_achievement_data_all_ini(dirs);
|
||||
for ini_game in ini_games {
|
||||
let mut game = Game {
|
||||
app_id: ini_game.id,
|
||||
achievements: Vec::new(),
|
||||
name: get_game_name(&client, &game_name_cache_path, &game_name_list, ini_game.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
};
|
||||
let game_data = get_steam_data(&client, 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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
games.push(game);
|
||||
}
|
||||
games
|
||||
}
|
||||
|
||||
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();
|
||||
for game_dir in game_dirs {
|
||||
let game_dir = game_dir.unwrap();
|
||||
let mut ini_game = IniGame::new(
|
||||
game_dir
|
||||
.file_name()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
let game_dir_path = game_dir.path().to_str().unwrap().to_owned();
|
||||
let i = Ini::load_from_file(format!("{game_dir_path}/achievements.ini",)).unwrap();
|
||||
for (sec, prop) in i.iter() {
|
||||
if let Some(section) = sec {
|
||||
if section == "SteamAchievements" {
|
||||
continue;
|
||||
}
|
||||
let mut ini_achievement = IniAchievement::new(section.to_owned());
|
||||
for (k, v) in prop.iter() {
|
||||
match k {
|
||||
"Achieved" => ini_achievement.achieved = v.parse::<u32>().unwrap() == 1,
|
||||
"CurProgress" => ini_achievement.current_progress = v.parse().unwrap(),
|
||||
"MaxProgress" => ini_achievement.max_progress = v.parse().unwrap(),
|
||||
"UnlockTime" => ini_achievement.unlock_time = v.parse().unwrap(),
|
||||
s => panic!("unexpected achievement stat {s}"),
|
||||
}
|
||||
}
|
||||
ini_game.achievements.push(ini_achievement);
|
||||
}
|
||||
}
|
||||
ini_games.push(ini_game);
|
||||
}
|
||||
}
|
||||
ini_games
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AppList {
|
||||
applist: Apps,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Apps {
|
||||
apps: Vec<App>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct App {
|
||||
name: String,
|
||||
appid: u32,
|
||||
}
|
||||
|
||||
async fn get_game_name(
|
||||
client: &Client,
|
||||
path: &str,
|
||||
game_name_list: &Vec<App>,
|
||||
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());
|
||||
}
|
||||
}
|
||||
let json = client
|
||||
.get("https://api.steampowered.com/ISteamApps/GetAppList/v0002/?format=json")
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
let applist: AppList = serde_json::from_str(&json).unwrap();
|
||||
let applist = applist.applist.apps;
|
||||
for app in applist {
|
||||
if app.appid == appid {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
writeln!(file, "{}:{}", app.appid, app.name).unwrap();
|
||||
return Some(app.name);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn get_game_name_file_cache(path: &str) -> Vec<App> {
|
||||
std::fs::read_to_string(path)
|
||||
.unwrap()
|
||||
.lines()
|
||||
.map(|m| {
|
||||
let a = m.split_once(":").unwrap();
|
||||
App {
|
||||
appid: a.0.parse().unwrap(),
|
||||
name: a.1.to_owned(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_id_to_name() {
|
||||
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 game_name_list = 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 = api_key.trim();
|
||||
let game = get_achievement_data(
|
||||
&client,
|
||||
api_key,
|
||||
570940,
|
||||
&game_name_cache_path,
|
||||
&game_name_list,
|
||||
)
|
||||
.await;
|
||||
println!("game name: \"{}\"", game.name);
|
||||
assert_eq!(game.name, "DARK SOULS™: REMASTERED");
|
||||
} else {
|
||||
panic!("wrong arg count");
|
||||
}
|
||||
}
|
||||
|
||||
174
src/steam.rs
Normal file
174
src/steam.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use ini::Ini;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JsonGame {
|
||||
pub game: JsonGameContent,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JsonGameContent {
|
||||
pub game_name: String,
|
||||
pub game_version: String,
|
||||
pub available_game_stats: JsonAvailableGameStats,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JsonAvailableGameStats {
|
||||
pub achievements: Vec<JsonAchievement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JsonAchievement {
|
||||
pub name: String,
|
||||
pub defaultvalue: i32,
|
||||
pub display_name: String,
|
||||
pub hidden: i32,
|
||||
pub description: Option<String>,
|
||||
pub icon: String,
|
||||
pub icongray: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IniAchievement {
|
||||
pub name: String,
|
||||
pub achieved: bool,
|
||||
pub current_progress: u32,
|
||||
pub max_progress: u32,
|
||||
pub unlock_time: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IniGame {
|
||||
pub id: u32,
|
||||
pub achievements: Vec<IniAchievement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppList {
|
||||
pub applist: Apps,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Apps {
|
||||
pub apps: Vec<App>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct App {
|
||||
pub name: String,
|
||||
pub appid: u32,
|
||||
}
|
||||
|
||||
impl IniGame {
|
||||
fn new(id: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
achievements: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IniAchievement {
|
||||
fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
achieved: false,
|
||||
current_progress: 0,
|
||||
max_progress: 0,
|
||||
unlock_time: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_steam_data(client: &Client, key: &str, appid: u32, lang: &str) -> JsonGame {
|
||||
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();
|
||||
serde_json::from_str(&json).unwrap()
|
||||
}
|
||||
|
||||
pub async fn get_game_name(
|
||||
client: &Client,
|
||||
path: &str,
|
||||
game_name_list: &Vec<App>,
|
||||
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());
|
||||
}
|
||||
}
|
||||
let json = client
|
||||
.get("https://api.steampowered.com/ISteamApps/GetAppList/v0002/?format=json")
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
let applist: AppList = serde_json::from_str(&json).unwrap();
|
||||
let applist = applist.applist.apps;
|
||||
for app in applist {
|
||||
if app.appid == appid {
|
||||
let mut file = OpenOptions::new().append(true).open(path).unwrap();
|
||||
writeln!(file, "{}:{}", app.appid, app.name).unwrap();
|
||||
return Some(app.name);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
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();
|
||||
for game_dir in game_dirs {
|
||||
let game_dir = game_dir.unwrap();
|
||||
let app_id = game_dir
|
||||
.file_name()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.parse()
|
||||
.unwrap();
|
||||
let game_dir_path = game_dir.path().to_str().unwrap().to_owned();
|
||||
let ini_game = parse_ini_file(&format!("{game_dir_path}/achievements.ini"), app_id);
|
||||
ini_games.push(ini_game);
|
||||
}
|
||||
}
|
||||
ini_games
|
||||
}
|
||||
|
||||
pub fn parse_ini_file(path: &str, app_id: u32) -> IniGame {
|
||||
let mut ini_game = IniGame::new(app_id);
|
||||
let ini = Ini::load_from_file(path).unwrap();
|
||||
for (sec, prop) in ini.iter() {
|
||||
if let Some(section) = sec {
|
||||
if section == "SteamAchievements" {
|
||||
continue;
|
||||
}
|
||||
let mut ini_achievement = IniAchievement::new(section.to_owned());
|
||||
for (k, v) in prop.iter() {
|
||||
match k {
|
||||
"Achieved" => ini_achievement.achieved = v.parse::<u32>().unwrap() == 1,
|
||||
"CurProgress" => ini_achievement.current_progress = v.parse().unwrap(),
|
||||
"MaxProgress" => ini_achievement.max_progress = v.parse().unwrap(),
|
||||
"UnlockTime" => ini_achievement.unlock_time = v.parse().unwrap(),
|
||||
s => panic!("unexpected achievement stat {s}"),
|
||||
}
|
||||
}
|
||||
ini_game.achievements.push(ini_achievement);
|
||||
}
|
||||
}
|
||||
ini_game
|
||||
}
|
||||
32
src/test.rs
Normal file
32
src/test.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::fs;
|
||||
use reqwest::Client;
|
||||
//use achievement_watcher::noe
|
||||
use crate::util;
|
||||
use crate::get_achievement_data;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_id_to_name() {
|
||||
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 game_name_list = 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 = api_key.trim();
|
||||
let game = get_achievement_data(
|
||||
&client,
|
||||
api_key,
|
||||
570940,
|
||||
&game_name_cache_path,
|
||||
&game_name_list,
|
||||
)
|
||||
.await;
|
||||
println!("game name: \"{}\"", game.name);
|
||||
assert_eq!(game.name, "DARK SOULS™: REMASTERED");
|
||||
}
|
||||
}
|
||||
60
src/ui.rs
Normal file
60
src/ui.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use achievement_watcher::Game;
|
||||
use eframe::egui;
|
||||
|
||||
pub fn ui(games: Vec<Game>) -> eframe::Result {
|
||||
env_logger::init();
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut selected_game = 0;
|
||||
|
||||
eframe::run_simple_native("Achievement Watcher", options, move |ctx, _frame| {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::SidePanel::left("left_panel").show_inside(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
for (i, game) in games.iter().enumerate() {
|
||||
if ui.button(&game.name).clicked() {
|
||||
selected_game = i;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
let game = &games[selected_game];
|
||||
let achieved_count = game.achievements.iter().filter(|m| m.achieved).count();
|
||||
ui.heading(&game.name);
|
||||
ui.add(
|
||||
egui::Image::new(format!(
|
||||
"https://cdn.cloudflare.steamstatic.com/steam/apps/{}/header.jpg",
|
||||
game.app_id
|
||||
))
|
||||
.fit_to_original_size(1.),
|
||||
);
|
||||
ui.label(format!(
|
||||
"Completed: [{}/{}]",
|
||||
achieved_count,
|
||||
game.achievements.len()
|
||||
));
|
||||
ui.add(egui::ProgressBar::new(
|
||||
achieved_count as f32 / game.achievements.len() as f32,
|
||||
));
|
||||
|
||||
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
|
||||
} else {
|
||||
&achievement.icongray
|
||||
};
|
||||
ui.add(egui::Image::new(icon).fit_to_original_size(1.));
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
23
src/util.rs
Normal file
23
src/util.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use crate::steam::App;
|
||||
|
||||
pub fn get_game_name_file_cache(path: &str) -> Vec<App> {
|
||||
std::fs::read_to_string(path)
|
||||
.unwrap()
|
||||
.lines()
|
||||
.map(|m| {
|
||||
let a = m.split_once(":").unwrap();
|
||||
App {
|
||||
appid: a.0.parse().unwrap(),
|
||||
name: a.1.to_owned(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn send_notification(icon_path: &str, achievement_name: &str, achievement_description: &str) {
|
||||
std::process::Command::new("notify-send")
|
||||
.args(["-i", icon_path, achievement_name, achievement_description])
|
||||
.spawn()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
28
src/watch.rs
Normal file
28
src/watch.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use achievement_watcher::steam;
|
||||
use notify::{Event, RecursiveMode, Result, Watcher};
|
||||
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<()> {
|
||||
let (tx, rx) = mpsc::channel::<Result<Event>>();
|
||||
|
||||
let mut watcher = notify::recommended_watcher(tx)?;
|
||||
|
||||
watcher.watch(Path::new(path), RecursiveMode::NonRecursive)?;
|
||||
let client = Client::new();
|
||||
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);
|
||||
}
|
||||
Err(e) => panic!("watch error: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user