diff --git a/CHANGELOG.md b/CHANGELOG.md index dae0940..1e3463c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ +## 0.3.1 (2017-05-05) +- automatically check if grant is still valid and refresh if necessary (fixes #7) + ## 0.3.0 (2017-05-04) -- move hashes and content info into seperate files as to not clutter up the main config file. +- move hashes and content info into seperate files as to not clutter up the main config file ## 0.2.4 (2017-03-23) - fix a bug in serial key parsing diff --git a/Cargo.lock b/Cargo.lock index cd54faf..6bb8a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,9 +1,9 @@ [root] name = "gog-sync" -version = "0.3.0" +version = "0.3.1" dependencies = [ "chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.24.0 (registry+https://github.com/rust-lang/crates.io-index)", "curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "clap" -version = "2.23.3" +version = "2.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -429,7 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" "checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4" "checksum chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056" -"checksum clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f57e9b63057a545ad2ecd773ea61e49422ed1b1d63d74d5da5ecaee55b3396cd" +"checksum clap 2.24.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f31c42d0cecb245c1a0bee00ef433eb1bf253897fe472b6a3f4202e9dbbc4b25" "checksum curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c90e1240ef340dd4027ade439e5c7c2064dd9dc652682117bd50d1486a3add7b" "checksum curl-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "23e7e544dc5e1ba42c4a4a678bd47985e84b9c3f4d3404c29700622a029db9c3" "checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90" diff --git a/Cargo.toml b/Cargo.toml index 451cb3a..bb767a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gog-sync" -version = "0.3.0" +version = "0.3.1" authors = ["Sebastian Hugentobler "] description = "Synchronizes a GOG library with a local folder." documentation = "https://docs.rs/crate/gog-sync" diff --git a/src/gog.rs b/src/gog.rs index cb9b137..a7b55db 100644 --- a/src/gog.rs +++ b/src/gog.rs @@ -24,6 +24,7 @@ use std::fs::File; use std::io; use std::io::Write; use std::path::Path; +use std::path::PathBuf; /// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`. #[derive(Debug)] @@ -76,8 +77,6 @@ impl From for GogError { pub struct Gog<'a> { client_id: String, client_secret: String, - redirect_uri: String, - games_uri: String, http_client: &'a mut Http, } @@ -87,8 +86,6 @@ impl<'a> Gog<'a> { Gog { client_id: String::from("46899977096215655"), client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"), - redirect_uri: String::from("https://embed.gog.com/on_login_success?origin=client"), - games_uri: String::from("https://embed.gog.com/user/data/games"), http_client: http_client, } } @@ -98,6 +95,15 @@ impl<'a> Gog<'a> { /// will get an url which you have to open in a browser. There you have to /// login and copy the code parameter from the next page into the prompt. pub fn login(&mut self) -> Result<(), GogError> { + let token = self.refresh_token_from_file()?; + + let auth_header = format!("Authorization: Bearer {token}", token = token.access_token); + self.http_client.add_header(auth_header.as_str())?; + + Ok(()) + } + + fn refresh_token_from_file(&mut self) -> Result { let config = ConfigFiles::new(); let mut token: Token = match config.load("token.json") { Ok(value) => value, @@ -113,10 +119,7 @@ impl<'a> Gog<'a> { config.save("token.json", &token)?; - let auth_header = format!("Authorization: Bearer {token}", token = token.access_token); - self.http_client.add_header(auth_header.as_str())?; - - Ok(()) + Ok(token) } /// Syncs the contents of a gog account with a local folder. @@ -214,7 +217,7 @@ impl<'a> Gog<'a> { let data_uri = format!("https://embed.gog.com{}", data.manual_url); info!("downloading {} for {}...", &data.manual_url, &content.title); - let filename = self.http_client.download(data_uri.as_str(), &data_root)?; + let filename = self.api_request_download(data_uri.as_str(), &data_root)?; let data_info = DataInfo { hash: data_hash, @@ -222,16 +225,18 @@ impl<'a> Gog<'a> { language: data.language, }; - content_info.data.insert(data.manual_url.clone(), data_info); + content_info + .data + .insert(data.manual_url.clone(), data_info); ConfigFiles::save_to_path(&content_info_path, &content_info)?; } for extra in content.extras { - let extra_hash_saved = match content_info_saved.extras - .get(&content_id.to_string()) { - Some(value) => value.hash, - None => u64::min_value(), - }; + let extra_hash_saved = + match content_info_saved.extras.get(&content_id.to_string()) { + Some(value) => value.hash, + None => u64::min_value(), + }; let extra_hash = models::get_hash(&extra); @@ -243,7 +248,7 @@ impl<'a> Gog<'a> { let extra_uri = format!("https://embed.gog.com{}", extra.manual_url); info!("downloading {} for {}...", &extra.name, &content.title); - let filename = self.http_client.download(extra_uri.as_str(), &content_root)?; + let filename = self.api_request_download(extra_uri.as_str(), &content_root)?; let extra_info = ExtraInfo { hash: extra_hash, @@ -251,7 +256,9 @@ impl<'a> Gog<'a> { name: extra.name, }; - content_info.extras.insert(extra.manual_url.clone(), extra_info); + content_info + .extras + .insert(extra.manual_url.clone(), extra_info); ConfigFiles::save_to_path(&content_info_path, &content_info)?; } } @@ -259,8 +266,8 @@ impl<'a> Gog<'a> { Ok(()) } - fn get_code(&self) -> Result { - let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str()); + fn get_code(&mut self) -> Result { + let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri().as_str()); println!("Open the following url in a browser, login to your account and paste the \ resulting code parameter."); @@ -282,12 +289,9 @@ impl<'a> Gog<'a> { let token_uri = self.token_uri(self.client_id.as_str(), self.client_secret.as_str(), code, - self.redirect_uri.as_str()); + self.redirect_uri().as_str()); - let token_response = match self.http_client.get(token_uri.as_str()) { - Ok(value) => value, - Err(error) => return Err(GogError::HttpError(error)), - }; + let token_response = self.api_request_get(token_uri.as_str())?; match serde_json::from_str(&token_response) { Ok(value) => Ok(value), @@ -300,10 +304,7 @@ impl<'a> Gog<'a> { self.client_secret.as_str(), refresh_token); - let token_response = match self.http_client.get(token_refresh_uri.as_str()) { - Ok(value) => value, - Err(error) => return Err(GogError::HttpError(error)), - }; + let token_response = self.api_request_get(token_refresh_uri.as_str())?; match serde_json::from_str(&token_response) { Ok(value) => Ok(value), @@ -312,7 +313,8 @@ impl<'a> Gog<'a> { } fn get_content_ids(&mut self) -> Result, GogError> { - let response = self.http_client.get(self.games_uri.as_str())?; + let games_uri = self.games_uri(); + let response = self.api_request_get(games_uri.as_str())?; let content_ids_raw: Value = serde_json::from_str(response.as_str())?; let content_ids_serde = &content_ids_raw["owned"]; @@ -347,7 +349,8 @@ impl<'a> Gog<'a> { let mut raw_cd_keys_fix = raw_cd_keys.to_owned(); if raw_cd_keys_fix.contains("") { - raw_cd_keys_fix = raw_cd_keys_fix.replace("", ":") + raw_cd_keys_fix = raw_cd_keys_fix + .replace("", ":") .replace("", "") .replace("", ""); } @@ -379,6 +382,41 @@ impl<'a> Gog<'a> { return cd_keys; } + fn api_request_ensure_token(&mut self, response: &str) -> Result { + let response_json: Value = match serde_json::from_str(response) { + Ok(value) => value, + Err(_) => return Ok(false), + }; + + if response_json.is_object() && response_json.as_object().unwrap().contains_key("error") && + response_json["error"] == "invalid_grant" { + debug!("invalid grant, refreshing token..."); + self.refresh_token_from_file()?; + + Ok(true) + } else { + Ok(false) + } + } + + fn api_request_get(&mut self, uri: &str) -> Result { + let response = self.http_client.get(uri)?; + if self.api_request_ensure_token(&response)? { + Ok(self.http_client.get(uri)?) + } else { + Ok(response) + } + } + + fn api_request_download(&mut self, uri: &str, path: &PathBuf) -> Result { + let response = self.http_client.download(uri, path)?; + if self.api_request_ensure_token(&response)? { + Ok(self.http_client.download(uri, path)?) + } else { + Ok(response) + } + } + fn get_content(&mut self, content_id: u64, os_filters: &Vec, @@ -398,7 +436,11 @@ impl<'a> Gog<'a> { let downloads = &content_raw["downloads"]; - if content_raw.is_object() && !content_raw.as_object().unwrap().contains_key("forumLink") { + if content_raw.is_object() && + !content_raw + .as_object() + .unwrap() + .contains_key("forumLink") { return Err(GogError::Error("No forumLink property")); } @@ -479,11 +521,11 @@ impl<'a> Gog<'a> { let data = Data { manual_url: String::from(download["manualUrl"] - .as_str() - .unwrap()), + .as_str() + .unwrap()), version: String::from(download["version"] - .as_str() - .unwrap_or("")), + .as_str() + .unwrap_or("")), os: system.clone(), language: data_language.clone(), }; @@ -500,6 +542,14 @@ impl<'a> Gog<'a> { Ok(content) } + fn games_uri(&self) -> String { + String::from("https://embed.gog.com/user/data/games") + } + + fn redirect_uri(&self) -> String { + String::from("https://embed.gog.com/on_login_success?origin=client") + } + fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String { format!("https://auth.gog.\ com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2", diff --git a/src/main.rs b/src/main.rs index 8fb6eb6..5f9a936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,50 +28,50 @@ fn main() { env_logger::init().unwrap(); let matches = App::new("Gog Synchronizer") - .version("0.3.0") + .version("0.3.1") .author("Sebastian Hugentobler ") .about("Synchronizes your gog library to a local folder.") .arg(Arg::with_name("game-storage") - .short("s") - .long("game-storage") - .value_name("FOLDER") - .help("Sets the download folder (defaults to the working directory).") - .takes_value(true)) + .short("s") + .long("game-storage") + .value_name("FOLDER") + .help("Sets the download folder (defaults to the working directory).") + .takes_value(true)) .arg(Arg::with_name("movie-storage") - .short("m") - .long("movie-storage") - .value_name("FOLDER") - .help("Sets the download folder for movies (defaults to the working directory).") - .takes_value(true)) + .short("m") + .long("movie-storage") + .value_name("FOLDER") + .help("Sets the download folder for movies (defaults to the working directory).") + .takes_value(true)) .arg(Arg::with_name("os") - .short("o") - .long("os") - .value_name("FILTER") - .help("Only sync files for this comma seperated list of operating systems.\n\ + .short("o") + .long("os") + .value_name("FILTER") + .help("Only sync files for this comma seperated list of operating systems.\n\ Valid values are 'linux', 'mac' and 'windows'.") - .takes_value(true)) + .takes_value(true)) .arg(Arg::with_name("language") - .short("l") - .long("language") - .value_name("FILTER") - .help("Only sync files for this comma seperated list of languages.") - .takes_value(true)) + .short("l") + .long("language") + .value_name("FILTER") + .help("Only sync files for this comma seperated list of languages.") + .takes_value(true)) .arg(Arg::with_name("resolution") - .short("r") - .long("resolution") - .value_name("FILTER") - .help("Only sync movies for this comma seperated list of resolutions.") - .takes_value(true)) + .short("r") + .long("resolution") + .value_name("FILTER") + .help("Only sync movies for this comma seperated list of resolutions.") + .takes_value(true)) .arg(Arg::with_name("skip-movies") - .short("f") - .long("skip-movies") - .help("Skip movie content.") - .takes_value(false)) + .short("f") + .long("skip-movies") + .help("Skip movie content.") + .takes_value(false)) .arg(Arg::with_name("skip-games") - .short("g") - .long("skip-games") - .help("Skip game content.") - .takes_value(false)) + .short("g") + .long("skip-games") + .help("Skip game content.") + .takes_value(false)) .get_matches(); let configfiles = ConfigFiles::new();