use configfiles::{ConfigFiles, ConfigError};
use http::{Http, HttpError};
use models;
use models::content::Content;
use models::contentinfo::ContentInfo;
use models::data::Data;
use models::datainfo::DataInfo;
use models::extra::Extra;
use models::extrainfo::ExtraInfo;
use models::token::Token;
use serde_json;
use serde_json::Value;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::fmt;
use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug)]
pub enum GogError {
Error(&'static str),
ConfigError(ConfigError),
HttpError(HttpError),
SerdeError(serde_json::Error),
IOError(io::Error),
}
impl fmt::Display for GogError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GogError::Error(ref err) => fmt::Display::fmt(err, f),
GogError::ConfigError(ref err) => fmt::Display::fmt(err, f),
GogError::HttpError(ref err) => fmt::Display::fmt(err, f),
GogError::SerdeError(ref err) => fmt::Display::fmt(err, f),
GogError::IOError(ref err) => fmt::Display::fmt(err, f),
}
}
}
impl From<ConfigError> for GogError {
fn from(e: ConfigError) -> Self {
GogError::ConfigError(e)
}
}
impl From<HttpError> for GogError {
fn from(e: HttpError) -> Self {
GogError::HttpError(e)
}
}
impl From<serde_json::Error> for GogError {
fn from(e: serde_json::Error) -> Self {
GogError::SerdeError(e)
}
}
impl From<io::Error> for GogError {
fn from(e: io::Error) -> Self {
GogError::IOError(e)
}
}
pub struct Gog<'a> {
client_id: String,
client_secret: String,
http_client: &'a mut Http,
}
impl<'a> Gog<'a> {
pub fn new(http_client: &'a mut Http) -> Gog<'a> {
Gog {
client_id: String::from("46899977096215655"),
client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"),
http_client: http_client,
}
}
pub fn login(&mut self) -> Result<(), GogError> {
self.refresh_token_from_file()?;
Ok(())
}
fn update_auth_header(&mut self, token: &Token) -> Result<(), GogError> {
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
Ok(self.http_client.add_header(auth_header.as_str())?)
}
fn refresh_token_from_file(&mut self) -> Result<Token, GogError> {
let config = ConfigFiles::new();
let mut token: Token = match config.load("token.json") {
Ok(value) => value,
Err(_) => {
let code = self.get_code()?;
self.get_token(code.as_str())?
}
};
if token.is_expired() {
token = self.refresh_token(token.refresh_token.as_str())?;
}
self.update_auth_header(&token)?;
config.save("token.json", &token)?;
Ok(token)
}
pub fn sync(&mut self,
storage_path_games: &str,
storage_path_movies: &str,
os_filters: &Vec<String>,
language_filters: &Vec<String>,
resolution_filters: &Vec<String>,
skip_movies: bool,
skip_games: bool)
-> Result<(), GogError> {
let content_ids = self.get_content_ids()?;
for content_id in content_ids {
let content = match self.get_content(content_id,
&os_filters,
&language_filters,
&resolution_filters) {
Ok(value) => value,
Err(error) => {
error!("{}: {}", &content_id, error);
continue;
}
};
if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) {
info!("filtering {}", content.title);
continue;
}
match self.sync_content(content, storage_path_games, storage_path_movies) {
Ok(_) => (),
Err(error) => {
warn!("{:?}", error);
continue;
}
}
}
Ok(())
}
fn sync_content(&mut self,
content: Content,
storage_path_games: &str,
storage_path_movies: &str)
-> Result<(), GogError> {
let content_root = if content.is_movie {
Path::new(storage_path_movies).join(&content.title)
} else {
Path::new(storage_path_games).join(&content.title)
};
let content_info_path = Path::new(&content_root).join("info.json");
let content_info_saved =
match ConfigFiles::load_from_path::<ContentInfo>(&content_info_path) {
Ok(value) => value,
Err(_) => ContentInfo::new(),
};
let content_hash = models::get_hash(&content);
if content_info_saved.hash == content_hash {
info!("{} already up to date.", &content.title);
return Ok(());
}
fs::create_dir_all(&content_root)?;
let mut content_info = ContentInfo {
hash: content_hash,
id: content.id,
title: content.title.clone(),
cd_keys: content.cd_keys.clone(),
data: HashMap::new(),
extras: HashMap::new(),
};
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
self.save_keys(content.cd_keys, &content_root)?;
for data in content.data {
self.save_data(content.title.as_str(),
data,
&content_root,
&content_info_path,
&content_info_saved,
&mut content_info)?;
}
let dlc_root = Path::new(&storage_path_games)
.join(&content.title)
.join("dlcs");
for dlc in content.dlcs {
self.sync_content(dlc,
dlc_root.to_string_lossy().as_ref(),
storage_path_movies)?;
}
for extra in content.extras {
self.save_extra(content.title.as_str(),
extra,
&content_root,
&content_info_path,
&content_info_saved,
&mut content_info)?;
}
Ok(())
}
fn save_file(&mut self,
content_title: &str,
relative_uri: &str,
data_root: &PathBuf)
-> Result<String, GogError> {
fs::create_dir_all(&data_root)?;
let data_uri = format!("https://embed.gog.com{}", relative_uri);
let filename = self.api_request_get_filename(&data_uri)?;
let file_path = Path::new(&data_root).join(&filename);
if file_path.exists() {
info!("{} for {} already exists", &filename, content_title);
return Ok(filename.to_owned());
}
info!("downloading {} for {}...", &relative_uri, content_title);
self.api_request_download(data_uri.as_str(), &data_root)
}
fn save_data(&mut self,
content_title: &str,
data: Data,
content_root: &PathBuf,
content_info_path: &PathBuf,
content_info_saved: &ContentInfo,
content_info: &mut ContentInfo)
-> Result<(), GogError> {
let hash_saved = match content_info_saved.data.get(data.manual_url.as_str()) {
Some(value) => value.hash,
None => u64::min_value(),
};
let hash = models::get_hash(&data);
if hash_saved == hash {
info!("{} already up to date.", &data.manual_url);
return Ok(());
}
let data_root = Path::new(&content_root).join(&data.language);
let filename = self.save_file(content_title, &data.manual_url, &data_root)?;
let data_info = DataInfo {
hash: hash,
filename: filename,
language: data.language,
};
content_info
.data
.insert(data.manual_url.clone(), data_info);
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
Ok(())
}
fn save_extra(&mut self,
content_title: &str,
extra: Extra,
content_root: &PathBuf,
content_info_path: &PathBuf,
content_info_saved: &ContentInfo,
content_info: &mut ContentInfo)
-> Result<(), GogError> {
let hash_saved = match content_info_saved.data.get(extra.manual_url.as_str()) {
Some(value) => value.hash,
None => u64::min_value(),
};
let hash = models::get_hash(&extra);
if hash_saved == hash {
info!("{} already up to date.", &extra.manual_url);
return Ok(());
}
let filename = self.save_file(content_title, &extra.manual_url, &content_root)?;
let extra_info = ExtraInfo {
hash: hash,
filename: filename,
name: extra.name,
};
content_info
.extras
.insert(extra.manual_url.clone(), extra_info);
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
Ok(())
}
fn save_keys(&mut self,
cd_keys: BTreeMap<String, String>,
content_root: &PathBuf)
-> Result<(), GogError> {
let key_root = content_root.join("keys");
if cd_keys.len() > 0 {
fs::create_dir_all(&key_root)?;
}
for (key, value) in cd_keys {
let key_path = key_root.join(format!("{}.txt", key));
let mut key_file = File::create(&key_path)?;
key_file.write_all(value.as_bytes())?
}
Ok(())
}
fn get_code(&mut self) -> Result<String, GogError> {
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.");
println!("{}", auth_uri);
let mut code = String::new();
print!("Code: ");
io::stdout().flush()?;
io::stdin()
.read_line(&mut code)
.expect("Failed to read line");
Ok(code)
}
fn get_token(&mut self, code: &str) -> Result<Token, GogError> {
let token_uri = self.token_uri(self.client_id.as_str(),
self.client_secret.as_str(),
code,
self.redirect_uri().as_str());
let token_response = self.api_request_get(token_uri.as_str())?;
match serde_json::from_str(&token_response) {
Ok(value) => Ok(value),
Err(error) => Err(GogError::SerdeError(error)),
}
}
fn refresh_token(&mut self, refresh_token: &str) -> Result<Token, GogError> {
let token_refresh_uri = self.token_refresh_uri(self.client_id.as_str(),
self.client_secret.as_str(),
refresh_token);
let token_response = self.api_request_get(token_refresh_uri.as_str())?;
match serde_json::from_str(&token_response) {
Ok(value) => Ok(value),
Err(error) => Err(GogError::SerdeError(error)),
}
}
fn get_content_ids(&mut self) -> Result<Vec<u64>, GogError> {
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"];
let mut content_ids: Vec<u64> = Vec::new();
if !content_ids_serde.is_array() {
return Err(GogError::Error("Error parsing content ids."));
}
for content_id in content_ids_serde.as_array().unwrap() {
let content_id_parsed = content_id.as_u64().unwrap_or(0);
if content_id_parsed == 0 {
error!("Cant parse content id {}", content_id);
continue;
}
if [1, 2, 3, 4, 5].contains(&content_id_parsed) {
continue;
}
content_ids.push(content_id_parsed);
}
Ok(content_ids)
}
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_get_filename(&mut self, uri: &str) -> Result<String, GogError> {
let response = self.http_client.get_filename(uri)?;
if self.api_request_ensure_token(&response)? {
Ok(self.http_client.get_filename(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,
content_id: u64,
os_filters: &Vec<String>,
language_filters: &Vec<String>,
resolution_filters: &Vec<String>)
-> Result<Content, GogError> {
let content_uri = self.content_uri(content_id);
debug!("looking for information at {}...", &content_uri);
let response = self.api_request_get(content_uri.as_str())?;
models::content::deserialize(content_id,
response.as_str(),
os_filters,
language_filters,
resolution_filters)
}
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",
client_id = client_id,
redirect_uri = redirect_uri)
}
fn token_uri(&self,
client_id: &str,
client_secret: &str,
code: &str,
redirect_uri: &str)
-> String {
format!("https://auth.gog.\
com/token?client_id={client_id}&client_secret={client_secret}&grant_type=authorization_code&code={code}&redirect_uri={redirect_uri}",
client_id = client_id,
client_secret = client_secret,
code = code.trim(),
redirect_uri = redirect_uri)
}
fn token_refresh_uri(&self,
client_id: &str,
client_secret: &str,
refresh_token: &str)
-> String {
format!("https://auth.gog.\
com/token?client_id={client_id}&client_secret={client_secret}&grant_type=refresh_token&refresh_token={refresh_token}",
client_id = client_id,
client_secret = client_secret,
refresh_token = refresh_token)
}
fn content_uri(&self, content_id: u64) -> String {
format!("https://embed.gog.com/account/gameDetails/{content_id}.json",
content_id = content_id)
}
}