add error handling
This commit is contained in:
parent
65692ec481
commit
5ce0b953cb
8 changed files with 886 additions and 171 deletions
71
src/config.rs
Normal file
71
src/config.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std;
|
||||
use std::io::{Read, Write};
|
||||
use toml;
|
||||
use xdg;
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
IOError(std::io::Error),
|
||||
TomlDeError(toml::de::Error),
|
||||
TomlSeError(toml::ser::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ConfigError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
ConfigError::IOError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for ConfigError {
|
||||
fn from(e: toml::de::Error) -> Self {
|
||||
ConfigError::TomlDeError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::ser::Error> for ConfigError {
|
||||
fn from(e: toml::ser::Error) -> Self {
|
||||
ConfigError::TomlSeError(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub xdg_dirs: BaseDirectories,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Config {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("gog-sync").unwrap();
|
||||
Config { xdg_dirs: xdg_dirs }
|
||||
}
|
||||
|
||||
pub fn load<T>(&self, name: &str) -> Result<T, ConfigError>
|
||||
where T: Deserialize
|
||||
{
|
||||
let config_path = self.xdg_dirs.place_config_file(name)?;
|
||||
let mut config_file = File::open(config_path)?;
|
||||
|
||||
let mut config_contents = String::new();
|
||||
config_file.read_to_string(&mut config_contents)?;
|
||||
|
||||
match toml::from_str(config_contents.as_str()) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(error) => Err(ConfigError::TomlDeError(error)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save<T>(&self, name: &str, content: &T) -> Result<(), ConfigError>
|
||||
where T: Serialize
|
||||
{
|
||||
let config_path = self.xdg_dirs.place_config_file(name)?;
|
||||
let content_toml = toml::to_string(content)?;
|
||||
|
||||
let mut config_file = File::create(&config_path)?;
|
||||
match config_file.write_all(content_toml.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) => Err(ConfigError::IOError(error)),
|
||||
}
|
||||
}
|
||||
}
|
293
src/gog.rs
Normal file
293
src/gog.rs
Normal file
|
@ -0,0 +1,293 @@
|
|||
use config::{Config, ConfigError};
|
||||
use http::{Http, HttpError};
|
||||
use models;
|
||||
use models::{Token, Game, Installer};
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn login(&mut self) -> Result<(), GogError> {
|
||||
let config = Config::new();
|
||||
let mut token: Token = match config.load("token.toml") {
|
||||
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())?;
|
||||
}
|
||||
|
||||
config.save("token.toml", &token)?;
|
||||
|
||||
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
|
||||
self.http_client.add_header(auth_header.as_str())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync(&mut self, storage_path: &str) -> Result<(), GogError> {
|
||||
let game_ids = self.get_game_ids()?;
|
||||
|
||||
for game_id in game_ids {
|
||||
let game = self.get_game(game_id)?;
|
||||
let game_hash = models::get_hash(&game);
|
||||
|
||||
let game_root = Path::new(storage_path).join(&game.title);
|
||||
fs::create_dir_all(&game_root)?;
|
||||
|
||||
for installer in game.installers {
|
||||
let installer_hash = models::get_hash(&installer);
|
||||
|
||||
let installer_uri = format!("https://embed.gog.com{}", installer.manual_url);
|
||||
|
||||
info!("downloading {} for {}...",
|
||||
&installer.manual_url,
|
||||
&game.title);
|
||||
self.http_client.download(installer_uri.as_str(), &game_root)?;
|
||||
}
|
||||
|
||||
for extra in game.extras {
|
||||
let extra_hash = models::get_hash(&extra);
|
||||
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
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!("{}", 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;
|
||||
}
|
||||
|
||||
game_ids.push(game_id_parsed);
|
||||
}
|
||||
|
||||
return Ok(game_ids);
|
||||
}
|
||||
|
||||
fn get_game(&mut self, game_id: u64) -> Result<Game, GogError> {
|
||||
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,
|
||||
None => "",
|
||||
};
|
||||
|
||||
if installer_language == "" {
|
||||
error!("Skipping a language for {}", game.title);
|
||||
continue;
|
||||
}
|
||||
|
||||
for systems in language[1].as_object() {
|
||||
for system in systems.keys() {
|
||||
for real_downloads in systems.get(system) {
|
||||
for real_download in real_downloads.as_array() {
|
||||
let download = &real_download[0];
|
||||
|
||||
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: String::from(installer_language),
|
||||
};
|
||||
|
||||
game.installers.push(installer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
137
src/http.rs
Normal file
137
src/http.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use curl;
|
||||
use curl::easy::{Easy, List, WriteError};
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str;
|
||||
use std::str::Utf8Error;
|
||||
use url;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HttpError {
|
||||
Error(&'static str),
|
||||
CurlError(curl::Error),
|
||||
Utf8Error(Utf8Error),
|
||||
IOError(io::Error),
|
||||
UrlParseError(url::ParseError),
|
||||
}
|
||||
|
||||
impl From<curl::Error> for HttpError {
|
||||
fn from(e: curl::Error) -> Self {
|
||||
HttpError::CurlError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Utf8Error> for HttpError {
|
||||
fn from(e: Utf8Error) -> Self {
|
||||
HttpError::Utf8Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for HttpError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
HttpError::IOError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for HttpError {
|
||||
fn from(e: url::ParseError) -> Self {
|
||||
HttpError::UrlParseError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for WriteError {
|
||||
fn from(e: HttpError) -> Self {
|
||||
WriteError::__Nonexhaustive
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Http {
|
||||
curl: Easy,
|
||||
}
|
||||
|
||||
impl Http {
|
||||
pub fn new() -> Http {
|
||||
let mut curl = Easy::new();
|
||||
curl.follow_location(true).unwrap();
|
||||
|
||||
Http { curl: curl }
|
||||
}
|
||||
|
||||
pub fn get(&mut self, uri: &str) -> Result<String, HttpError> {
|
||||
let mut response_body = String::new();
|
||||
|
||||
self.curl.url(uri)?;
|
||||
{
|
||||
let mut transfer = self.curl.transfer();
|
||||
transfer.write_function(|data| {
|
||||
response_body = String::from(str::from_utf8(data).unwrap());
|
||||
Ok(data.len())
|
||||
})?;
|
||||
|
||||
transfer.perform()?;
|
||||
}
|
||||
|
||||
Ok(response_body)
|
||||
}
|
||||
|
||||
pub fn add_header(&mut self, header: &str) -> Result<(), HttpError> {
|
||||
let mut list = List::new();
|
||||
list.append(header)?;
|
||||
|
||||
match self.curl.http_headers(list) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) => Err(HttpError::CurlError(error)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn download(&mut self,
|
||||
download_uri: &str,
|
||||
download_dir: &PathBuf)
|
||||
-> Result<(), HttpError> {
|
||||
let download_path_tmp = Path::new(download_dir.as_os_str()).join(".progress");
|
||||
let mut file_download = File::create(&download_path_tmp)?;
|
||||
|
||||
self.curl.url(download_uri)?;
|
||||
{
|
||||
let mut transfer = self.curl.transfer();
|
||||
transfer.write_function(|data| {
|
||||
match file_download.write(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) => Err(HttpError::IOError(error)),
|
||||
}?;
|
||||
Ok(data.len())
|
||||
})?;
|
||||
transfer.perform()?;
|
||||
}
|
||||
|
||||
let download_url_string = match self.curl.effective_url()? {
|
||||
Some(value) => value,
|
||||
None => return Err(HttpError::Error("Can't get effective download url.")),
|
||||
};
|
||||
|
||||
let download_url = Url::parse(download_url_string)?;
|
||||
|
||||
let download_url_segments = match download_url.path_segments() {
|
||||
Some(value) => value,
|
||||
None => return Err(HttpError::Error("Can't parse download segments.")),
|
||||
};
|
||||
|
||||
let file_name = match download_url_segments.last() {
|
||||
Some(value) => value,
|
||||
None => return Err(HttpError::Error("No segments in download url.")),
|
||||
};
|
||||
|
||||
let download_path = Path::new(download_dir.as_os_str()).join(file_name);
|
||||
match fs::rename(download_path_tmp, download_path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) => Err(HttpError::IOError(error)),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
202
src/main.rs
202
src/main.rs
|
@ -1,182 +1,48 @@
|
|||
extern crate url;
|
||||
extern crate chrono;
|
||||
extern crate clap;
|
||||
extern crate curl;
|
||||
extern crate env_logger;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate toml;
|
||||
extern crate url;
|
||||
extern crate xdg;
|
||||
|
||||
use std::io;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use curl::easy::{Easy, List};
|
||||
use std::str;
|
||||
use serde_json::Value;
|
||||
use url::{Url, Host};
|
||||
use std::path::Path;
|
||||
mod config;
|
||||
mod gog;
|
||||
mod http;
|
||||
mod models;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Token {
|
||||
access_token: String,
|
||||
expires_in: u16,
|
||||
token_type: String,
|
||||
scope: String,
|
||||
session_id: String,
|
||||
refresh_token: String,
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Game {
|
||||
title: String,
|
||||
cd_key: String,
|
||||
#[serde(skip_deserializing)]
|
||||
installers: Vec<Installer>,
|
||||
extras: Vec<Extra>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Installer {
|
||||
manual_url: String,
|
||||
version: String,
|
||||
os: String,
|
||||
language: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Extra {
|
||||
manual_url: String,
|
||||
name: String,
|
||||
}
|
||||
use config::Config;
|
||||
use clap::{Arg, App};
|
||||
use gog::Gog;
|
||||
use http::Http;
|
||||
|
||||
fn main() {
|
||||
const CLIENT_ID: &'static str = "46899977096215655";
|
||||
const CLIENT_SECRET: &'static str = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9";
|
||||
// const redirect_uri: &'static str = "https://embed.gog.com/on_login_success?origin=client";
|
||||
const REDIRECT_URI_QUOTED: &'static str = "https%3A%2F%2Fembed.gog.\
|
||||
com%2Fon_login_success%3Forigin%3Dclient";
|
||||
env_logger::init().unwrap();
|
||||
|
||||
let auth_url = 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_QUOTED);
|
||||
let matches = App::new("Gog Synchronizer")
|
||||
.version("0.1.0")
|
||||
.author("Sebastian Hugentobler <sebastian@vanwa.ch>")
|
||||
.about("Synchronizes your gog library to a local folder.")
|
||||
.arg(Arg::with_name("storage")
|
||||
.short("s")
|
||||
.long("storage")
|
||||
.value_name("FOLDER")
|
||||
.help("Sets the download folder (defaults to the working directory).")
|
||||
.takes_value(true))
|
||||
.get_matches();
|
||||
|
||||
println!("{}", auth_url);
|
||||
let download_folder = matches.value_of("storage").unwrap_or(".");
|
||||
|
||||
let mut code = String::new();
|
||||
let config = Config::new();
|
||||
|
||||
print!("Code: ");
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
io::stdin()
|
||||
.read_line(&mut code)
|
||||
.expect("Failed to read line");
|
||||
|
||||
let token_query = format!("https://auth.gog.\
|
||||
com/token?client_id={client_id}&client_secret={client_secret}&grant_type=authorization_code&code={code}&redirect_uri={redirect_uri_quoted}",
|
||||
client_id = CLIENT_ID,
|
||||
client_secret = CLIENT_SECRET,
|
||||
code = code.trim(),
|
||||
redirect_uri_quoted = REDIRECT_URI_QUOTED);
|
||||
|
||||
let mut token_response_body = String::new();
|
||||
|
||||
let mut curl = Easy::new();
|
||||
curl.url(token_query.as_str()).unwrap();
|
||||
{
|
||||
let mut transfer = curl.transfer();
|
||||
transfer.write_function(|data| {
|
||||
token_response_body = String::from(str::from_utf8(data).unwrap());
|
||||
Ok(data.len())
|
||||
})
|
||||
.unwrap();
|
||||
transfer.perform().unwrap();
|
||||
}
|
||||
|
||||
let token: Token = serde_json::from_str(&token_response_body).unwrap();
|
||||
|
||||
let mut list = List::new();
|
||||
list.append(format!("Authorization: Bearer {token}", token = token.access_token).as_str())
|
||||
.unwrap();
|
||||
curl.http_headers(list).unwrap();
|
||||
curl.follow_location(true);
|
||||
|
||||
let mut games_response_body = String::new();
|
||||
curl.url("https://embed.gog.com/user/data/games").unwrap();
|
||||
{
|
||||
let mut transfer = curl.transfer();
|
||||
transfer.write_function(|data| {
|
||||
games_response_body = String::from(str::from_utf8(data).unwrap());
|
||||
Ok(data.len())
|
||||
})
|
||||
.unwrap();
|
||||
transfer.perform().unwrap();
|
||||
}
|
||||
|
||||
let game_ids_raw: Value = serde_json::from_str(games_response_body.as_str()).unwrap();
|
||||
let game_ids = &game_ids_raw["owned"];
|
||||
|
||||
let mut game_response_body = String::new();
|
||||
let game_query = format!("https://embed.gog.com/account/gameDetails/{game_id}.json",
|
||||
game_id = game_ids[10]);
|
||||
curl.url(game_query.as_str()).unwrap();
|
||||
{
|
||||
let mut transfer = curl.transfer();
|
||||
transfer.write_function(|data| {
|
||||
game_response_body = String::from(str::from_utf8(data).unwrap());
|
||||
Ok(data.len())
|
||||
})
|
||||
.unwrap();
|
||||
transfer.perform().unwrap();
|
||||
}
|
||||
|
||||
let mut game: Game = serde_json::from_str(&game_response_body).unwrap();
|
||||
|
||||
let game_raw: Value = serde_json::from_str(game_response_body.as_str()).unwrap();
|
||||
let downloads = &game_raw["downloads"];
|
||||
|
||||
for languages in downloads.as_array() {
|
||||
for language in languages {
|
||||
for systems in language[1].as_object() {
|
||||
for system in systems.keys() {
|
||||
for real_downloads in systems.get(system) {
|
||||
for real_download in real_downloads.as_array() {
|
||||
let download = &real_download[0];
|
||||
|
||||
let installer = Installer {
|
||||
manual_url: String::from(download["manualUrl"].as_str().unwrap()),
|
||||
version: String::from(download["manualUrl"].as_str().unwrap()),
|
||||
os: system.clone(),
|
||||
language: String::from(language[0].as_str().unwrap()),
|
||||
};
|
||||
|
||||
game.installers.push(installer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let download_query = format!("https://embed.gog.com{}", game.installers[0].manual_url);
|
||||
curl.url(download_query.as_str()).unwrap();
|
||||
curl.perform().unwrap();
|
||||
|
||||
let download_url = Url::parse(curl.effective_url().unwrap().unwrap()).unwrap();
|
||||
let file_name = download_url.path_segments().unwrap().last().unwrap();
|
||||
|
||||
let download_path = Path::new("/Users/sebastianhugentobler/Downloads/").join(file_name);
|
||||
let mut file_download = File::create(download_path).unwrap();
|
||||
curl.url(download_query.as_str()).unwrap();
|
||||
{
|
||||
let mut transfer = curl.transfer();
|
||||
transfer.write_function(|data| {
|
||||
file_download.write(data);
|
||||
Ok(data.len())
|
||||
})
|
||||
.unwrap();
|
||||
transfer.perform().unwrap();
|
||||
}
|
||||
let mut http_client = Http::new();
|
||||
let mut gog = Gog::new(&mut http_client);
|
||||
gog.login().unwrap();
|
||||
gog.sync(download_folder).unwrap();
|
||||
}
|
||||
|
|
89
src/models.rs
Normal file
89
src/models.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use chrono::UTC;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Token {
|
||||
pub access_token: String,
|
||||
pub expires_in: u16,
|
||||
pub token_type: String,
|
||||
pub scope: String,
|
||||
pub session_id: String,
|
||||
pub refresh_token: String,
|
||||
pub user_id: String,
|
||||
#[serde(default = "timestamp")]
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn is_expired(&self) -> bool {
|
||||
let now = UTC::now().timestamp();
|
||||
|
||||
now > self.timestamp + self.expires_in as i64
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp() -> i64 {
|
||||
UTC::now().timestamp()
|
||||
}
|
||||
|
||||
#[derive(Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Game {
|
||||
pub title: String,
|
||||
pub cd_key: String,
|
||||
#[serde(skip_deserializing)]
|
||||
pub installers: Vec<Installer>,
|
||||
pub extras: Vec<Extra>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Game {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({})", self.title)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Installer {
|
||||
pub manual_url: String,
|
||||
pub version: String,
|
||||
pub os: String,
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for Installer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"({}, {}, {}, {})",
|
||||
self.manual_url,
|
||||
self.version,
|
||||
self.os,
|
||||
self.language)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Extra {
|
||||
pub manual_url: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for Extra {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({}, {})", self.manual_url, self.name)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_hash<T>(obj: &T) -> u64
|
||||
where T: Hash
|
||||
{
|
||||
let mut s = DefaultHasher::new();
|
||||
obj.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue