From 865f3980137d8e6029172144b763f0dd6ed9cf81 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Wed, 10 May 2017 12:03:48 +0200 Subject: [PATCH 01/10] move content deserialization in module file --- src/gog.rs | 157 ++-------------------------------------- src/models/content.rs | 165 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 154 deletions(-) diff --git a/src/gog.rs b/src/gog.rs index 5dc69b0..7172156 100644 --- a/src/gog.rs +++ b/src/gog.rs @@ -17,12 +17,10 @@ use models::content::Content; use models::contentinfo::ContentInfo; use models::datainfo::DataInfo; use models::extrainfo::ExtraInfo; -use models::data::Data; use models::token::Token; use serde_json; use serde_json::Value; use std::collections::HashMap; -use std::collections::BTreeMap; use std::fmt; use std::fs; use std::fs::File; @@ -352,44 +350,6 @@ impl<'a> Gog<'a> { return Ok(content_ids); } - fn parse_cd_keys(&self, content_title: &str, raw_cd_keys: &str) -> BTreeMap { - let mut cd_keys = BTreeMap::new(); - let mut raw_cd_keys_fix = raw_cd_keys.to_owned(); - - if raw_cd_keys_fix.contains("") { - raw_cd_keys_fix = raw_cd_keys_fix - .replace("", ":") - .replace("", "") - .replace("", ""); - } - - raw_cd_keys_fix = raw_cd_keys_fix.replace("
", ":").replace("::", ":"); - - if raw_cd_keys_fix.contains(":") { - let splitted_keys = raw_cd_keys_fix.split(":"); - - let mut key_names: Vec = Vec::new(); - let mut key_values: Vec = Vec::new(); - - for (token_index, token) in splitted_keys.enumerate() { - if token_index % 2 == 0 { - key_names.push(token.to_owned()); - } else { - key_values.push(token.trim().to_owned()); - } - } - - for (index, key_name) in key_names.iter().enumerate() { - let key_value = key_values[index].clone(); - cd_keys.insert(key_name.clone(), key_value); - } - } else if !raw_cd_keys_fix.is_empty() { - cd_keys.insert(content_title.to_owned(), raw_cd_keys_fix.trim().to_owned()); - } - - return cd_keys; - } - fn api_request_ensure_token(&mut self, response: &str) -> Result { let response_json: Value = match serde_json::from_str(response) { Ok(value) => value, @@ -436,118 +396,11 @@ impl<'a> Gog<'a> { let response = self.api_request_get(content_uri.as_str())?; - let content_raw: Value = serde_json::from_str(response.as_str())?; - debug!("found {:?}", &content_raw); - - let mut content: Content = serde_json::from_str(&response)?; - content.id = content_id; - - let downloads = &content_raw["downloads"]; - - if content_raw.is_object() && - !content_raw - .as_object() - .unwrap() - .contains_key("forumLink") { - return Err(GogError::Error("No forumLink property")); - } - - let is_movie = match content_raw["forumLink"].as_str() { - Some(value) => value == "https://embed.gog.com/forum/movies", - None => false, - }; - - content.is_movie = is_movie; - - if content_raw.is_object() && content_raw.as_object().unwrap().contains_key("cdKey") { - let cd_keys_raw = content_raw["cdKey"].as_str().unwrap(); - content.cd_keys = self.parse_cd_keys(&content.title, cd_keys_raw); - } - - debug!("processing installer fields: {:?}", &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 {}", content.title); - continue; - } - - let data_language = match language[0].as_str() { - Some(value) => value.to_lowercase(), - None => String::default(), - }; - - if data_language.is_empty() { - error!("Skipping a language for {}", content.title); - continue; - } - - if !is_movie && !language_filters.is_empty() && - !language_filters.contains(&data_language) { - info!("Skipping {} for {}", &data_language, content.title); - continue; - } - - for systems in language[1].as_object() { - for system in systems.keys() { - if is_movie && !os_filters.is_empty() && !os_filters.contains(system) { - info!("Skipping {} {} for {}", - &data_language, - system, - content.title); - continue; - } - - 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") || - !download.as_object().unwrap().contains_key("name") { - error!("Skipping data for {}", content.title); - continue; - } - - let name: &str = download["name"].as_str().unwrap(); - - - if content.is_movie && !resolution_filters.is_empty() { - let mut found_resolution = false; - for resolution_filter in resolution_filters { - let filter = format!("({})", resolution_filter); - if name.ends_with(&filter) { - found_resolution = true; - break; - } - } - - if !found_resolution { - info!("Skipping {}: not a suitable resolution.", name); - continue; - } - } - - let data = Data { - manual_url: String::from(download["manualUrl"] - .as_str() - .unwrap()), - version: String::from(download["version"] - .as_str() - .unwrap_or("")), - os: system.clone(), - language: data_language.clone(), - }; - - content.data.push(data); - } - } - } - } - } - } - } - - Ok(content) + models::content::deserialize(content_id, + response.as_str(), + os_filters, + language_filters, + resolution_filters) } fn games_uri(&self) -> String { diff --git a/src/models/content.rs b/src/models/content.rs index a311e21..2148d8c 100644 --- a/src/models/content.rs +++ b/src/models/content.rs @@ -1,7 +1,10 @@ +use gog::GogError; use models::data::Data; use models::extra::Extra; use regex::Regex; use serde::{Deserialize, Deserializer}; +use serde_json; +use serde_json::Value; use std::collections::BTreeMap; use std::fmt; @@ -11,7 +14,7 @@ use std::fmt; pub struct Content { #[serde(skip_deserializing)] pub id: u64, - #[serde(deserialize_with = "normalize_title")] + #[serde(deserialize_with = "deserialize_title")] pub title: String, #[serde(skip_deserializing)] #[serde(rename(deserialize = "cdKey"))] @@ -21,6 +24,7 @@ pub struct Content { #[serde(skip_deserializing)] pub data: Vec, pub extras: Vec, + pub dlcs: Vec, } impl fmt::Display for Content { @@ -29,7 +33,7 @@ impl fmt::Display for Content { } } -fn normalize_title(deserializer: D) -> Result +fn deserialize_title(deserializer: D) -> Result where D: Deserializer { let raw_title = String::deserialize(deserializer)?.replace(":", " - "); @@ -47,3 +51,160 @@ fn normalize_title(deserializer: D) -> Result Ok(title_whitespace) } + +fn deserialize_cd_keys(content_title: &str, raw_cd_keys: &str) -> BTreeMap { + let mut cd_keys = BTreeMap::new(); + let mut raw_cd_keys_fix = raw_cd_keys.to_owned(); + + if raw_cd_keys_fix.contains("") { + raw_cd_keys_fix = raw_cd_keys_fix + .replace("", ":") + .replace("", "") + .replace("", ""); + } + + raw_cd_keys_fix = raw_cd_keys_fix.replace("
", ":").replace("::", ":"); + + if raw_cd_keys_fix.contains(":") { + let splitted_keys = raw_cd_keys_fix.split(":"); + + let mut key_names: Vec = Vec::new(); + let mut key_values: Vec = Vec::new(); + + for (token_index, token) in splitted_keys.enumerate() { + if token_index % 2 == 0 { + key_names.push(token.to_owned()); + } else { + key_values.push(token.trim().to_owned()); + } + } + + for (index, key_name) in key_names.iter().enumerate() { + let key_value = key_values[index].clone(); + cd_keys.insert(key_name.clone(), key_value); + } + } else if !raw_cd_keys_fix.is_empty() { + cd_keys.insert(content_title.to_owned(), raw_cd_keys_fix.trim().to_owned()); + } + + cd_keys +} + +pub fn deserialize(content_id: u64, + content_string: &str, + os_filters: &Vec, + language_filters: &Vec, + resolution_filters: &Vec) + -> Result { + let content_raw: Value = serde_json::from_str(content_string)?; + debug!("found {:?}", &content_raw); + + let mut content: Content = serde_json::from_str(content_string)?; + content.id = content_id; + + let downloads = &content_raw["downloads"]; + + if content_raw.is_object() && + !content_raw + .as_object() + .unwrap() + .contains_key("forumLink") { + return Err(GogError::Error("No forumLink property")); + } + + let is_movie = match content_raw["forumLink"].as_str() { + Some(value) => value == "https://embed.gog.com/forum/movies", + None => false, + }; + + content.is_movie = is_movie; + + if content_raw.is_object() && content_raw.as_object().unwrap().contains_key("cdKey") { + let cd_keys_raw = content_raw["cdKey"].as_str().unwrap(); + content.cd_keys = deserialize_cd_keys(&content.title, cd_keys_raw); + } + + debug!("processing installer fields: {:?}", &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 {}", content.title); + continue; + } + + let data_language = match language[0].as_str() { + Some(value) => value.to_lowercase(), + None => String::default(), + }; + + if data_language.is_empty() { + error!("Skipping a language for {}", content.title); + continue; + } + + if !is_movie && !language_filters.is_empty() && + !language_filters.contains(&data_language) { + info!("Skipping {} for {}", &data_language, content.title); + continue; + } + + for systems in language[1].as_object() { + for system in systems.keys() { + if is_movie && !os_filters.is_empty() && !os_filters.contains(system) { + info!("Skipping {} {} for {}", + &data_language, + system, + content.title); + continue; + } + + 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") || + !download.as_object().unwrap().contains_key("name") { + error!("Skipping data for {}", content.title); + continue; + } + + let name: &str = download["name"].as_str().unwrap(); + + + if content.is_movie && !resolution_filters.is_empty() { + let mut found_resolution = false; + for resolution_filter in resolution_filters { + let filter = format!("({})", resolution_filter); + if name.ends_with(&filter) { + found_resolution = true; + break; + } + } + + if !found_resolution { + info!("Skipping {}: not a suitable resolution.", name); + continue; + } + } + + let data = Data { + manual_url: String::from(download["manualUrl"] + .as_str() + .unwrap()), + version: + String::from(download["version"].as_str().unwrap_or("")), + os: system.clone(), + language: data_language.clone(), + }; + + content.data.push(data); + } + } + } + } + } + } + } + + Ok(content) +} From 7a40242d67dabe52eef0501fea56d428bd6f63d5 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Wed, 10 May 2017 14:19:09 +0200 Subject: [PATCH 02/10] save dlcs --- src/gog.rs | 254 ++++++++++++++++++++++++-------------- src/models/content.rs | 19 +++ src/sync.rs | 277 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 461 insertions(+), 89 deletions(-) create mode 100644 src/sync.rs diff --git a/src/gog.rs b/src/gog.rs index 7172156..6361dae 100644 --- a/src/gog.rs +++ b/src/gog.rs @@ -15,15 +15,18 @@ 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::fmt; use std::fs; use std::fs::File; +use std::fmt; use std::io; use std::io::Write; use std::path::Path; @@ -131,7 +134,7 @@ impl<'a> Gog<'a> { /// 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, + storage_path_games: &str, storage_path_movies: &str, os_filters: &Vec, language_filters: &Vec, @@ -153,120 +156,192 @@ impl<'a> Gog<'a> { } }; - let content_root = if content.is_movie { - Path::new(storage_path_movies).join(&content.title) - } else { - Path::new(storage_path).join(&content.title) - }; - - let content_info_path = Path::new(&content_root).join("info.json"); - - let content_info_saved = - match ConfigFiles::load_from_path::(&content_info_path) { - Ok(value) => value, - Err(_) => ContentInfo::new(), - }; - if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) { info!("filtering {}", content.title); continue; } - let content_hash = models::get_hash(&content); - - if content_info_saved.hash == content_hash { - info!("{} already up to date.", &content.title); - continue; + match self.sync_content(content, storage_path_games, storage_path_movies) { + Ok(_) => (), + Err(error) => { + warn!("{:?}", error); + continue; + } } + } + Ok(()) + } - fs::create_dir_all(&content_root)?; + fn sync_content(&mut self, + content: Content, + storage_path_games: &str, + storage_path_movies: &str) + -> Result<(), GogError> { - 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(), + 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::(&content_info_path) { + Ok(value) => value, + Err(_) => ContentInfo::new(), }; - ConfigFiles::save_to_path(&content_info_path, &content_info)?; + let content_hash = models::get_hash(&content); - let key_root = content_root.join("keys"); + if content_info_saved.hash == content_hash { + info!("{} already up to date.", &content.title); + return Ok(()); + } - if content.cd_keys.len() > 0 { - fs::create_dir_all(&key_root)?; - } + fs::create_dir_all(&content_root)?; - for (key, value) in content.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())? - } + 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(), + }; - for data in content.data { - let data_hash_saved = match content_info_saved.data.get(&content_id.to_string()) { - Some(value) => value.hash, - None => u64::min_value(), - }; + ConfigFiles::save_to_path(&content_info_path, &content_info)?; - let data_hash = models::get_hash(&data); + self.save_keys(content.cd_keys, &content_root)?; - if data_hash_saved == data_hash { - info!("{} already up to date.", &data.manual_url); - continue; - } + 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 data_root = Path::new(&content_root).join(&data.language); - fs::create_dir_all(&data_root)?; + let dlc_root = Path::new(&storage_path_games) + .join(&content.title) + .join("dlcs"); - let data_uri = format!("https://embed.gog.com{}", data.manual_url); + for dlc in content.dlcs { + self.sync_content(dlc, + dlc_root.to_string_lossy().as_ref(), + storage_path_movies)?; + } - info!("downloading {} for {}...", &data.manual_url, &content.title); - let filename = self.api_request_download(data_uri.as_str(), &data_root)?; + for extra in content.extras { + self.save_extra(content.title.as_str(), + extra, + &content_root, + &content_info_path, + &content_info_saved, + &mut content_info)?; + } - let data_info = DataInfo { - hash: data_hash, - filename: filename, - language: data.language, - }; + Ok(()) + } - content_info - .data - .insert(data.manual_url.clone(), data_info); - ConfigFiles::save_to_path(&content_info_path, &content_info)?; - } + fn save_file(&mut self, + content_title: &str, + relative_uri: &str, + data_root: &PathBuf) + -> Result { + fs::create_dir_all(&data_root)?; + let data_uri = format!("https://embed.gog.com{}", relative_uri); - for extra in content.extras { - let extra_hash_saved = - match content_info_saved.extras.get(&content_id.to_string()) { - Some(value) => value.hash, - None => u64::min_value(), - }; + info!("downloading {} for {}...", &relative_uri, content_title); + self.api_request_download(data_uri.as_str(), &data_root) + } - let extra_hash = models::get_hash(&extra); + 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(), + }; - if extra_hash_saved == extra_hash { - info!("{} already up to date.", &extra.manual_url); - continue; - } + let hash = models::get_hash(&data); + if hash_saved == hash { + info!("{} already up to date.", &data.manual_url); + return Ok(()); + } - let extra_uri = format!("https://embed.gog.com{}", extra.manual_url); + let data_root = Path::new(&content_root).join(&data.language); - info!("downloading {} for {}...", &extra.name, &content.title); - let filename = self.api_request_download(extra_uri.as_str(), &content_root)?; + let filename = self.save_file(content_title, &data.manual_url, &data_root)?; - let extra_info = ExtraInfo { - hash: extra_hash, - filename: filename, - name: extra.name, - }; + let data_info = DataInfo { + hash: hash, + filename: filename, + language: data.language, + }; - content_info - .extras - .insert(extra.manual_url.clone(), extra_info); - ConfigFiles::save_to_path(&content_info_path, &content_info)?; - } + 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, + 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(()) @@ -347,7 +422,8 @@ impl<'a> Gog<'a> { content_ids.push(content_id_parsed); } - return Ok(content_ids); + //Ok(content_ids) + Ok(vec![1207658995]) } fn api_request_ensure_token(&mut self, response: &str) -> Result { diff --git a/src/models/content.rs b/src/models/content.rs index 2148d8c..b8a506d 100644 --- a/src/models/content.rs +++ b/src/models/content.rs @@ -102,6 +102,7 @@ pub fn deserialize(content_id: u64, let mut content: Content = serde_json::from_str(content_string)?; content.id = content_id; + let dlcs = &content_raw["dlcs"]; let downloads = &content_raw["downloads"]; if content_raw.is_object() && @@ -124,6 +125,24 @@ pub fn deserialize(content_id: u64, content.cd_keys = deserialize_cd_keys(&content.title, cd_keys_raw); } + match dlcs.as_array() { + Some(value) => { + if value.len() > 0 { + debug!("processing dlc fields: {:?}", &value); + for dlc in value { + let dlc = deserialize(content_id, + serde_json::to_string(&dlc).unwrap().as_str(), + os_filters, + language_filters, + resolution_filters)?; + content.dlcs.push(dlc); + } + } + } + None => (), + } + + debug!("processing installer fields: {:?}", &downloads); for languages in downloads.as_array() { for language in languages { diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..74e2886 --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,277 @@ +use configfiles::ConfigFiles; +use gog::{Gog, GogError}; +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 std::collections::HashMap; +use std::collections::BTreeMap; +use std::io; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::fs; +use std::fs::File; + +pub fn sync_content(content: Content, + storage_path_games: &str, + storage_path_movies: &str, + os_filters: &Vec, + language_filters: &Vec, + resolution_filters: &Vec, + skip_movies: bool, + skip_games: bool) + -> 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::(&content_info_path) { + Ok(value) => value, + Err(_) => ContentInfo::new(), + }; + + if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) { + info!("filtering {}", content.title); + return Ok(()); + } + + 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)?; + + save_keys(content.cd_keys, &content_root)?; + + for data in content.data { + save_data(content.title.as_str(), + data, + &content_root, + &content_info_path, + &content_info_saved, + &mut content_info); + } + + Ok(()) +} + +fn save_data(content_title: &str, + data: Data, + content_root: &PathBuf, + content_info_path: &PathBuf, + content_info_saved: &ContentInfo, + content_info: &mut ContentInfo) + -> Result<(), GogError> { + let data_hash_saved = match content_info_saved.data.get(data.manual_url.as_str()) { + Some(value) => value.hash, + None => u64::min_value(), + }; + + let data_hash = models::get_hash(&data); + if data_hash_saved == data_hash { + info!("{} already up to date.", &data.manual_url); + return Ok(()); + } + + let data_root = Path::new(&content_root).join(&data.language); + fs::create_dir_all(&data_root)?; + + let data_uri = format!("https://embed.gog.com{}", data.manual_url); + + info!("downloading {} for {}...", &data.manual_url, content_title); + let filename = self.api_request_download(data_uri.as_str(), &data_root)?; + + let data_info = DataInfo { + hash: data_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_keys(cd_keys: BTreeMap, 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(()) +} + +/* +/// Syncs the contents of a gog account with a local folder. +/// Uses a hash to figure out whether something has changed. +pub fn syncold(storage_path: &str, + storage_path_movies: &str, + os_filters: &Vec, + language_filters: &Vec, + resolution_filters: &Vec, + 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; + } + }; + + let content_root = if content.is_movie { + Path::new(storage_path_movies).join(&content.title) + } else { + Path::new(storage_path).join(&content.title) + }; + + let content_info_path = Path::new(&content_root).join("info.json"); + + let content_info_saved = + match ConfigFiles::load_from_path::(&content_info_path) { + Ok(value) => value, + Err(_) => ContentInfo::new(), + }; + + if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) { + info!("filtering {}", content.title); + continue; + } + + let content_hash = models::get_hash(&content); + + if content_info_saved.hash == content_hash { + info!("{} already up to date.", &content.title); + continue; + } + + 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)?; + + let key_root = content_root.join("keys"); + + if content.cd_keys.len() > 0 { + fs::create_dir_all(&key_root)?; + } + + for (key, value) in content.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())? + } + + for data in content.data { + let data_hash_saved = match content_info_saved.data.get(&content_id.to_string()) { + Some(value) => value.hash, + None => u64::min_value(), + }; + + let data_hash = models::get_hash(&data); + + if data_hash_saved == data_hash { + info!("{} already up to date.", &data.manual_url); + continue; + } + + let data_root = Path::new(&content_root).join(&data.language); + fs::create_dir_all(&data_root)?; + + let data_uri = format!("https://embed.gog.com{}", data.manual_url); + + info!("downloading {} for {}...", &data.manual_url, &content.title); + let filename = self.api_request_download(data_uri.as_str(), &data_root)?; + + let data_info = DataInfo { + hash: data_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)?; + } + + for extra in content.extras { + let extra_hash_saved = match content_info_saved.extras.get(&content_id.to_string()) { + Some(value) => value.hash, + None => u64::min_value(), + }; + + let extra_hash = models::get_hash(&extra); + + if extra_hash_saved == extra_hash { + info!("{} already up to date.", &extra.manual_url); + continue; + } + + let extra_uri = format!("https://embed.gog.com{}", extra.manual_url); + + info!("downloading {} for {}...", &extra.name, &content.title); + let filename = self.api_request_download(extra_uri.as_str(), &content_root)?; + + let extra_info = ExtraInfo { + hash: extra_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(()) +}*/ From 1f8329cd3a4f7521b2caef73b77d9a8317b88d7d Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Wed, 10 May 2017 14:50:38 +0200 Subject: [PATCH 03/10] sync dlcs (fixes #12) break content deserializing and sync code into smaller pieces (fixes #9) --- CHANGELOG.md | 4 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- src/gog.rs | 20 ++++++++++++++-- src/http.rs | 64 +++++++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 2 +- 6 files changed, 78 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6492045..89557bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.4 (2017-05-10) +- sync dlcs (fixes #12) +- break content deserializing and sync code into smaller pieces (fixes #9) + ## 0.3.3 (2017-05-09) - normalize content directory names (fixes #11) diff --git a/Cargo.lock b/Cargo.lock index a6fb3ef..78c132b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ [root] name = "gog-sync" -version = "0.3.3" +version = "0.3.4" dependencies = [ "chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.24.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index ea3391c..4478e65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gog-sync" -version = "0.3.3" +version = "0.3.4" authors = ["Sebastian Hugentobler "] description = "Synchronizes a GOG library with a local folder." documentation = "https://docs.rs/crate/gog-sync" diff --git a/src/gog.rs b/src/gog.rs index 6361dae..8137c55 100644 --- a/src/gog.rs +++ b/src/gog.rs @@ -252,6 +252,14 @@ impl<'a> Gog<'a> { 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) } @@ -422,8 +430,7 @@ impl<'a> Gog<'a> { content_ids.push(content_id_parsed); } - //Ok(content_ids) - Ok(vec![1207658995]) + Ok(content_ids) } fn api_request_ensure_token(&mut self, response: &str) -> Result { @@ -452,6 +459,15 @@ impl<'a> Gog<'a> { } } + fn api_request_get_filename(&mut self, uri: &str) -> Result { + 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 { let response = self.http_client.download(uri, path)?; if self.api_request_ensure_token(&response)? { diff --git a/src/http.rs b/src/http.rs index 61335ff..c6b6f29 100644 --- a/src/http.rs +++ b/src/http.rs @@ -96,10 +96,11 @@ impl Http { self.curl.url(uri)?; { let mut transfer = self.curl.transfer(); - transfer.write_function(|new_data| { - data.extend_from_slice(new_data); - Ok(new_data.len()) - })?; + transfer + .write_function(|new_data| { + data.extend_from_slice(new_data); + Ok(new_data.len()) + })?; transfer.perform()?; } @@ -129,6 +130,46 @@ impl Http { } } + /// Find the filename for a download uri. + /// + /// Useful if the initial link gets redirected. + /// + /// The filename is taken from the last URI segment and returned in the result. + /// # Example + /// ``` + /// use http::Http; + /// use std::path::Path; + /// + /// let mut http_client = Http::new(); + /// + /// http_client.get_filename("https://example.com/sed"); + /// ``` + pub fn get_filename(&mut self, download_uri: &str) -> Result { + self.curl.url(download_uri)?; + self.curl.nobody(true)?; + self.curl.perform()?; + self.curl.nobody(false)?; + + 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.")), + }; + + Ok(file_name.to_owned()) + } + /// Download a file to the specified folder without creating the folder. /// /// The filename is taken from the last URI segment and returned in the result. @@ -152,13 +193,14 @@ impl Http { 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 + .write_function(|data| { + match file_download.write(data) { + Ok(_) => Ok(()), + Err(error) => Err(HttpError::IOError(error)), + }?; + Ok(data.len()) + })?; transfer.perform()?; } diff --git a/src/main.rs b/src/main.rs index 0c0bcd9..10d7b7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ fn main() { env_logger::init().unwrap(); let matches = App::new("Gog Synchronizer") - .version("0.3.3") + .version("0.3.4") .author("Sebastian Hugentobler ") .about("Synchronizes your gog library to a local folder.") .arg(Arg::with_name("game-storage") From 150cf0fe737963876a1cf1cd9d9b2592d76949a6 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Wed, 10 May 2017 15:00:28 +0200 Subject: [PATCH 04/10] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 56227a4..4a33bd0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ A bare configuration with default values before first use: "movieStorage": ".", "osFilters": [], "languageFilters": [], + "resolutionFilters": [], "skipMovies": false, "skipGames": false } From 0adf982da165b933d6b1f2f318e895adcd9c09f0 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Fri, 12 May 2017 10:03:18 +0200 Subject: [PATCH 05/10] remoce obsolete sync.rs --- src/sync.rs | 277 ---------------------------------------------------- 1 file changed, 277 deletions(-) delete mode 100644 src/sync.rs diff --git a/src/sync.rs b/src/sync.rs deleted file mode 100644 index 74e2886..0000000 --- a/src/sync.rs +++ /dev/null @@ -1,277 +0,0 @@ -use configfiles::ConfigFiles; -use gog::{Gog, GogError}; -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 std::collections::HashMap; -use std::collections::BTreeMap; -use std::io; -use std::io::Write; -use std::path::Path; -use std::path::PathBuf; -use std::fs; -use std::fs::File; - -pub fn sync_content(content: Content, - storage_path_games: &str, - storage_path_movies: &str, - os_filters: &Vec, - language_filters: &Vec, - resolution_filters: &Vec, - skip_movies: bool, - skip_games: bool) - -> 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::(&content_info_path) { - Ok(value) => value, - Err(_) => ContentInfo::new(), - }; - - if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) { - info!("filtering {}", content.title); - return Ok(()); - } - - 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)?; - - save_keys(content.cd_keys, &content_root)?; - - for data in content.data { - save_data(content.title.as_str(), - data, - &content_root, - &content_info_path, - &content_info_saved, - &mut content_info); - } - - Ok(()) -} - -fn save_data(content_title: &str, - data: Data, - content_root: &PathBuf, - content_info_path: &PathBuf, - content_info_saved: &ContentInfo, - content_info: &mut ContentInfo) - -> Result<(), GogError> { - let data_hash_saved = match content_info_saved.data.get(data.manual_url.as_str()) { - Some(value) => value.hash, - None => u64::min_value(), - }; - - let data_hash = models::get_hash(&data); - if data_hash_saved == data_hash { - info!("{} already up to date.", &data.manual_url); - return Ok(()); - } - - let data_root = Path::new(&content_root).join(&data.language); - fs::create_dir_all(&data_root)?; - - let data_uri = format!("https://embed.gog.com{}", data.manual_url); - - info!("downloading {} for {}...", &data.manual_url, content_title); - let filename = self.api_request_download(data_uri.as_str(), &data_root)?; - - let data_info = DataInfo { - hash: data_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_keys(cd_keys: BTreeMap, 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(()) -} - -/* -/// Syncs the contents of a gog account with a local folder. -/// Uses a hash to figure out whether something has changed. -pub fn syncold(storage_path: &str, - storage_path_movies: &str, - os_filters: &Vec, - language_filters: &Vec, - resolution_filters: &Vec, - 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; - } - }; - - let content_root = if content.is_movie { - Path::new(storage_path_movies).join(&content.title) - } else { - Path::new(storage_path).join(&content.title) - }; - - let content_info_path = Path::new(&content_root).join("info.json"); - - let content_info_saved = - match ConfigFiles::load_from_path::(&content_info_path) { - Ok(value) => value, - Err(_) => ContentInfo::new(), - }; - - if (content.is_movie && skip_movies) || (!content.is_movie && skip_games) { - info!("filtering {}", content.title); - continue; - } - - let content_hash = models::get_hash(&content); - - if content_info_saved.hash == content_hash { - info!("{} already up to date.", &content.title); - continue; - } - - 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)?; - - let key_root = content_root.join("keys"); - - if content.cd_keys.len() > 0 { - fs::create_dir_all(&key_root)?; - } - - for (key, value) in content.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())? - } - - for data in content.data { - let data_hash_saved = match content_info_saved.data.get(&content_id.to_string()) { - Some(value) => value.hash, - None => u64::min_value(), - }; - - let data_hash = models::get_hash(&data); - - if data_hash_saved == data_hash { - info!("{} already up to date.", &data.manual_url); - continue; - } - - let data_root = Path::new(&content_root).join(&data.language); - fs::create_dir_all(&data_root)?; - - let data_uri = format!("https://embed.gog.com{}", data.manual_url); - - info!("downloading {} for {}...", &data.manual_url, &content.title); - let filename = self.api_request_download(data_uri.as_str(), &data_root)?; - - let data_info = DataInfo { - hash: data_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)?; - } - - for extra in content.extras { - let extra_hash_saved = match content_info_saved.extras.get(&content_id.to_string()) { - Some(value) => value.hash, - None => u64::min_value(), - }; - - let extra_hash = models::get_hash(&extra); - - if extra_hash_saved == extra_hash { - info!("{} already up to date.", &extra.manual_url); - continue; - } - - let extra_uri = format!("https://embed.gog.com{}", extra.manual_url); - - info!("downloading {} for {}...", &extra.name, &content.title); - let filename = self.api_request_download(extra_uri.as_str(), &content_root)?; - - let extra_info = ExtraInfo { - hash: extra_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(()) -}*/ From 2b13a2beff8de8ccccc5a8312f346a7ee3753c87 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Thu, 22 Jun 2017 09:20:47 +0200 Subject: [PATCH 06/10] document the cd key extraction process --- src/models/content.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/models/content.rs b/src/models/content.rs index b8a506d..f4f4898 100644 --- a/src/models/content.rs +++ b/src/models/content.rs @@ -52,6 +52,40 @@ fn deserialize_title(deserializer: D) -> Result Ok(title_whitespace) } +/// Keys come in at least three different forms. +/// It is possible to parse them all into the same structure, a map. +/// +/// # Variants +/// ## I +/// Only one key and the value is the key itself. +/// `1234-5678-1234-5678` +/// +/// ## II +/// Multiple keys split at single `
` tags with the key name and value itself +/// split at `:
`. +/// ``` +/// Neverwinter Nights:
1234-5678-1234-5678
Shadows of Undrentide:
1234-5678-1234-5678
Hordes of the Underdark:
1234-5678-1234-5678 +/// ``` +/// +/// ## III +/// Multiple keys split at `
` tags with the key name and value itself +/// again split at `:
`. +/// +/// ``` +/// Neverwinter Nights 2:
1234-5678-1234-5678
Mask of the Betrayer:
1234-5678-1234-5678
Storm of Zehir:
1234-5678-1234-5678
+/// ``` +/// +/// With this information the method works as follows: +/// - replace every `
` with `:` +/// - remove every `` and `` +/// - replace every `
` with `:` +/// - replace every `::` with `:` +/// - if there is at least one `:` +/// - split at `:` +/// - every odd position in the resulting array is a key name, every even its corresponding value +/// - if not +/// - the content name is the key name and the value is used as is +/// fn deserialize_cd_keys(content_title: &str, raw_cd_keys: &str) -> BTreeMap { let mut cd_keys = BTreeMap::new(); let mut raw_cd_keys_fix = raw_cd_keys.to_owned(); From 64a4751cc37f6d932752ad2b8493891cd8ff8a46 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Fri, 3 Nov 2017 18:37:19 +0100 Subject: [PATCH 07/10] update deserialize_cd_keys documentation --- src/models/content.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/content.rs b/src/models/content.rs index f4f4898..7e47df2 100644 --- a/src/models/content.rs +++ b/src/models/content.rs @@ -83,7 +83,7 @@ fn deserialize_title(deserializer: D) -> Result /// - if there is at least one `:` /// - split at `:` /// - every odd position in the resulting array is a key name, every even its corresponding value -/// - if not +/// - else /// - the content name is the key name and the value is used as is /// fn deserialize_cd_keys(content_title: &str, raw_cd_keys: &str) -> BTreeMap { From 55d18a54f45ef7ab5ca63a80fab0516cb434153c Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Tue, 4 Dec 2018 18:47:50 +0100 Subject: [PATCH 08/10] update cargo lock --- Cargo.lock | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78c132b..d1c7ebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,20 +1,3 @@ -[root] -name = "gog-sync" -version = "0.3.4" -dependencies = [ - "chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.24.1 (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)", - "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "aho-corasick" version = "0.5.3" @@ -128,6 +111,23 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "gog-sync" +version = "0.3.4" +dependencies = [ + "chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.24.1 (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)", + "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "idna" version = "0.1.1" From 6b93d8dbb7b3c39f5f5245a2cafad361f6df2ae4 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Mon, 21 Jan 2019 11:06:40 +0100 Subject: [PATCH 09/10] update repository location --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4478e65..ca4a724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.3.4" authors = ["Sebastian Hugentobler "] description = "Synchronizes a GOG library with a local folder." documentation = "https://docs.rs/crate/gog-sync" -repository = "https://gitlab.com/thallian/gog-sync" +repository = "https://code.vanwa.ch/sebastian@vanwa.ch/gog-sync" readme = "README.md" keywords = ["gog", "sync", "cli"] categories = ["command-line-utilities"] From 43709e31c0d3983402770bebbd00cedc99125675 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Thu, 28 Mar 2024 15:43:54 +0100 Subject: [PATCH 10/10] add unmaintained note --- README.md | 55 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4a33bd0..cb379be 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,21 @@ +# UNMAINTAINED AND BROKEN + +Used to work, does not anymore, maybe can be still of interest for someone + # 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 with a local folder. -It builds on the work of the [unofficial GOG API Documentation](https://gogapidocs.readthedocs.io/en/latest/). +It builds on the work of the +[unofficial GOG API Documentation](https://gogapidocs.readthedocs.io/en/latest/). This is the first time I am building something with rust, so beware :) # Installation + Install from [crates.io](https://crates.io/crates/gog-sync). ``` @@ -16,8 +23,9 @@ cargo install gog-sync ``` # Configuration -The configuration file is in the config folder as described by the xdg specification -with a prefix of `gog-sync`. + +The configuration file is in the config folder as described by the xdg +specification with a prefix of `gog-sync`. For example on macOS or Linux @@ -39,27 +47,32 @@ A bare configuration with default values before first use: } ``` -- *gameStorage*: Where to save games -- *movieStorage*: Where to save movies -- *osFilters*: An array of operating systems. If it is not empty, game data is limited to the ones in the list. -- *languageFilters*: An array of languages. If it is not empty, game data is limited to the ones in the list. -- *resolutionFilters*: An array of resolutions. If it is not empty, movie data is limited to the ones in the list. -- *skipMovies*: Whether to skip movie content -- *skipGames*: Whether to skip game content +- _gameStorage_: Where to save games +- _movieStorage_: Where to save movies +- _osFilters_: An array of operating systems. If it is not empty, game data is + limited to the ones in the list. +- _languageFilters_: An array of languages. If it is not empty, game data is + limited to the ones in the list. +- _resolutionFilters_: An array of resolutions. If it is not empty, movie data + is limited to the ones in the list. +- _skipMovies_: Whether to skip movie content +- _skipGames_: Whether to skip game content -Valid values for *osFilter*: -- `linux` -- `mac` -- `windows` +Valid values for _osFilter_: + +- `linux` +- `mac` +- `windows` Check on [gog.com](https://www.gog.com/) which languages are available. An incomplete list of resolutions on gog: -- `DVD` -- `576p` -- `720p` -- `1080p` -- `4k` + +- `DVD` +- `576p` +- `720p` +- `1080p` +- `4k` # Usage @@ -90,8 +103,8 @@ OPTIONS: gog-sync ``` -Normal invocation, uses the current working directory as storage if not configured -otherwise. +Normal invocation, uses the current working directory as storage if not +configured otherwise. ---