refactor naming to be more generic

This commit is contained in:
Sebastian Hugentobler 2017-03-22 15:39:46 +01:00
parent c1779265af
commit 0ec9a41bff
6 changed files with 163 additions and 105 deletions

View File

@ -7,3 +7,6 @@ trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[*.md]
trim_trailing_whitespace = true

View File

@ -1,5 +1,8 @@
## 0.2.2 (2017-03-22) ## 0.2.2 (2017-03-22)
- ability to save movies seperately
- default value for storage config (fixes #6) - default value for storage config (fixes #6)
- ability to skip movie and game content
- fix a bug with truncating an http response
## 0.2.1 (2017-03-21) ## 0.2.1 (2017-03-21)
- add instructions for getting the code if there is no token - add instructions for getting the code if there is no token

View File

@ -1,4 +1,5 @@
# GOG-SYNC # GOG-SYNC
## **Please note that this is alpha software, you should not trust it.**
A small tool to synchronize the stuff in a [GOG](https://www.gog.com/) library A small tool to synchronize the stuff in a [GOG](https://www.gog.com/) library
with a local folder. with a local folder.
@ -7,8 +8,6 @@ It builds on the work of the [unofficial GOG API Documentation](https://gogapido
This is the first time I am building something with rust, so beware :) This is the first time I am building something with rust, so beware :)
**Please note that this is alpha software, you should not trust it.**
# Installation # Installation
Install from [crates.io](https://crates.io). Install from [crates.io](https://crates.io).
@ -26,13 +25,68 @@ For example on macOS or Linux
~/.config/gog-sync/config.json ~/.config/gog-sync/config.json
``` ```
It is in Json format and the only relevant key is `storage`. The rest is information A bare configuration with default values before first use:
about content hashes. ```
{
"gameStorage": ".",
"movieStorage": ".",
"content": {},
"data": {},
"extras": {},
"osFilters": [],
"languageFilters": [],
"skipMovies": false,
"skipGames": false
}
```
- *gameStorage*: Where to save games
- *movieStorage*: Where to save movies
- *content*: A map, content id => hash
- *data*: A map, data url => hash
- *extras*: A map, extra url => hash
- *osFilters*: An array of operating systems. If it is not empty, game data is limited to the ones in the list.
Valid values are `linux`, `mac` and `windows`
- *languageFilters*: An array of languages. If it is not empty, game data is limited to the ones in the list.
- *skipMovies*: Whether to skip movie content
- *skipGames*: Whether to skip game content
An incomplete list of languages on gog:
- `english`
- `český`
- `deutsch`
- `español`
- `français`
- `italiano`
- `magyar`
- `polski`
- `русский`
- `中文`
You should have no need of changing `content`, `data` or `extras`, as these are
used to determine whether specific content is up to date.
# Usage # Usage
If you want to see the information log while running set `RUST_LOG=info`. If you want to see the information log while running set `RUST_LOG=info`.
```
LAGS] [OPTIONS]
FLAGS:
-h, --help Prints help information
-g, --skip-games Skip game content.
-f, --skip-movies Skip movie content.
-V, --version Prints version information
OPTIONS:
-s, --game-storage <FOLDER> Sets the download folder (defaults to the working directory).
-l, --language <FILTER> Only sync files for this comma seperated list of languages.
-m, --movie-storage <FOLDER> Sets the download folder for movies (defaults to the working directory).
-o, --os <FILTER> Only sync files for this comma seperated list of operating systems.
Valid values are 'linux', 'mac' and 'windows'.
```
--- ---
``` ```

View File

@ -13,7 +13,7 @@
use configfiles::{ConfigFiles, ConfigError}; use configfiles::{ConfigFiles, ConfigError};
use http::{Http, HttpError}; use http::{Http, HttpError};
use models; use models;
use models::{Token, Game, Installer, Config}; use models::{Token, Content, Data, Config};
use serde_json; use serde_json;
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
@ -135,10 +135,10 @@ impl<'a> Gog<'a> {
error!("Configuration error, generating new one..."); error!("Configuration error, generating new one...");
Config { Config {
storage: String::from(storage_path), game_storage: String::from(storage_path),
movie_storage: String::from(storage_path_movies), movie_storage: String::from(storage_path_movies),
games: HashMap::new(), content: HashMap::new(),
installers: HashMap::new(), data: HashMap::new(),
extras: HashMap::new(), extras: HashMap::new(),
os_filters: os_filters.clone(), os_filters: os_filters.clone(),
language_filters: language_filters.clone(), language_filters: language_filters.clone(),
@ -148,75 +148,73 @@ impl<'a> Gog<'a> {
} }
}; };
let game_ids = self.get_game_ids()?; let content_ids = self.get_content_ids()?;
for game_id in game_ids { for content_id in content_ids {
let game_hash_saved = match config.games.get(&game_id.to_string()) { let content_hash_saved = match config.content.get(&content_id.to_string()) {
Some(value) => value.clone(), Some(value) => value.clone(),
None => u64::min_value(), None => u64::min_value(),
}; };
let game = match self.get_game(game_id, &os_filters, &language_filters) { let content = match self.get_content(content_id, &os_filters, &language_filters) {
Ok(value) => value, Ok(value) => value,
Err(error) => { Err(error) => {
error!("{}", error); error!("{}: {}", &content_id, error);
break; continue;
} }
}; };
if (game.is_movie && skip_movies) || (!game.is_movie && skip_games) { if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) {
info!("filtering {}", game.title); info!("filtering {}", content.title);
continue; continue;
} }
let game_hash = models::get_hash(&game); let content_hash = models::get_hash(&content);
if game_hash_saved == game_hash { if content_hash_saved == content_hash {
info!("{} already up to date.", &game.title); info!("{} already up to date.", &content.title);
continue; continue;
} }
let game_root = if game.is_movie { let content_root = if content.is_movie {
Path::new(storage_path_movies).join(&game.title) Path::new(storage_path_movies).join(&content.title)
} else { } else {
Path::new(storage_path).join(&game.title) Path::new(storage_path).join(&content.title)
}; };
fs::create_dir_all(&game_root)?; fs::create_dir_all(&content_root)?;
if !game.cd_key.is_empty() { if !content.cd_key.is_empty() {
let key_path = Path::new(game_root.as_os_str()).join("key.txt"); let key_path = Path::new(content_root.as_os_str()).join("key.txt");
let mut key_file = File::create(&key_path)?; let mut key_file = File::create(&key_path)?;
key_file.write_all(game.cd_key.as_bytes())? key_file.write_all(content.cd_key.as_bytes())?
} }
for installer in game.installers { for data in content.data {
let installer_hash_saved = match config.installers.get(&installer.manual_url) { let data_hash_saved = match config.data.get(&data.manual_url) {
Some(value) => value.clone(), Some(value) => value.clone(),
None => u64::min_value(), None => u64::min_value(),
}; };
let installer_hash = models::get_hash(&installer); let data_hash = models::get_hash(&data);
if installer_hash_saved == installer_hash { if data_hash_saved == data_hash {
info!("{} already up to date.", &installer.manual_url); info!("{} already up to date.", &data.manual_url);
continue; continue;
} }
let installer_root = Path::new(&game_root).join(&installer.language); let data_root = Path::new(&content_root).join(&data.language);
fs::create_dir_all(&installer_root)?; fs::create_dir_all(&data_root)?;
let installer_uri = format!("https://embed.gog.com{}", installer.manual_url); let data_uri = format!("https://embed.gog.com{}", data.manual_url);
info!("downloading {} for {}...", info!("downloading {} for {}...", &data.manual_url, &content.title);
&installer.manual_url, self.http_client.download(data_uri.as_str(), &data_root)?;
&game.title);
self.http_client.download(installer_uri.as_str(), &installer_root)?;
config.installers.insert(installer.manual_url, installer_hash); config.data.insert(data.manual_url, data_hash);
configfiles.save("config.json", &config)?; configfiles.save("config.json", &config)?;
} }
for extra in game.extras { for extra in content.extras {
let extra_hash_saved = match config.extras.get(&extra.manual_url) { let extra_hash_saved = match config.extras.get(&extra.manual_url) {
Some(value) => value.clone(), Some(value) => value.clone(),
None => u64::min_value(), None => u64::min_value(),
@ -231,14 +229,14 @@ 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, &game.title); info!("downloading {} for {}...", &extra.name, &content.title);
self.http_client.download(extra_uri.as_str(), &game_root)?; self.http_client.download(extra_uri.as_str(), &content_root)?;
config.extras.insert(extra.manual_url, extra_hash); config.extras.insert(extra.manual_url, extra_hash);
configfiles.save("config.json", &config)?; configfiles.save("config.json", &config)?;
} }
config.games.insert(game_id.to_string(), game_hash); config.content.insert(content_id.to_string(), content_hash);
configfiles.save("config.json", &config)?; configfiles.save("config.json", &config)?;
} }
@ -297,86 +295,86 @@ impl<'a> Gog<'a> {
} }
} }
fn get_game_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 response = self.http_client.get(self.games_uri.as_str())?;
let game_ids_raw: Value = serde_json::from_str(response.as_str())?; let content_ids_raw: Value = serde_json::from_str(response.as_str())?;
let game_ids_serde = &game_ids_raw["owned"]; let content_ids_serde = &content_ids_raw["owned"];
let mut game_ids: Vec<u64> = Vec::new(); let mut content_ids: Vec<u64> = Vec::new();
if !game_ids_serde.is_array() { if !content_ids_serde.is_array() {
return Err(GogError::Error("Error parsing game ids.")); return Err(GogError::Error("Error parsing content ids."));
} }
for game_id in game_ids_serde.as_array().unwrap() { for content_id in content_ids_serde.as_array().unwrap() {
let game_id_parsed = game_id.as_u64().unwrap_or(0); let content_id_parsed = content_id.as_u64().unwrap_or(0);
if game_id_parsed == 0 { if content_id_parsed == 0 {
error!("Cant parse game id {}", game_id); error!("Cant parse content id {}", content_id);
continue; continue;
} }
// The numbers in this list are excluded because they refer to // The numbers in this list are excluded because they refer to
// favourites, promotions and such. // favourites, promotions and such.
if [1, 2, 3, 4, 5].contains(&game_id_parsed) { if [1, 2, 3, 4, 5].contains(&content_id_parsed) {
continue; continue;
} }
game_ids.push(game_id_parsed); content_ids.push(content_id_parsed);
} }
return Ok(game_ids); return Ok(content_ids);
} }
fn get_game(&mut self, fn get_content(&mut self,
game_id: u64, content_id: u64,
os_filters: &Vec<String>, os_filters: &Vec<String>,
language_filters: &Vec<String>) language_filters: &Vec<String>)
-> Result<Game, GogError> { -> Result<Content, GogError> {
let game_uri = self.game_uri(game_id); let content_uri = self.content_uri(content_id);
debug!("looking for information at {}...", &game_uri); debug!("looking for information at {}...", &content_uri);
let response = self.http_client.get(game_uri.as_str())?; let response = self.http_client.get(content_uri.as_str())?;
let game_raw: Value = serde_json::from_str(response.as_str())?; let content_raw: Value = serde_json::from_str(response.as_str())?;
debug!("found {:?}", &game_raw); debug!("found {:?}", &content_raw);
let mut game: Game = serde_json::from_str(&response)?; let mut content: Content = serde_json::from_str(&response)?;
let downloads = &game_raw["downloads"]; let downloads = &content_raw["downloads"];
if game_raw.is_object() && !game_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"));
} }
let is_movie = match game_raw["forumLink"].as_str() { let is_movie = match content_raw["forumLink"].as_str() {
Some(value) => value == "https://embed.gog.com/forum/movies", Some(value) => value == "https://embed.gog.com/forum/movies",
None => false, None => false,
}; };
game.is_movie = is_movie; content.is_movie = is_movie;
debug!("processing installer fields: {:?}", &downloads); debug!("processing installer fields: {:?}", &downloads);
for languages in downloads.as_array() { for languages in downloads.as_array() {
for language in languages { for language in languages {
if !language.is_array() || language.as_array().unwrap().len() < 2 { if !language.is_array() || language.as_array().unwrap().len() < 2 {
error!("Skipping a language for {}", game.title); error!("Skipping a language for {}", content.title);
continue; continue;
} }
let installer_language = match language[0].as_str() { let data_language = match language[0].as_str() {
Some(value) => value.to_lowercase(), Some(value) => value.to_lowercase(),
None => String::default(), None => String::default(),
}; };
if installer_language.is_empty() { if data_language.is_empty() {
error!("Skipping a language for {}", game.title); error!("Skipping a language for {}", content.title);
continue; continue;
} }
if !is_movie && !language_filters.is_empty() && if !is_movie && !language_filters.is_empty() &&
!language_filters.contains(&installer_language) { !language_filters.contains(&data_language) {
info!("Skipping {} for {}", &installer_language, game.title); info!("Skipping {} for {}", &data_language, content.title);
continue; continue;
} }
@ -384,9 +382,9 @@ impl<'a> Gog<'a> {
for system in systems.keys() { for system in systems.keys() {
if is_movie && !os_filters.is_empty() && !os_filters.contains(system) { if is_movie && !os_filters.is_empty() && !os_filters.contains(system) {
info!("Skipping {} {} for {}", info!("Skipping {} {} for {}",
&installer_language, &data_language,
system, system,
game.title); content.title);
continue; continue;
} }
@ -395,11 +393,11 @@ impl<'a> Gog<'a> {
for download in real_download { for download in real_download {
if !download.is_object() || if !download.is_object() ||
!download.as_object().unwrap().contains_key("manualUrl") { !download.as_object().unwrap().contains_key("manualUrl") {
error!("Skipping an installer for {}", game.title); error!("Skipping an installer for {}", content.title);
continue; continue;
} }
let installer = Installer { let data = Data {
manual_url: String::from(download["manualUrl"] manual_url: String::from(download["manualUrl"]
.as_str() .as_str()
.unwrap()), .unwrap()),
@ -407,10 +405,10 @@ impl<'a> Gog<'a> {
.as_str() .as_str()
.unwrap_or("")), .unwrap_or("")),
os: system.clone(), os: system.clone(),
language: installer_language.clone(), language: data_language.clone(),
}; };
game.installers.push(installer); content.data.push(data);
} }
} }
} }
@ -419,7 +417,7 @@ impl<'a> Gog<'a> {
} }
} }
Ok(game) Ok(content)
} }
fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String { fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String {
@ -455,8 +453,8 @@ impl<'a> Gog<'a> {
refresh_token = refresh_token) refresh_token = refresh_token)
} }
fn game_uri(&self, game_id: u64) -> String { fn content_uri(&self, content_id: u64) -> String {
format!("https://embed.gog.com/account/gameDetails/{game_id}.json", format!("https://embed.gog.com/account/gameDetails/{content_id}.json",
game_id = game_id) content_id = content_id)
} }
} }

