diff --git a/Cargo.lock b/Cargo.lock index bc03c9e..b23f2ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,6 +1092,8 @@ dependencies = [ "failure", "fl2rust", "fltk", + "nom 7.0.0", + "nom_locate", "reqwest", "serde 1.0.130", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index fd88dde..a7ee26a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,16 +5,18 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -steam_shortcuts_util = "1.1.4" -steamgriddb_api = "0.2,0" -serde = { version = "1.0.130", features = ["derive"] } -serde_json = "1.0.68" -tokio = { version = "1.11.0", features = ["full"] } -reqwest = { version = "0.11.4", features = ["default"] } -config = "0.11.0" -failure = "0.1.8" +steam_shortcuts_util = "^1.1.4" +steamgriddb_api = "^0.2,0" +serde = { version = "^1.0.130", features = ["derive"] } +serde_json = "^1.0.68" +tokio = { version = "^1.11.0", features = ["full"] } +reqwest = { version = "^0.11.4", features = ["default"] } +config = "^0.11.0" +failure = "^0.1.8" #https://fltk-rs.github.io/fltk-rs/ fltk = { version = "^1.2", features = ["fltk-bundled"], optional = true } +nom = "^7.0.*" +nom_locate = "^3.0.*" [build-dependencies] fl2rust = { version = "0.4", optional = true } diff --git a/src/defaultconfig.toml b/src/defaultconfig.toml index 6d6d461..b9f58c2 100644 --- a/src/defaultconfig.toml +++ b/src/defaultconfig.toml @@ -6,5 +6,8 @@ debug= false [steam] +[itch] +enabled = false + [steamgrid_db] enabled = true \ No newline at end of file diff --git a/src/itch/butler_db_parser.rs b/src/itch/butler_db_parser.rs new file mode 100644 index 0000000..15f8e36 --- /dev/null +++ b/src/itch/butler_db_parser.rs @@ -0,0 +1,73 @@ +use nom::{ + bytes::complete::{tag, take_until}, + multi::many0, + IResult, +}; + +pub(crate) struct DbPaths<'a> { + pub(crate) base_path: &'a str, + pub(crate) path: &'a str, +} + +pub(crate) fn parse_butler_db<'a>(content: &'a [u8]) -> nom::IResult<&[u8], Vec>> { + many0(parse_path)(content) +} + +fn parse_path<'a>(i: &'a [u8]) -> nom::IResult<&[u8], DbPaths<'a>> { + let prefix = "{\"basePath\":\""; + let suffix = "\",\"totalSize\""; + let (i, _taken) = take_until(prefix)(i)?; + let (i, _taken) = tag(prefix)(i)?; + let (i, base_path) = take_until(suffix)(i)?; + + let prefix = ":[{\"path\":\""; + let suffix = "\",\"depth"; + let (i, _taken) = take_until(prefix)(i)?; + let (i, _taken) = tag(prefix)(i)?; + let (i, path) = take_until(suffix)(i)?; + + IResult::Ok(( + i, + DbPaths { + base_path: std::str::from_utf8(base_path).unwrap(), + path: std::str::from_utf8(path).unwrap(), + }, + )) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn parse_itch_butler_db_test() { + let content = include_bytes!("../testdata/itch/butler.db-wal"); + let result = parse_butler_db(content); + assert!(result.is_ok()); + let (_r, paths) = result.unwrap(); + assert_eq!(paths.len(), 6); + + assert_eq!(paths[0].base_path, "/home/philip/.config/itch/apps/islands"); + assert_eq!(paths[0].path, "Islands_Linux.x86_64"); + assert_eq!( + paths[1].base_path, + "/home/philip/.config/itch/apps/night-in-the-woods" + ); + assert_eq!(paths[1].path, "Night in the Woods.x86_64"); + assert_eq!(paths[2].base_path, "/home/philip/.config/itch/apps/islands"); + assert_eq!(paths[2].path, "Islands_Linux.x86_64"); + assert_eq!( + paths[3].base_path, + "/home/philip/.config/itch/apps/overland" + ); + assert_eq!(paths[3].path, "Overland.x86_64"); + assert_eq!( + paths[4].base_path, + "/home/philip/.config/itch/apps/night-in-the-woods" + ); + assert_eq!(paths[4].path, "Night in the Woods.x86_64"); + assert_eq!(paths[5].base_path, "/home/philip/.config/itch/apps/islands"); + assert_eq!(paths[5].path, "Islands_Linux.x86_64"); + } +} diff --git a/src/itch/itch_game.rs b/src/itch/itch_game.rs new file mode 100644 index 0000000..25cb5c2 --- /dev/null +++ b/src/itch/itch_game.rs @@ -0,0 +1,35 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; + +#[derive(Serialize, Deserialize, Debug, Clone)] + +pub struct ItchGame { + pub install_path: String, + pub executable: String, + pub title: String, +} + +impl From for ShortcutOwned { + fn from(game: ItchGame) -> Self { + let exe = Path::new(&game.install_path).join(&game.executable); + let exe = exe.to_str().unwrap().to_string(); + let shortcut = Shortcut::new( + 0, + game.title.as_str(), + exe.as_str(), + &game.install_path, + exe.as_str(), + "", + "", + ); + + let mut owned_shortcut = shortcut.to_owned(); + owned_shortcut.tags.push("Itch".to_owned()); + owned_shortcut.tags.push("Ready TO Play".to_owned()); + owned_shortcut.tags.push("Installed".to_owned()); + + owned_shortcut + } +} diff --git a/src/itch/itch_platform.rs b/src/itch/itch_platform.rs new file mode 100644 index 0000000..01d0fa5 --- /dev/null +++ b/src/itch/itch_platform.rs @@ -0,0 +1,86 @@ +use super::butler_db_parser::*; +use super::{ItchGame, ItchSettings}; +use crate::platform::Platform; +use failure::*; +use std::path::Path; + +pub struct ItchPlatform { + settings: ItchSettings, +} + +impl ItchPlatform { + pub fn new(settings: ItchSettings) -> ItchPlatform { + ItchPlatform { settings } + } +} + +impl Platform for ItchPlatform { + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn name(&self) -> &str { + "Itch" + } + + fn get_shortcuts(&self) -> Result, ItchErrors> { + let itch_location = self.settings.location.clone(); + let itch_location = itch_location.unwrap_or_else(get_default_location); + + let itch_db_location = Path::new(&itch_location).join("db").join("butler.db-wal"); + if !itch_db_location.exists() { + return Err(ItchErrors::PathNotFound { + path: itch_db_location.to_str().unwrap().to_string(), + }); + } + + let shortcut_bytes = std::fs::read(&itch_db_location).unwrap(); + + let res = match parse_butler_db(&shortcut_bytes) { + Ok((_, shortcuts)) => Ok(shortcuts), + Err(e) => Err(ItchErrors::ParseError { + error: e.to_string(), + path: itch_db_location.to_str().unwrap().to_string(), + }), + }?; + + let res = res + .iter() + .map(|paths| ItchGame { + install_path: paths.base_path.to_owned(), + executable: paths.path.to_owned(), + title: paths.path.to_owned(), + }) + .collect(); + Ok(res) + } +} + +#[cfg(target_os = "linux")] +fn get_default_location() -> String { + //If we don't have a home drive we have to just die + let home = std::env::var("HOME").expect("Expected a home variable to be defined"); + format!("{}/.config/itch/", home) +} + +#[cfg(target_os = "windows")] +fn get_default_location() -> String { + let key = "PROGRAMFILES(X86)"; + let program_files = env::var(key).expect("Expected a program files variable to be defined"); + format!("{}//Itch//", program_files) +} + +#[derive(Debug, Fail)] +pub enum ItchErrors { + #[fail( + display = "Itch path: {} could not be found. Try to specify a different path for the Itch.", + path + )] + PathNotFound { path: String }, + + #[fail(display = "Could not read Itch db at {} error: {}", path, error)] + ReadDirError { path: String, error: std::io::Error }, + + #[fail(display = "Could not parse Itch db at {} error: {}", path, error)] + ParseError { path: String, error: String }, +} diff --git a/src/itch/mod.rs b/src/itch/mod.rs new file mode 100644 index 0000000..2e2b7c1 --- /dev/null +++ b/src/itch/mod.rs @@ -0,0 +1,8 @@ +mod settings; +mod itch_game; +mod itch_platform; +mod butler_db_parser; + +pub use settings::*; +pub use itch_game::*; +pub use itch_platform::*; \ No newline at end of file diff --git a/src/itch/settings.rs b/src/itch/settings.rs new file mode 100644 index 0000000..2115f89 --- /dev/null +++ b/src/itch/settings.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct ItchSettings { + pub enabled: bool, + pub location: Option, +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a354ea2..63ace47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,10 @@ -use crate::steamgriddb::{download_images, CachedSearch}; +use crate::{ + itch::ItchPlatform, + steamgriddb::{download_images, CachedSearch}, +}; use std::{fs::File, io::Write, path::Path}; mod egs; +mod itch; mod legendary; mod platform; mod settings; @@ -84,6 +88,11 @@ async fn run_sync() -> Result<(), Box> { &mut new_user_shortcuts, ); + update_platform_shortcuts( + &ItchPlatform::new(settings.itch.clone()), + &mut new_user_shortcuts, + ); + let shortcuts = new_user_shortcuts.iter().map(|f| f.borrow()).collect(); save_shortcuts(&shortcuts, Path::new(&shortcut_info.path)); diff --git a/src/settings.rs b/src/settings.rs index 7d9f916..f769274 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,4 +1,4 @@ -use crate::{egs::EpicGamesLauncherSettings, legendary::LegendarySettings, steam::SteamSettings, steamgriddb::SteamGridDbSettings}; +use crate::{egs::EpicGamesLauncherSettings, itch::ItchSettings, legendary::LegendarySettings, steam::SteamSettings, steamgriddb::SteamGridDbSettings}; use config::{Config, ConfigError, Environment, File}; use serde::Deserialize; @@ -9,6 +9,7 @@ pub struct Settings { pub debug: bool, pub epic_games: EpicGamesLauncherSettings, pub legendary: LegendarySettings, + pub itch: ItchSettings, pub steamgrid_db: SteamGridDbSettings, pub steam: SteamSettings } diff --git a/src/testdata/itch/butler.db-wal b/src/testdata/itch/butler.db-wal new file mode 100644 index 0000000..2f73552 Binary files /dev/null and b/src/testdata/itch/butler.db-wal differ