Merge branch '7-access-token-expired' into 'master'
automatically refresh invalid grants Closes #7 See merge request !8
This commit is contained in:
commit
db1c5af255
@ -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)
|
## 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)
|
## 0.2.4 (2017-03-23)
|
||||||
- fix a bug in serial key parsing
|
- fix a bug in serial key parsing
|
||||||
|
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -1,9 +1,9 @@
|
|||||||
[root]
|
[root]
|
||||||
name = "gog-sync"
|
name = "gog-sync"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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)",
|
"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)",
|
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@ -53,7 +53,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "2.23.3"
|
version = "2.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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 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 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 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 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 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"
|
"checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gog-sync"
|
name = "gog-sync"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Sebastian Hugentobler <sebastian@vanwa.ch>"]
|
authors = ["Sebastian Hugentobler <sebastian@vanwa.ch>"]
|
||||||
description = "Synchronizes a GOG library with a local folder."
|
description = "Synchronizes a GOG library with a local folder."
|
||||||
documentation = "https://docs.rs/crate/gog-sync"
|
documentation = "https://docs.rs/crate/gog-sync"
|
||||||
|
106
src/gog.rs
106
src/gog.rs
@ -24,6 +24,7 @@ use std::fs::File;
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`.
|
/// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -76,8 +77,6 @@ impl From<io::Error> for GogError {
|
|||||||
pub struct Gog<'a> {
|
pub struct Gog<'a> {
|
||||||
client_id: String,
|
client_id: String,
|
||||||
client_secret: String,
|
client_secret: String,
|
||||||
redirect_uri: String,
|
|
||||||
games_uri: String,
|
|
||||||
http_client: &'a mut Http,
|
http_client: &'a mut Http,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,8 +86,6 @@ impl<'a> Gog<'a> {
|
|||||||
Gog {
|
Gog {
|
||||||
client_id: String::from("46899977096215655"),
|
client_id: String::from("46899977096215655"),
|
||||||
client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"),
|
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,
|
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
|
/// 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.
|
/// login and copy the code parameter from the next page into the prompt.
|
||||||
pub fn login(&mut self) -> Result<(), GogError> {
|
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<Token, GogError> {
|
||||||
let config = ConfigFiles::new();
|
let config = ConfigFiles::new();
|
||||||
let mut token: Token = match config.load("token.json") {
|
let mut token: Token = match config.load("token.json") {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
@ -113,10 +119,7 @@ impl<'a> Gog<'a> {
|
|||||||
|
|
||||||
config.save("token.json", &token)?;
|
config.save("token.json", &token)?;
|
||||||
|
|
||||||
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
|
Ok(token)
|
||||||
self.http_client.add_header(auth_header.as_str())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Syncs the contents of a gog account with a local folder.
|
/// 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);
|
let data_uri = format!("https://embed.gog.com{}", data.manual_url);
|
||||||
|
|
||||||
info!("downloading {} for {}...", &data.manual_url, &content.title);
|
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 {
|
let data_info = DataInfo {
|
||||||
hash: data_hash,
|
hash: data_hash,
|
||||||
@ -222,13 +225,15 @@ impl<'a> Gog<'a> {
|
|||||||
language: data.language,
|
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)?;
|
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for extra in content.extras {
|
for extra in content.extras {
|
||||||
let extra_hash_saved = match content_info_saved.extras
|
let extra_hash_saved =
|
||||||
.get(&content_id.to_string()) {
|
match content_info_saved.extras.get(&content_id.to_string()) {
|
||||||
Some(value) => value.hash,
|
Some(value) => value.hash,
|
||||||
None => u64::min_value(),
|
None => u64::min_value(),
|
||||||
};
|
};
|
||||||
@ -243,7 +248,7 @@ impl<'a> Gog<'a> {
|
|||||||
let extra_uri = format!("https://embed.gog.com{}", extra.manual_url);
|
let extra_uri = format!("https://embed.gog.com{}", extra.manual_url);
|
||||||
|
|
||||||
info!("downloading {} for {}...", &extra.name, &content.title);
|
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 {
|
let extra_info = ExtraInfo {
|
||||||
hash: extra_hash,
|
hash: extra_hash,
|
||||||
@ -251,7 +256,9 @@ impl<'a> Gog<'a> {
|
|||||||
name: extra.name,
|
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)?;
|
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,8 +266,8 @@ impl<'a> Gog<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_code(&self) -> Result<String, GogError> {
|
fn get_code(&mut self) -> Result<String, GogError> {
|
||||||
let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str());
|
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 \
|
println!("Open the following url in a browser, login to your account and paste the \
|
||||||
resulting code parameter.");
|
resulting code parameter.");
|
||||||
@ -282,12 +289,9 @@ impl<'a> Gog<'a> {
|
|||||||
let token_uri = self.token_uri(self.client_id.as_str(),
|
let token_uri = self.token_uri(self.client_id.as_str(),
|
||||||
self.client_secret.as_str(),
|
self.client_secret.as_str(),
|
||||||
code,
|
code,
|
||||||
self.redirect_uri.as_str());
|
self.redirect_uri().as_str());
|
||||||
|
|
||||||
let token_response = match self.http_client.get(token_uri.as_str()) {
|
let token_response = self.api_request_get(token_uri.as_str())?;
|
||||||
Ok(value) => value,
|
|
||||||
Err(error) => return Err(GogError::HttpError(error)),
|
|
||||||
};
|
|
||||||
|
|
||||||
match serde_json::from_str(&token_response) {
|
match serde_json::from_str(&token_response) {
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
@ -300,10 +304,7 @@ impl<'a> Gog<'a> {
|
|||||||
self.client_secret.as_str(),
|
self.client_secret.as_str(),
|
||||||
refresh_token);
|
refresh_token);
|
||||||
|
|
||||||
let token_response = match self.http_client.get(token_refresh_uri.as_str()) {
|
let token_response = self.api_request_get(token_refresh_uri.as_str())?;
|
||||||
Ok(value) => value,
|
|
||||||
Err(error) => return Err(GogError::HttpError(error)),
|
|
||||||
};
|
|
||||||
|
|
||||||
match serde_json::from_str(&token_response) {
|
match serde_json::from_str(&token_response) {
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
@ -312,7 +313,8 @@ impl<'a> Gog<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_content_ids(&mut self) -> Result<Vec<u64>, GogError> {
|
fn get_content_ids(&mut self) -> Result<Vec<u64>, 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_raw: Value = serde_json::from_str(response.as_str())?;
|
||||||
let content_ids_serde = &content_ids_raw["owned"];
|
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();
|
let mut raw_cd_keys_fix = raw_cd_keys.to_owned();
|
||||||
|
|
||||||
if raw_cd_keys_fix.contains("<span>") {
|
if raw_cd_keys_fix.contains("<span>") {
|
||||||
raw_cd_keys_fix = raw_cd_keys_fix.replace("</span><span>", ":")
|
raw_cd_keys_fix = raw_cd_keys_fix
|
||||||
|
.replace("</span><span>", ":")
|
||||||
.replace("<span>", "")
|
.replace("<span>", "")
|
||||||
.replace("</span>", "");
|
.replace("</span>", "");
|
||||||
}
|
}
|
||||||
@ -379,6 +382,41 @@ impl<'a> Gog<'a> {
|
|||||||
return cd_keys;
|
return cd_keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn api_request_ensure_token(&mut self, response: &str) -> Result<bool, GogError> {
|
||||||
|
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<String, GogError> {
|
||||||
|
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<String, GogError> {
|
||||||
|
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,
|
fn get_content(&mut self,
|
||||||
content_id: u64,
|
content_id: u64,
|
||||||
os_filters: &Vec<String>,
|
os_filters: &Vec<String>,
|
||||||
@ -398,7 +436,11 @@ impl<'a> Gog<'a> {
|
|||||||
|
|
||||||
let downloads = &content_raw["downloads"];
|
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"));
|
return Err(GogError::Error("No forumLink property"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,6 +542,14 @@ impl<'a> Gog<'a> {
|
|||||||
Ok(content)
|
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 {
|
fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String {
|
||||||
format!("https://auth.gog.\
|
format!("https://auth.gog.\
|
||||||
com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2",
|
com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2",
|
||||||
|
@ -28,7 +28,7 @@ fn main() {
|
|||||||
env_logger::init().unwrap();
|
env_logger::init().unwrap();
|
||||||
|
|
||||||
let matches = App::new("Gog Synchronizer")
|
let matches = App::new("Gog Synchronizer")
|
||||||
.version("0.3.0")
|
.version("0.3.1")
|
||||||
.author("Sebastian Hugentobler <sebastian@vanwa.ch>")
|
.author("Sebastian Hugentobler <sebastian@vanwa.ch>")
|
||||||
.about("Synchronizes your gog library to a local folder.")
|
.about("Synchronizes your gog library to a local folder.")
|
||||||
.arg(Arg::with_name("game-storage")
|
.arg(Arg::with_name("game-storage")
|
||||||
|
Loading…
Reference in New Issue
Block a user