View File

@ -48,9 +48,9 @@ fn main() {
.version("0.2.2") .version("0.2.2")
.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("storage") .arg(Arg::with_name("game-storage")
.short("s") .short("s")
.long("storage") .long("game-storage")
.value_name("FOLDER") .value_name("FOLDER")
.help("Sets the download folder (defaults to the working directory).") .help("Sets the download folder (defaults to the working directory).")
.takes_value(true)) .takes_value(true))
@ -58,7 +58,7 @@ fn main() {
.short("m") .short("m")
.long("movie-storage") .long("movie-storage")
.value_name("FOLDER") .value_name("FOLDER")
.help("Sets the download folder for movies (defaults to the game directory).") .help("Sets the download folder for movies (defaults to the working directory).")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("os") .arg(Arg::with_name("os")
.short("o") .short("o")
@ -91,14 +91,14 @@ fn main() {
Err(_) => Config::new(), Err(_) => Config::new(),
}; };
let download_folder: &str = match matches.value_of("storage") { let download_folder: &str = match matches.value_of("game-storage") {
Some(value) => value, Some(value) => value,
None => config.storage.as_str(), None => config.game_storage.as_str(),
}; };
let download_folder_movies: &str = match matches.value_of("movieStorage") { let download_folder_movies: &str = match matches.value_of("movie-storage") {
Some(value) => value, Some(value) => value,
None => download_folder, None => config.movie_storage.as_str(),
}; };
let os_filters: Vec<String> = match matches.value_of("os") { let os_filters: Vec<String> = match matches.value_of("os") {

View File

@ -33,13 +33,13 @@ fn timestamp() -> i64 {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Config { pub struct Config {
#[serde(default)] #[serde(default)]
pub storage: String, pub game_storage: String,
#[serde(default)] #[serde(default)]
pub movie_storage: String, pub movie_storage: String,
#[serde(default = "default_map")] #[serde(default = "default_map")]
pub games: HashMap<String, u64>, pub content: HashMap<String, u64>,
#[serde(default = "default_map")] #[serde(default = "default_map")]
pub installers: HashMap<String, u64>, pub data: HashMap<String, u64>,
#[serde(default = "default_map")] #[serde(default = "default_map")]
pub extras: HashMap<String, u64>, pub extras: HashMap<String, u64>,
#[serde(default = "default_list")] #[serde(default = "default_list")]
@ -63,10 +63,10 @@ fn default_list() -> Vec<String> {
impl Config { impl Config {
pub fn new() -> Config { pub fn new() -> Config {
Config { Config {
storage: String::from("."), game_storage: String::from("."),
movie_storage: String::from("."), movie_storage: String::from("."),
games: HashMap::new(), content: HashMap::new(),
installers: HashMap::new(), data: HashMap::new(),
extras: HashMap::new(), extras: HashMap::new(),
os_filters: Vec::new(), os_filters: Vec::new(),
language_filters: Vec::new(), language_filters: Vec::new(),
@ -79,17 +79,17 @@ impl Config {
#[derive(Hash)] #[derive(Hash)]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Game { pub struct Content {
pub title: String, pub title: String,
pub cd_key: String, pub cd_key: String,
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub is_movie: bool, pub is_movie: bool,
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub installers: Vec<Installer>, pub data: Vec<Data>,
pub extras: Vec<Extra>, pub extras: Vec<Extra>,
} }
impl fmt::Display for Game { impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({})", self.title) write!(f, "({})", self.title)
} }
@ -98,14 +98,14 @@ impl fmt::Display for Game {
#[derive(Hash)] #[derive(Hash)]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Installer { pub struct Data {
pub manual_url: String, pub manual_url: String,
pub version: String, pub version: String,
pub os: String, pub os: String,
pub language: String, pub language: String,
} }
impl fmt::Display for Installer { impl fmt::Display for Data {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, write!(f,
"({}, {}, {}, {})", "({}, {}, {}, {})",