diff --git a/src/cgg_data_source.rs b/src/cgg_data_source.rs new file mode 100644 index 0000000..4a36b53 --- /dev/null +++ b/src/cgg_data_source.rs @@ -0,0 +1,90 @@ +extern crate indexmap; +extern crate lazy_static; +extern crate log; +extern crate regex; +extern crate reqwest; +extern crate select; +extern crate serde; +extern crate serde_json; +extern crate simple_logger; +extern crate winreg; + +use indexmap::IndexMap; +use regex::Regex; +use select::document::Document; +use select::predicate::{Class, Name}; +use serde_json::Value; + +use data_source::DataSource; + +pub struct CGGDataSource; +impl DataSource for CGGDataSource { + fn get_champs_with_positions_and_patch( + client: &reqwest::Client, + ) -> (IndexMap>, String) { + let page = client + .get("https://champion.gg") + .send() + .unwrap() + .text() + .unwrap(); + let document = Document::from(&*page); + + let patch = document + .find(Class("analysis-holder")) + .next() + .unwrap() + .find(Name("strong")) + .next() + .unwrap() + .text(); + + let mut champions = IndexMap::new(); + for node in document.find(Class("champ-height")) { + let id = node + .find(Class("home-champion")) + .next() + .unwrap() + .attr("class") + .unwrap() + .split(' ') + .last() + .unwrap() + .to_string(); + let positions = node + .find(Name("a")) + .skip(1) + .map(|x| { + x.attr("href") + .unwrap() + .split('/') + .last() + .unwrap() + .to_string() + }) + .collect(); + champions.insert(id, positions); + } + + (champions, patch) + } + + fn get_champ_data(id: &str, position: &str, client: &reqwest::Client) -> Option { + let mut req = client + .get(&format!( + "https://champion.gg/champion/{}/{}?league=", + id, position + )) + .send() + .unwrap(); + if req.status().is_success() { + lazy_static! { + static ref RE: Regex = + Regex::new(r"(?m)^\s+matchupData\.championData = (.*)$").unwrap(); + } + serde_json::from_str(&RE.captures(&req.text().unwrap())?[1]).unwrap() + } else { + None + } + } +} diff --git a/src/data_source.rs b/src/data_source.rs new file mode 100644 index 0000000..e63dc8c --- /dev/null +++ b/src/data_source.rs @@ -0,0 +1,135 @@ +extern crate indexmap; +extern crate lazy_static; +extern crate log; +extern crate regex; +extern crate reqwest; +extern crate select; +extern crate serde; +extern crate serde_json; +extern crate simple_logger; +extern crate winreg; + +use indexmap::IndexMap; +use serde_json::Value; +use std::fs; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize)] +struct ItemSet { + title: String, + #[serde(rename = "type")] + type_: String, + map: String, + mode: String, + priority: bool, + sortrank: u32, + blocks: Vec, +} + +const CONSUMABLES: [u32; 9] = [2003, 2004, 2055, 2031, 2032, 2033, 2138, 2140, 2139]; +const TRINKETS: [u32; 3] = [3340, 3364, 3363]; +const ITEM_TYPES: &'static [(&str, [&str; 2]); 4] = &[ + ("Most Frequent Starters", ["firstItems", "mostGames"]), + ( + "Highest Win % Starters", + ["firstItems", "highestWinPercent"], + ), + ("Most Frequent Core Build", ["items", "mostGames"]), + ("Highest Win % Core Build", ["items", "highestWinPercent"]), +]; + +pub trait DataSource { + fn get_champs_with_positions_and_patch( + client: &reqwest::Client, + ) -> (IndexMap>, String); + fn get_champ_data(id: &str, position: &str, client: &reqwest::Client) -> Option; + + fn make_item_set(data: &Value, label: &str) -> Value { + json!({ + "items": data["items"].as_array().unwrap().iter().map(|x| json!({"id": x["id"].as_str(), "count": 1})).collect::>(), + "type": format!("{} ({:.2}% - {} games)", label, data["winPercent"].as_f64().unwrap() * 100., data["games"].as_u64().unwrap()) + }) + } + + fn make_item_set_from_list(list: &Vec, label: &str, key: &str, data: &Value) -> Value { + let mut key_order = String::new(); + if !data["skills"].get("skillInfo").is_none() { + key_order = data["skills"][key]["order"] + .as_array() + .unwrap() + .iter() + .map(|x| { + data["skills"]["skillInfo"].as_array().unwrap() + [x.as_str().unwrap().parse::().unwrap() - 1]["key"] + .as_str() + .unwrap() + }) + .collect::>() + .join("."); + } + json!({ + "items": list.iter().map(|x| json!({"id": x.to_string(), "count": 1})).collect::>(), + "type": format!("{} {}", label, key_order) + }) + } + fn write_item_set( + id: &str, + name: &str, + pos: &str, + ver: &str, + path: &PathBuf, + client: &reqwest::Client, + ) { + info!("Retrieving data for {} at {}", name, pos); + let data = Self::get_champ_data(id, pos, client); + + match data { + Some(data) => { + let mut item_set = ItemSet { + title: format!( + "CGG {} {} - {:.2}%", + pos, + ver, + data["stats"]["winRate"].as_f64().unwrap() * 100. + ), + type_: "custom".to_string(), + map: "any".to_string(), + mode: "any".to_string(), + priority: false, + sortrank: 0, + blocks: vec![], + }; + + for (label, path) in ITEM_TYPES.iter() { + if !data[&path[0]].get(&path[1]).is_none() { + item_set + .blocks + .push(Self::make_item_set(&data[&path[0]][&path[1]], label)); + } + } + + item_set.blocks.push(Self::make_item_set_from_list( + &CONSUMABLES.to_vec(), + "Consumables | Frequent:", + "mostGames", + &data, + )); + item_set.blocks.push(Self::make_item_set_from_list( + &TRINKETS.to_vec(), + "Trinkets | Wins:", + "highestWinPercent", + &data, + )); + + info!("Writing item set for {} at {}", name, pos); + fs::write( + path.join(format!("CGG_{}_{}.json", id, pos)), + serde_json::to_string_pretty(&item_set).unwrap(), + ).unwrap(); + } + None => { + error!("Can't get data for {} at {}", id, pos); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 034430a..3bb6b84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,12 +14,13 @@ extern crate lazy_static; extern crate indexmap; extern crate winreg; +mod cgg_data_source; +mod data_source; + +use cgg_data_source::CGGDataSource; +use data_source::DataSource; use indexmap::IndexMap; -use regex::Regex; use reqwest::header::{Headers, UserAgent}; -use select::document::Document; -use select::predicate::{Class, Name}; -use serde_json::Value; use std::io::{Error, ErrorKind}; use std::path::PathBuf; use std::{fs, thread, time}; @@ -41,32 +42,9 @@ struct ChampInfo { name: String, } -#[derive(Serialize, Deserialize)] -struct ItemSet { - title: String, - #[serde(rename = "type")] - type_: String, - map: String, - mode: String, - priority: bool, - sortrank: u32, - blocks: Vec, -} - const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0"; const LOL_CHAMPS_DIR: &str = ".\\champs"; -const CONSUMABLES: [u32; 9] = [2003, 2004, 2055, 2031, 2032, 2033, 2138, 2140, 2139]; -const TRINKETS: [u32; 3] = [3340, 3364, 3363]; -const ITEM_TYPES: &'static [(&str, [&str; 2]); 4] = &[ - ("Most Frequent Starters", ["firstItems", "mostGames"]), - ( - "Highest Win % Starters", - ["firstItems", "highestWinPercent"], - ), - ("Most Frequent Core Build", ["items", "mostGames"]), - ("Highest Win % Core Build", ["items", "highestWinPercent"]), -]; fn main() { simple_logger::init_with_level(log::Level::Info).unwrap(); @@ -107,7 +85,7 @@ fn main() { .unwrap(); info!("LoL numbers of champs: {}", champion.data.len()); - let (champs, patch) = get_champs_with_positions_and_patch(&client); + let (champs, patch) = CGGDataSource::get_champs_with_positions_and_patch(&client); info!("CGG version: {}", patch); info!("CGG numbers of champs: {}", champs.len()); @@ -117,7 +95,7 @@ fn main() { let path = lol_champs_dir.join(&id).join("Recommended"); fs::create_dir_all(&path).unwrap(); for pos in positions { - write_item_set( + CGGDataSource::write_item_set( &id, &champion.data.get(id).unwrap().name, &pos, @@ -133,152 +111,6 @@ fn main() { } } -fn get_champs_with_positions_and_patch( - client: &reqwest::Client, -) -> (IndexMap>, String) { - let page = client - .get("https://champion.gg") - .send() - .unwrap() - .text() - .unwrap(); - let document = Document::from(&*page); - - let patch = document - .find(Class("analysis-holder")) - .next() - .unwrap() - .find(Name("strong")) - .next() - .unwrap() - .text(); - - let mut champions = IndexMap::new(); - for node in document.find(Class("champ-height")) { - let id = node - .find(Class("home-champion")) - .next() - .unwrap() - .attr("class") - .unwrap() - .split(' ') - .last() - .unwrap() - .to_string(); - let positions = node - .find(Name("a")) - .skip(1) - .map(|x| { - x.attr("href") - .unwrap() - .split('/') - .last() - .unwrap() - .to_string() - }) - .collect(); - champions.insert(id, positions); - } - - (champions, patch) -} - -fn get_champ_data(id: &str, position: &str, client: &reqwest::Client) -> Option { - let mut req = client - .get(&format!( - "https://champion.gg/champion/{}/{}?league=", - id, position - )) - .send() - .unwrap(); - if req.status().is_success() { - lazy_static! { - static ref RE: Regex = - Regex::new(r"(?m)^\s+matchupData\.championData = (.*)$").unwrap(); - } - serde_json::from_str(&RE.captures(&req.text().unwrap())?[1]).unwrap() - } else { - None - } -} - -fn make_item_set(data: &Value, label: &str) -> Value { - json!({ - "items": data["items"].as_array().unwrap().iter().map(|x| json!({"id": x["id"].as_str(), "count": 1})).collect::>(), - "type": format!("{} ({:.2}% - {} games)", label, data["winPercent"].as_f64().unwrap() * 100., data["games"].as_u64().unwrap()) - }) -} - -fn make_item_set_from_list(list: &Vec, label: &str, key: &str, data: &Value) -> Value { - json!({ - "items": list.iter().map(|x| json!({"id": x.to_string(), "count": 1})).collect::>(), - "type": format!("{} {}", label, data["skills"][key]["order"].as_array().unwrap().iter() - .map(|x| data["skills"]["skillInfo"].as_array().unwrap()[x.as_str().unwrap().parse::().unwrap()-1]["key"].as_str().unwrap()) - .collect::>().join(".")) - }) -} - -fn write_item_set( - id: &str, - name: &str, - pos: &str, - ver: &str, - path: &PathBuf, - client: &reqwest::Client, -) { - info!("Retrieving data for {} at {}", name, pos); - let data = get_champ_data(id, pos, client); - - match data { - Some(data) => { - let mut item_set = ItemSet { - title: format!( - "CGG {} {} - {:.2}%", - pos, - ver, - data["stats"]["winRate"].as_f64().unwrap() * 100. - ), - type_: "custom".to_string(), - map: "any".to_string(), - mode: "any".to_string(), - priority: false, - sortrank: 0, - blocks: vec![], - }; - - for (label, path) in ITEM_TYPES.iter() { - if !data[&path[0]].get(&path[1]).is_none() { - item_set - .blocks - .push(make_item_set(&data[&path[0]][&path[1]], label)); - } - } - - item_set.blocks.push(make_item_set_from_list( - &CONSUMABLES.to_vec(), - "Consumables | Frequent:", - "mostGames", - &data, - )); - item_set.blocks.push(make_item_set_from_list( - &TRINKETS.to_vec(), - "Trinkets | Wins:", - "highestWinPercent", - &data, - )); - - info!("Writing item set for {} at {}", name, pos); - fs::write( - path.join(format!("CGG_{}_{}.json", id, pos)), - serde_json::to_string_pretty(&item_set).unwrap(), - ).unwrap(); - } - None => { - error!("Can't get data for {} at {}", id, pos); - } - } -} - fn lol_champ_dir() -> Result { let hklm = RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); let node = hklm.open_subkey(r"SOFTWARE\WOW6432Node\Riot Games\RADS");