diff --git a/Cargo.lock b/Cargo.lock index 3340033..3778444 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "cggitem_sets" version = "1.0.0" dependencies = [ + "chrono", "indexmap", "log", "logsy", diff --git a/Cargo.toml b/Cargo.toml index 60a948e..a90dc1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ version = "1.0.0" include = ["src/**/*"] [dependencies] +chrono = {version = "0.4.41", features = ["std"], default-features = false} indexmap = {version = "2.2", features = ["serde", "rayon"]} log = "0.4" logsy = "1.0.1" diff --git a/src/data_source.rs b/src/data_source.rs index 6ad7b9b..4f869f1 100644 --- a/src/data_source.rs +++ b/src/data_source.rs @@ -44,11 +44,7 @@ pub trait DataSource { fn get_timeout(&self) -> u64; - fn get_champs_with_positions( - &self, - client: &ureq::Agent, - champion: &Champion, - ) -> IndexMap>; + fn get_champs_with_positions(&self, champion: &Champion) -> IndexMap>; fn make_item_set(&self, items: Vec<&str>, label: String) -> Value { json!({ @@ -61,7 +57,6 @@ pub trait DataSource { &self, champ: &ChampInfo, positions: &[String], - client: &ureq::Agent, ) -> Vec<(String, Vec, Stat)>; fn write_item_set( @@ -69,7 +64,6 @@ pub trait DataSource { champ: &ChampInfo, positions: &[String], path: &Path, - client: &ureq::Agent, ) -> Result<(), Box> { info!( "{}: Retrieving data for {} at {}", @@ -77,7 +71,7 @@ pub trait DataSource { champ.name, positions.join(", ") ); - let data = self.get_champ_data_with_win_pourcentage(champ, positions, client); + let data = self.get_champ_data_with_win_pourcentage(champ, positions); let mut missing_roles = vec![]; for pos in positions { diff --git a/src/kb_data_source.rs b/src/kb_data_source.rs index 8921b9f..3203834 100644 --- a/src/kb_data_source.rs +++ b/src/kb_data_source.rs @@ -1,15 +1,13 @@ use crate::ChampInfo; use crate::Champion as ChampionLoL; -use crate::USER_AGENT_VALUE; use crate::data_source::{Build, DataSource, Item, Stat}; use indexmap::IndexMap; use log::error; use serde_derive::Deserialize; use serde_json::{Value, json}; -use std::time::Duration; -use ureq::Agent; pub struct KBDataSource { + client: ureq::Agent, token: Option, } @@ -117,39 +115,38 @@ struct Summoner { name: String, } -// It will be better to use Result... -fn get_auth_token() -> Option { - let client: Agent = Agent::config_builder() - .user_agent(USER_AGENT_VALUE) - .timeout_global(Some(Duration::from_secs(10))) - .build() - .into(); - let mut bundle = match client.get("https://koreanbuilds.net/bundle.js").call() { - Ok(mut resp) => match resp.body_mut().read_to_string() { - Ok(val) => val, - Err(_) => return None, - }, - Err(_) => return None, - }; - let auth_position = bundle.find("Authorization")?; - bundle = bundle[(auth_position + 13)..].to_string(); - let q_position = bundle.find('"')?; - bundle = bundle[(q_position + 1)..].to_string(); - bundle - .find('"') - .map(|position| bundle[..position].to_string()) -} - impl KBDataSource { - pub fn new() -> KBDataSource { + pub fn new(client: &ureq::Agent) -> Self { Self { - token: get_auth_token(), + client: client.clone(), + token: Self::fetch_auth_token(client), } } - fn get_champion_response(&self, client: &ureq::Agent) -> Option { + fn fetch_auth_token(client: &ureq::Agent) -> Option { + let resp = client.get("https://koreanbuilds.net/bundle.js").call(); + if let Ok(mut resp) = resp { + if let Ok(bundle) = resp.body_mut().read_to_string() { + if let Some(token) = Self::extract_token(&bundle) { + return Some(token); + } + } + } + None + } + + fn extract_token(bundle: &str) -> Option { + let auth_marker = "Authorization"; + let start = bundle.find(auth_marker)? + auth_marker.len(); + let after_marker = bundle[start..].find('"')? + start + 1; + let end = bundle[after_marker..].find('"')? + after_marker; + Some(bundle[after_marker..end].to_string()) + } + + fn get_champion_response(&self) -> Option { if let Some(token) = &self.token { - return match client + return match self + .client .get("https://api.koreanbuilds.net/champions?patchid=-1") .header("Accept", "application/json") .header("Authorization", token.as_str()) @@ -307,13 +304,9 @@ impl DataSource for KBDataSource { 300 } - fn get_champs_with_positions( - &self, - client: &ureq::Agent, - _champion: &ChampionLoL, - ) -> IndexMap> { + fn get_champs_with_positions(&self, _champion: &ChampionLoL) -> IndexMap> { let mut champions = IndexMap::new(); - let data: ChampionResponse = match self.get_champion_response(client) { + let data: ChampionResponse = match self.get_champion_response() { Some(val) => val, None => { return champions; @@ -329,11 +322,11 @@ impl DataSource for KBDataSource { &self, champ: &ChampInfo, position: &[String], - client: &ureq::Agent, ) -> Vec<(String, Vec, Stat)> { let mut champ_data = vec![]; if let Some(token) = &self.token { - let data: BuildResponse = match client + let data: BuildResponse = match self + .client .get(&format!( "https://api.koreanbuilds.net/builds?chmpname={}&patchid=-2&position=COMPOSITE", champ.name @@ -371,10 +364,15 @@ impl DataSource for KBDataSource { mod tests { use super::*; + use crate::create_http_client; + use std::sync::LazyLock; + + static DATASOURCE: LazyLock = + LazyLock::new(|| KBDataSource::new(&create_http_client())); #[test] fn test_get_auth_token() { - match get_auth_token() { + match &DATASOURCE.token { Some(token) => assert!(token.len() > 0), None => assert!(false), }; @@ -382,37 +380,22 @@ mod tests { #[test] fn test_get_champs_with_positions_and_patch() { - let client = ureq::Agent::config_builder() - .user_agent(USER_AGENT_VALUE) - .timeout_global(Some(Duration::from_secs(10))) - .build() - .into(); - let datasource = KBDataSource::new(); let champion = ChampionLoL { data: IndexMap::new(), }; - let champs_with_positions = datasource.get_champs_with_positions(&client, &champion); + let champs_with_positions = DATASOURCE.get_champs_with_positions(&champion); assert!(champs_with_positions.len() > 0); } #[test] fn test_get_champ_data_with_win_pourcentage() { - let client = ureq::Agent::config_builder() - .user_agent(USER_AGENT_VALUE) - .timeout_global(Some(Duration::from_secs(10))) - .build() - .into(); - let datasource = KBDataSource::new(); let champ = ChampInfo { id: String::from("Annie"), name: String::from("Annie"), key: String::from("1"), }; - let result = datasource.get_champ_data_with_win_pourcentage( - &champ, - &vec!["MID".to_string()], - &client, - ); + let result = + DATASOURCE.get_champ_data_with_win_pourcentage(&champ, &vec!["MID".to_string()]); assert!(!result.is_empty()); assert!(!result[0].1.is_empty()); assert!(result[0].2.win_rate > 0.); diff --git a/src/main.rs b/src/main.rs index d72ac1c..0d1f1bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use chrono::{Datelike, Local, NaiveDate}; use indexmap::IndexMap; #[cfg(target_os = "windows")] use log::debug; @@ -10,7 +11,6 @@ use std::io; use std::io::Error; #[cfg(target_os = "windows")] use std::io::ErrorKind; -use std::ops::Deref; use std::path::{Path, PathBuf}; use std::time::Instant; use std::{fs, thread, time}; @@ -44,8 +44,6 @@ pub struct ChampInfo { key: String, } -const USER_AGENT_VALUE: &str = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0"; const DEFAULT_LOL_CHAMPS_DIR: &str = "./champs"; #[cfg(target_os = "windows")] const REG_KEY_LOL_RADS: &str = r"SOFTWARE\WOW6432Node\Riot Games\RADS"; @@ -76,11 +74,7 @@ fn main() -> Result<(), Box> { }; info!("LoL Champs Folder: {}", lol_champs_dir.display()); - let client: Agent = Agent::config_builder() - .user_agent(USER_AGENT_VALUE) - .timeout_global(Some(Duration::from_secs(10))) - .build() - .into(); + let client: Agent = create_http_client(); let realm: Realm = client .get("https://ddragon.leagueoflegends.com/realms/euw.json") @@ -98,11 +92,13 @@ fn main() -> Result<(), Box> { .read_json()?; info!("LoL numbers of champs: {}", champion.data.len()); - let data_sources: Vec> = - vec![Box::new(KBDataSource::new()), Box::new(MSDataSource)]; + let data_sources: Vec> = vec![ + Box::new(KBDataSource::new(&client)), + Box::new(MSDataSource::new(&client)), + ]; data_sources.par_iter().for_each(|data_source| { let init = Instant::now(); - execute_data_source(data_source.deref(), &client, &champion, &lol_champs_dir); + execute_data_source(&**data_source, &champion, &lol_champs_dir); info!( "{}: done in {}s", data_source.get_alias(), @@ -123,11 +119,10 @@ fn get_champ_from_key(champs: &Champion, key: u32) -> Option { fn execute_data_source( data_source: &(dyn DataSource + Sync + Send), - client: &ureq::Agent, champion: &Champion, lol_champs_dir: &Path, ) { - let champs = data_source.get_champs_with_positions(client, champion); + let champs = data_source.get_champs_with_positions(champion); info!( "{} numbers of champs: {}", @@ -135,35 +130,22 @@ fn execute_data_source( champs.len() ); - if data_source.get_timeout() == 0 { - champs.par_iter().for_each(|(id, positions)| { - get_and_write_item_set( - data_source, - client, - champion, - lol_champs_dir, - *id, - positions, - ); - }); - } else { - champs.iter().for_each(|(id, positions)| { - get_and_write_item_set( - data_source, - client, - champion, - lol_champs_dir, - *id, - positions, - ); + let process = |(id, positions): (&u32, &Vec)| { + get_and_write_item_set(data_source, champion, lol_champs_dir, *id, positions); + if data_source.get_timeout() > 0 { thread::sleep(Duration::from_millis(data_source.get_timeout())); - }); + } }; + + if data_source.get_timeout() == 0 { + champs.par_iter().for_each(process); + } else { + champs.iter().for_each(process); + } } fn get_and_write_item_set( data_source: &(dyn DataSource + Sync + Send), - client: &ureq::Agent, champion: &Champion, lol_champs_dir: &Path, id: u32, @@ -176,7 +158,7 @@ fn get_and_write_item_set( } else { let path = lol_champs_dir.join(&champ_id).join("Recommended"); match fs::create_dir_all(&path) { - Ok(_) => match data_source.write_item_set(champ, positions, &path, client) { + Ok(_) => match data_source.write_item_set(champ, positions, &path) { Ok(_) => (), Err(e) => error!( "{}: Failed to write item set for {} at {}: {}", @@ -197,6 +179,28 @@ fn get_and_write_item_set( } } +fn create_http_client() -> Agent { + Agent::config_builder() + .user_agent(get_browser_user_agent()) + .timeout_global(Some(Duration::from_secs(10))) + .build() + .into() +} + +fn get_browser_user_agent() -> String { + let base_version = 125; + let start_date = NaiveDate::from_ymd_opt(2024, 4, 16).unwrap(); + let now = Local::now().naive_local().date(); + + let months_between = + (now.year() - start_date.year()) * 12 + (now.month() as i32 - start_date.month() as i32); + let version = base_version + months_between; + + format!( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{version}.0) Gecko/20100101 Firefox/{version}.0" + ) +} + #[cfg(target_os = "windows")] fn lol_champ_dir() -> Result { let hklm = RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); diff --git a/src/ms_data_source.rs b/src/ms_data_source.rs index 7db90b3..1c50c21 100644 --- a/src/ms_data_source.rs +++ b/src/ms_data_source.rs @@ -8,7 +8,9 @@ use crate::ChampInfo; use crate::Champion; use crate::data_source::{DataSource, Stat}; -pub struct MSDataSource; +pub struct MSDataSource { + client: ureq::Agent, +} #[derive(Deserialize)] struct MSChampion { @@ -38,6 +40,14 @@ fn find_next_number(rest: &str) -> f32 { 0.0 } +impl MSDataSource { + pub fn new(client: &ureq::Agent) -> Self { + MSDataSource { + client: client.clone(), + } + } +} + impl DataSource for MSDataSource { fn get_alias(&self) -> &str { "MS" @@ -47,14 +57,11 @@ impl DataSource for MSDataSource { 300 } - fn get_champs_with_positions( - &self, - client: &ureq::Agent, - champion: &Champion, - ) -> IndexMap> { + fn get_champs_with_positions(&self, champion: &Champion) -> IndexMap> { let mut champs = IndexMap::new(); - let champions: Vec = match client + let champions: Vec = match self + .client .get("https://www.metasrc.com/lol/search/lol") .call() .and_then(|mut resp| resp.body_mut().read_json()) @@ -85,11 +92,11 @@ impl DataSource for MSDataSource { &self, champ: &ChampInfo, positions: &[String], - client: &ureq::Agent, ) -> Vec<(String, Vec, Stat)> { let mut builds = vec![]; - let rep = client + let rep = self + .client .get( format!( "https://www.metasrc.com/lol/build/{}/{}",