gog-sync/src/gog.rs

411 lines
14 KiB
Rust
Raw Normal View History

2017-03-20 18:00:27 +00:00
//! Uses the GOG api as described [here](https://gogapidocs.readthedocs.io/en/latest/).
//!
//! # Example
//! ```
//! use gog::Gog;
//!
//! let mut http_client = Http::new();
//! let mut gog = Gog::new(&mut http_client);
//! gog.login();
//! gog.sync(download_folder);
//! ```
2017-03-20 15:20:37 +00:00
use configfiles::{ConfigFiles, ConfigError};
2017-03-20 12:14:49 +00:00
use http::{Http, HttpError};
use models;
2017-03-20 15:20:37 +00:00
use models::{Token, Game, Installer, Config};
2017-03-20 12:14:49 +00:00
use serde_json;
use serde_json::Value;
2017-03-20 15:20:37 +00:00
use std::collections::HashMap;
2017-03-20 12:14:49 +00:00
use std::fs;
2017-03-20 15:20:37 +00:00
use std::fs::File;
2017-03-20 12:14:49 +00:00
use std::io;
use std::io::Write;
use std::path::Path;
2017-03-20 18:00:27 +00:00
/// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`.
2017-03-20 12:14:49 +00:00
#[derive(Debug)]
pub enum GogError {
Error(&'static str),
ConfigError(ConfigError),
HttpError(HttpError),
SerdeError(serde_json::Error),
IOError(io::Error),
}
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)
}
}
2017-03-20 18:00:27 +00:00
/// Wraps the GOG api.
/// At the moment Client ID and Client Secret are hardcoded to the galaxy client.
2017-03-20 12:14:49 +00:00
pub struct Gog<'a> {
client_id: String,
client_secret: String,
redirect_uri: String,
games_uri: String,
http_client: &'a mut Http,
}
impl<'a> Gog<'a> {
2017-03-20 18:00:27 +00:00
/// Create a new instance of `Gog`.
2017-03-20 12:14:49 +00:00
pub fn new(http_client: &'a mut Http) -> 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,
}
}
2017-03-20 18:00:27 +00:00
/// Logs into a gog account.
/// If a token was already saved it should work automatically. Otherwise you
/// 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.
2017-03-20 12:14:49 +00:00
pub fn login(&mut self) -> Result<(), GogError> {
2017-03-20 15:20:37 +00:00
let config = ConfigFiles::new();
let mut token: Token = match config.load("token.json") {
2017-03-20 12:14:49 +00:00
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())?;
}
2017-03-20 15:20:37 +00:00
config.save("token.json", &token)?;
2017-03-20 12:14:49 +00:00
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
self.http_client.add_header(auth_header.as_str())?;
Ok(())
}
2017-03-20 18:00:27 +00:00
/// Syncs the contents of a gog account with a local folder.
/// Uses a hash to figure out whether something has changed.
pub fn sync(&mut self,
storage_path: &str,
os_filters: &Vec<String>,
language_filters: &Vec<String>)
-> Result<(), GogError> {
2017-03-20 15:20:37 +00:00
let configfiles = ConfigFiles::new();
let mut config: Config = match configfiles.load("config.json") {
Ok(value) => value,
Err(_) => {
error!("Configuration error, generating new one...");
Config {
storage: String::from(storage_path),
games: HashMap::new(),
installers: HashMap::new(),
extras: HashMap::new(),
os_filters: os_filters.clone(),
language_filters: language_filters.clone(),
2017-03-20 15:20:37 +00:00
}
}
};
2017-03-20 12:14:49 +00:00
let game_ids = self.get_game_ids()?;
for game_id in game_ids {
2017-03-20 15:20:37 +00:00
let game_hash_saved = match config.games.get(&game_id.to_string()) {
Some(value) => value.clone(),
None => u64::min_value(),
};
let game = self.get_game(game_id, &os_filters, &language_filters)?;
2017-03-20 12:14:49 +00:00
let game_hash = models::get_hash(&game);
2017-03-20 15:20:37 +00:00
if game_hash_saved == game_hash {
info!("{} already up to date.", &game.title);
continue;
}
2017-03-20 12:14:49 +00:00
let game_root = Path::new(storage_path).join(&game.title);
fs::create_dir_all(&game_root)?;
2017-03-20 15:20:37 +00:00
if !game.cd_key.is_empty() {
let key_path = Path::new(game_root.as_os_str()).join("key.txt");
let mut key_file = File::create(&key_path)?;
key_file.write_all(game.cd_key.as_bytes())?
}
2017-03-20 12:14:49 +00:00
for installer in game.installers {
2017-03-20 15:20:37 +00:00
let installer_hash_saved = match config.installers.get(&installer.manual_url) {
Some(value) => value.clone(),
None => u64::min_value(),
};
2017-03-20 12:14:49 +00:00
let installer_hash = models::get_hash(&installer);
2017-03-20 15:20:37 +00:00
if installer_hash_saved == installer_hash {
info!("{} already up to date.", &installer.manual_url);
continue;
}
2017-03-20 18:00:27 +00:00
let installer_root = Path::new(&game_root).join(&installer.language);
fs::create_dir_all(&installer_root)?;
2017-03-20 12:14:49 +00:00
let installer_uri = format!("https://embed.gog.com{}", installer.manual_url);
info!("downloading {} for {}...",
&installer.manual_url,
&game.title);
2017-03-20 18:00:27 +00:00
self.http_client.download(installer_uri.as_str(), &installer_root)?;
2017-03-20 15:20:37 +00:00
config.installers.insert(installer.manual_url, installer_hash);
configfiles.save("config.json", &config)?;
2017-03-20 12:14:49 +00:00
}
for extra in game.extras {
let extra_hash_saved = match config.extras.get(&extra.manual_url) {
2017-03-20 15:20:37 +00:00
Some(value) => value.clone(),
None => u64::min_value(),
};
2017-03-20 12:14:49 +00:00
let extra_hash = models::get_hash(&extra);
2017-03-20 15:20:37 +00:00
if extra_hash_saved == extra_hash {
info!("{} already up to date.", &extra.manual_url);
continue;
}
2017-03-20 12:14:49 +00:00
let extra_uri = format!("https://embed.gog.com{}", extra.manual_url);
info!("downloading {} for {}...", &extra.name, &game.title);
self.http_client.download(extra_uri.as_str(), &game_root)?;
2017-03-20 15:20:37 +00:00
config.extras.insert(extra.manual_url, extra_hash);
configfiles.save("config.json", &config)?;
2017-03-20 12:14:49 +00:00
}
2017-03-20 15:20:37 +00:00
config.games.insert(game_id.to_string(), game_hash);
configfiles.save("config.json", &config)?;
2017-03-20 12:14:49 +00:00
}
Ok(())
}
fn get_code(&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.");
2017-03-20 12:14:49 +00:00
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 = match self.http_client.get(token_uri.as_str()) {
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
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 = match self.http_client.get(token_refresh_uri.as_str()) {
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
match serde_json::from_str(&token_response) {
Ok(value) => Ok(value),
Err(error) => Err(GogError::SerdeError(error)),
}
}
fn get_game_ids(&mut self) -> Result<Vec<u64>, GogError> {
let response = self.http_client.get(self.games_uri.as_str())?;
let game_ids_raw: Value = serde_json::from_str(response.as_str())?;
let game_ids_serde = &game_ids_raw["owned"];
let mut game_ids: Vec<u64> = Vec::new();
if !game_ids_serde.is_array() {
return Err(GogError::Error("Error parsing game ids."));
}
for game_id in game_ids_serde.as_array().unwrap() {
let game_id_parsed = game_id.as_u64().unwrap_or(0);
if game_id_parsed == 0 {
error!("Cant parse game id {}", game_id);
continue;
2017-03-20 15:20:37 +00:00
}
// The numbers in this list are excluded because they refer to
// favourites, promotions and such.
2017-03-20 15:20:37 +00:00
if [1, 2, 3, 4, 5].contains(&game_id_parsed) {
continue;
2017-03-20 12:14:49 +00:00
}
game_ids.push(game_id_parsed);
}
return Ok(game_ids);
}
fn get_game(&mut self,
game_id: u64,
os_filters: &Vec<String>,
language_filters: &Vec<String>)
-> Result<Game, GogError> {
2017-03-20 12:14:49 +00:00
let game_uri = self.game_uri(game_id);
let response = self.http_client.get(game_uri.as_str())?;
let mut game: Game = serde_json::from_str(&response)?;
let game_raw: Value = serde_json::from_str(response.as_str())?;
let downloads = &game_raw["downloads"];
for languages in downloads.as_array() {
for language in languages {
if !language.is_array() || language.as_array().unwrap().len() < 2 {
error!("Skipping a language for {}", game.title);
continue;
}
let installer_language = match language[0].as_str() {
Some(value) => value.to_lowercase(),
None => String::default(),
2017-03-20 12:14:49 +00:00
};
if installer_language.is_empty() {
2017-03-20 12:14:49 +00:00
error!("Skipping a language for {}", game.title);
continue;
}
if !language_filters.is_empty() && !language_filters.contains(&installer_language) {
info!("Skipping {} for {}", &installer_language, game.title);
continue;
}
2017-03-20 12:14:49 +00:00
for systems in language[1].as_object() {
for system in systems.keys() {
if !os_filters.is_empty() && !os_filters.contains(system) {
info!("Skipping {} {} for {}",
&installer_language,
system,
game.title);
continue;
}
2017-03-20 12:14:49 +00:00
for real_downloads in systems.get(system) {
for real_download in real_downloads.as_array() {
for download in real_download {
if !download.is_object() ||
!download.as_object().unwrap().contains_key("manualUrl") {
error!("Skipping an installer for {}", game.title);
continue;
}
let installer = Installer {
manual_url: String::from(download["manualUrl"]
.as_str()
.unwrap()),
version: String::from(download["version"]
.as_str()
.unwrap_or("")),
os: system.clone(),
language: installer_language.clone(),
};
game.installers.push(installer);
2017-03-20 12:14:49 +00:00
}
}
}
}
}
}
}
Ok(game)
}
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 game_uri(&self, game_id: u64) -> String {
format!("https://embed.gog.com/account/gameDetails/{game_id}.json",
game_id = game_id)
}
}