gog-sync 0.3.4

Synchronizes a GOG library with a local folder.
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;

#[derive(Hash)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Content {
    #[serde(skip_deserializing)]
    pub id: u64,
    #[serde(deserialize_with = "deserialize_title")]
    pub title: String,
    #[serde(skip_deserializing)]
    #[serde(rename(deserialize = "cdKey"))]
    pub cd_keys: BTreeMap<String, String>,
    #[serde(skip_deserializing)]
    pub is_movie: bool,
    #[serde(skip_deserializing)]
    pub data: Vec<Data>,
    pub extras: Vec<Extra>,
    pub dlcs: Vec<Content>,
}

impl fmt::Display for Content {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({})", self.title)
    }
}

fn deserialize_title<D>(deserializer: D) -> Result<String, D::Error>
    where D: Deserializer
{
    let raw_title = String::deserialize(deserializer)?.replace(":", " - ");

    let regex_normalize = Regex::new(r"[^'^\w^\s^-]+").unwrap();
    let regex_whitespace = Regex::new(r"\s\s+").unwrap();

    let title_normalized = regex_normalize
        .replace_all(raw_title.as_str(), "")
        .to_string();

    let title_whitespace = regex_whitespace
        .replace_all(title_normalized.as_str(), " ")
        .to_string();

    Ok(title_whitespace)
}

fn deserialize_cd_keys(content_title: &str, raw_cd_keys: &str) -> BTreeMap<String, String> {
    let mut cd_keys = BTreeMap::new();
    let mut raw_cd_keys_fix = raw_cd_keys.to_owned();

    if raw_cd_keys_fix.contains("<span>") {
        raw_cd_keys_fix = raw_cd_keys_fix
            .replace("</span><span>", ":")
            .replace("<span>", "")
            .replace("</span>", "");
    }

    raw_cd_keys_fix = raw_cd_keys_fix.replace("<br>", ":").replace("::", ":");

    if raw_cd_keys_fix.contains(":") {
        let splitted_keys = raw_cd_keys_fix.split(":");

        let mut key_names: Vec<String> = Vec::new();
        let mut key_values: Vec<String> = 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<String>,
                   language_filters: &Vec<String>,
                   resolution_filters: &Vec<String>)
                   -> Result<Content, GogError> {
    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 dlcs = &content_raw["dlcs"];
    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);
    }

    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 {
            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)
}