refactor: enhance data source
This commit is contained in:
parent
03d4537743
commit
16f33620cd
4 changed files with 329 additions and 250 deletions
|
@ -7,18 +7,27 @@ use serde_json::{Value, json};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Represents a full item set for a champion, ready to be serialized to JSON.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct ItemSet {
|
struct ItemSet {
|
||||||
|
/// The title of the item set.
|
||||||
title: String,
|
title: String,
|
||||||
|
/// The type of the item set (e.g., "custom").
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
type_: String,
|
type_: String,
|
||||||
|
/// The map this item set is for (e.g., "any").
|
||||||
map: String,
|
map: String,
|
||||||
|
/// The mode this item set is for (e.g., "any").
|
||||||
mode: String,
|
mode: String,
|
||||||
|
/// Whether this item set has priority.
|
||||||
priority: bool,
|
priority: bool,
|
||||||
|
/// The sort rank of the item set.
|
||||||
sortrank: u32,
|
sortrank: u32,
|
||||||
|
/// The blocks of items in the set.
|
||||||
blocks: Vec<Value>,
|
blocks: Vec<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a build (block) of items.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Build {
|
pub struct Build {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
@ -26,6 +35,7 @@ pub struct Build {
|
||||||
pub items: Vec<Item>,
|
pub items: Vec<Item>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a single item in a build.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -39,26 +49,33 @@ pub struct Stat {
|
||||||
pub patch: String,
|
pub patch: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait for a data source that can provide champion item set data.
|
||||||
pub trait DataSource {
|
pub trait DataSource {
|
||||||
|
/// Returns the alias of the data source.
|
||||||
fn get_alias(&self) -> &str;
|
fn get_alias(&self) -> &str;
|
||||||
|
|
||||||
|
/// Returns the timeout for the data source.
|
||||||
fn get_timeout(&self) -> u64;
|
fn get_timeout(&self) -> u64;
|
||||||
|
|
||||||
|
/// Returns a map of champion IDs to their possible positions.
|
||||||
fn get_champs_with_positions(&self, champion: &Champion) -> IndexMap<u32, Vec<String>>;
|
fn get_champs_with_positions(&self, champion: &Champion) -> IndexMap<u32, Vec<String>>;
|
||||||
|
|
||||||
fn make_item_set(&self, items: Vec<&str>, label: String) -> Value {
|
/// Creates a JSON value representing an item set block from a list of item IDs and a label.
|
||||||
|
fn make_item_set(&self, items: Vec<String>, label: String) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"items": items.iter().map(|x| json!({"id": x.to_string(), "count": 1})).collect::<Vec<Value>>(),
|
"items": items.iter().map(|x| json!({"id": x, "count": 1})).collect::<Vec<Value>>(),
|
||||||
"type": label
|
"type": label
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns champion data with win percentage for the given positions.
|
||||||
fn get_champ_data_with_win_pourcentage(
|
fn get_champ_data_with_win_pourcentage(
|
||||||
&self,
|
&self,
|
||||||
champ: &ChampInfo,
|
champ: &ChampInfo,
|
||||||
positions: &[String],
|
positions: &[String],
|
||||||
) -> Vec<(String, Vec<Value>, Stat)>;
|
) -> Vec<(String, Vec<Value>, Stat)>;
|
||||||
|
|
||||||
|
/// Writes item sets for the given champion and positions to the specified path.
|
||||||
fn write_item_set(
|
fn write_item_set(
|
||||||
&self,
|
&self,
|
||||||
champ: &ChampInfo,
|
champ: &ChampInfo,
|
||||||
|
@ -73,19 +90,11 @@ pub trait DataSource {
|
||||||
);
|
);
|
||||||
let data = self.get_champ_data_with_win_pourcentage(champ, positions);
|
let data = self.get_champ_data_with_win_pourcentage(champ, positions);
|
||||||
|
|
||||||
let mut missing_roles = vec![];
|
let missing_roles: Vec<_> = positions
|
||||||
for pos in positions {
|
.iter()
|
||||||
let mut ok = false;
|
.filter(|pos| !data.iter().any(|build| &build.0 == *pos))
|
||||||
for build in &data {
|
.cloned()
|
||||||
if build.0 == *pos {
|
.collect();
|
||||||
ok = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
missing_roles.push(pos.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !missing_roles.is_empty() {
|
if !missing_roles.is_empty() {
|
||||||
error!(
|
error!(
|
||||||
"{}: Can't get data for {} at {}",
|
"{}: Can't get data for {} at {}",
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::ChampInfo;
|
use crate::ChampInfo;
|
||||||
use crate::Champion as ChampionLoL;
|
use crate::Champion as ChampionLoL;
|
||||||
use crate::data_source::{Build, DataSource, Item, Stat};
|
use crate::data_source::{DataSource, Item, Stat};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::Value;
|
||||||
|
|
||||||
pub struct KBDataSource {
|
pub struct KBDataSource {
|
||||||
client: ureq::Agent,
|
client: ureq::Agent,
|
||||||
|
@ -160,127 +160,74 @@ impl KBDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_positions(position: Option<Position>) -> Vec<String> {
|
fn get_positions(position: Option<Position>) -> Vec<String> {
|
||||||
let mut positions = Vec::new();
|
let positions = vec!["TOP", "JUNGLE", "MID", "BOT", "SUPPORT"];
|
||||||
if let Some(pos) = position {
|
if let Some(pos) = position {
|
||||||
if pos.top > 0 {
|
positions
|
||||||
positions.push("TOP".to_owned());
|
.into_iter()
|
||||||
}
|
.zip([pos.top, pos.jungle, pos.mid, pos.bot, pos.support])
|
||||||
if pos.jungle > 0 {
|
.filter_map(|(name, count)| {
|
||||||
positions.push("JUNGLE".to_owned());
|
if count > 0 {
|
||||||
}
|
Some(name.to_owned())
|
||||||
if pos.mid > 0 {
|
} else {
|
||||||
positions.push("MID".to_owned());
|
None
|
||||||
}
|
}
|
||||||
if pos.bot > 0 {
|
})
|
||||||
positions.push("BOT".to_owned());
|
.collect()
|
||||||
}
|
} else {
|
||||||
if pos.support > 0 {
|
positions.into_iter().map(|s| s.to_owned()).collect()
|
||||||
positions.push("SUPPORT".to_owned());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: find better solution, activate all positions for retrieve older builds
|
|
||||||
if positions.is_empty() {
|
|
||||||
positions.push("TOP".to_owned());
|
|
||||||
positions.push("JUNGLE".to_owned());
|
|
||||||
positions.push("MID".to_owned());
|
|
||||||
positions.push("BOT".to_owned());
|
|
||||||
positions.push("SUPPORT".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
positions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_build(&self, build: &KBBuild) -> (String, Vec<Value>, Stat) {
|
fn get_build(&self, build: &KBBuild) -> (String, Vec<Value>, Stat) {
|
||||||
let mut starting_items: Vec<Item> = vec![];
|
let mut starting_items: Vec<Item> = vec![];
|
||||||
let mut blocks = vec![];
|
let mut blocks = vec![];
|
||||||
if build.str_item_sets[0].str_item0.item_id != 0 {
|
for i in 0..6 {
|
||||||
starting_items.push(Item {
|
let item_id = match i {
|
||||||
id: build.str_item_sets[0].str_item0.item_id.to_string(),
|
0 => build.str_item_sets[0].str_item0.item_id,
|
||||||
count: 1,
|
1 => build.str_item_sets[0].str_item1.item_id,
|
||||||
})
|
2 => build.str_item_sets[0].str_item2.item_id,
|
||||||
|
3 => build.str_item_sets[0].str_item3.item_id,
|
||||||
|
4 => build.str_item_sets[0].str_item4.item_id,
|
||||||
|
5 => build.str_item_sets[0].str_item5.item_id,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
if item_id != 0 {
|
||||||
|
starting_items.push(Item {
|
||||||
|
id: item_id.to_string(),
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if build.str_item_sets[0].str_item1.item_id != 0 {
|
blocks.push(self.make_item_set(
|
||||||
starting_items.push(Item {
|
starting_items.iter().map(|item| item.id.clone()).collect(),
|
||||||
id: build.str_item_sets[0].str_item1.item_id.to_string(),
|
format!(
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.str_item_sets[0].str_item2.item_id != 0 {
|
|
||||||
starting_items.push(Item {
|
|
||||||
id: build.str_item_sets[0].str_item2.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.str_item_sets[0].str_item3.item_id != 0 {
|
|
||||||
starting_items.push(Item {
|
|
||||||
id: build.str_item_sets[0].str_item3.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.str_item_sets[0].str_item4.item_id != 0 {
|
|
||||||
starting_items.push(Item {
|
|
||||||
id: build.str_item_sets[0].str_item4.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.str_item_sets[0].str_item5.item_id != 0 {
|
|
||||||
starting_items.push(Item {
|
|
||||||
id: build.str_item_sets[0].str_item5.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
blocks.push(json!(Build {
|
|
||||||
type_: format!(
|
|
||||||
"Early game items | skillOrder : {}",
|
"Early game items | skillOrder : {}",
|
||||||
build.skill_sets[0].skill_order
|
build.skill_sets[0].skill_order
|
||||||
),
|
),
|
||||||
items: starting_items
|
));
|
||||||
}));
|
|
||||||
let mut final_items: Vec<Item> = vec![];
|
let mut final_items: Vec<Item> = vec![];
|
||||||
if build.item_sets[0].item0.item_id != 0 {
|
for item in [
|
||||||
final_items.push(Item {
|
&build.item_sets[0].item0,
|
||||||
id: build.item_sets[0].item0.item_id.to_string(),
|
&build.item_sets[0].item1,
|
||||||
count: 1,
|
&build.item_sets[0].item2,
|
||||||
})
|
&build.item_sets[0].item3,
|
||||||
|
&build.item_sets[0].item4,
|
||||||
|
&build.item_sets[0].item5,
|
||||||
|
] {
|
||||||
|
if item.item_id != 0 {
|
||||||
|
final_items.push(Item {
|
||||||
|
id: item.item_id.to_string(),
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if build.item_sets[0].item1.item_id != 0 {
|
blocks.push(self.make_item_set(
|
||||||
final_items.push(Item {
|
final_items.iter().map(|item| item.id.clone()).collect(),
|
||||||
id: build.item_sets[0].item1.item_id.to_string(),
|
format!(
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.item_sets[0].item2.item_id != 0 {
|
|
||||||
final_items.push(Item {
|
|
||||||
id: build.item_sets[0].item2.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.item_sets[0].item3.item_id != 0 {
|
|
||||||
final_items.push(Item {
|
|
||||||
id: build.item_sets[0].item3.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.item_sets[0].item4.item_id != 0 {
|
|
||||||
final_items.push(Item {
|
|
||||||
id: build.item_sets[0].item4.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if build.item_sets[0].item5.item_id != 0 {
|
|
||||||
final_items.push(Item {
|
|
||||||
id: build.item_sets[0].item5.item_id.to_string(),
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
blocks.push(json!(Build {
|
|
||||||
type_: format!(
|
|
||||||
"Item order by time finished | Summoner : {}",
|
"Item order by time finished | Summoner : {}",
|
||||||
build.summoner.name
|
build.summoner.name
|
||||||
),
|
),
|
||||||
items: final_items
|
));
|
||||||
}));
|
|
||||||
|
|
||||||
(
|
(
|
||||||
build.position.position_name.to_uppercase(),
|
build.position.position_name.to_uppercase(),
|
||||||
|
@ -325,12 +272,13 @@ impl DataSource for KBDataSource {
|
||||||
) -> Vec<(String, Vec<Value>, Stat)> {
|
) -> Vec<(String, Vec<Value>, Stat)> {
|
||||||
let mut champ_data = vec![];
|
let mut champ_data = vec![];
|
||||||
if let Some(token) = &self.token {
|
if let Some(token) = &self.token {
|
||||||
|
let url = format!(
|
||||||
|
"https://api.koreanbuilds.net/builds?chmpname={}&patchid=-2&position=COMPOSITE",
|
||||||
|
champ.name.replace(" ", "%20")
|
||||||
|
);
|
||||||
let data: BuildResponse = match self
|
let data: BuildResponse = match self
|
||||||
.client
|
.client
|
||||||
.get(&format!(
|
.get(&url)
|
||||||
"https://api.koreanbuilds.net/builds?chmpname={}&patchid=-2&position=COMPOSITE",
|
|
||||||
champ.name
|
|
||||||
))
|
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
.header("Authorization", token.as_str())
|
.header("Authorization", token.as_str())
|
||||||
.call()
|
.call()
|
||||||
|
@ -343,7 +291,7 @@ impl DataSource for KBDataSource {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
error!("Call failed: {}", x);
|
error!("Call failed for URL: {}, error: {}", url, x);
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -392,7 +340,7 @@ mod tests {
|
||||||
let champ = ChampInfo {
|
let champ = ChampInfo {
|
||||||
id: String::from("Annie"),
|
id: String::from("Annie"),
|
||||||
name: String::from("Annie"),
|
name: String::from("Annie"),
|
||||||
key: String::from("1"),
|
key: 1,
|
||||||
};
|
};
|
||||||
let result =
|
let result =
|
||||||
DATASOURCE.get_champ_data_with_win_pourcentage(&champ, &vec!["MID".to_string()]);
|
DATASOURCE.get_champ_data_with_win_pourcentage(&champ, &vec!["MID".to_string()]);
|
||||||
|
|
149
src/main.rs
149
src/main.rs
|
@ -26,6 +26,7 @@ mod ms_data_source;
|
||||||
use data_source::DataSource;
|
use data_source::DataSource;
|
||||||
use kb_data_source::KBDataSource;
|
use kb_data_source::KBDataSource;
|
||||||
use ms_data_source::MSDataSource;
|
use ms_data_source::MSDataSource;
|
||||||
|
use serde::{self, Deserialize, Deserializer};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Realm {
|
struct Realm {
|
||||||
|
@ -41,7 +42,16 @@ pub struct Champion {
|
||||||
pub struct ChampInfo {
|
pub struct ChampInfo {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
key: String,
|
#[serde(deserialize_with = "from_string_to_u32")]
|
||||||
|
key: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_string_to_u32<'de, D>(deserializer: D) -> Result<u32, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
s.parse::<u32>().map_err(serde::de::Error::custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_LOL_CHAMPS_DIR: &str = "./champs";
|
const DEFAULT_LOL_CHAMPS_DIR: &str = "./champs";
|
||||||
|
@ -56,40 +66,16 @@ const REG_KEY_WIN_64_UNINSTALL: &str =
|
||||||
const REG_KEY_WIN_UNINSTALL: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\";
|
const REG_KEY_WIN_UNINSTALL: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\";
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args: Vec<String> = env::args().collect();
|
setup_logging_from_args();
|
||||||
let mut level = LevelFilter::Info;
|
info!("CGG Item Sets v{}", env!("CARGO_PKG_VERSION"));
|
||||||
for s in &args {
|
|
||||||
if s.eq_ignore_ascii_case("-v") || s.eq_ignore_ascii_case("--verbose") {
|
|
||||||
level = LevelFilter::Debug;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logsy::set_echo(true);
|
|
||||||
logsy::set_level(level);
|
|
||||||
info!("CGG Item Sets");
|
|
||||||
|
|
||||||
let lol_champs_dir: PathBuf = match lol_champ_dir() {
|
let lol_champs_dir = get_lol_champs_dir();
|
||||||
Ok(x) => x,
|
|
||||||
Err(_e) => PathBuf::from(DEFAULT_LOL_CHAMPS_DIR),
|
|
||||||
};
|
|
||||||
info!("LoL Champs Folder: {}", lol_champs_dir.display());
|
info!("LoL Champs Folder: {}", lol_champs_dir.display());
|
||||||
|
|
||||||
let client: Agent = create_http_client();
|
let client = create_http_client();
|
||||||
|
let realm = fetch_realm(&client)?;
|
||||||
let realm: Realm = client
|
|
||||||
.get("https://ddragon.leagueoflegends.com/realms/euw.json")
|
|
||||||
.call()?
|
|
||||||
.body_mut()
|
|
||||||
.read_json()?;
|
|
||||||
info!("LoL version: {}", realm.v);
|
info!("LoL version: {}", realm.v);
|
||||||
let champion: Champion = client
|
let champion = fetch_champions(&client, &realm)?;
|
||||||
.get(&format!(
|
|
||||||
"https://ddragon.leagueoflegends.com/cdn/{}/data/en_US/champion.json",
|
|
||||||
realm.v
|
|
||||||
))
|
|
||||||
.call()?
|
|
||||||
.body_mut()
|
|
||||||
.read_json()?;
|
|
||||||
info!("LoL numbers of champs: {}", champion.data.len());
|
info!("LoL numbers of champs: {}", champion.data.len());
|
||||||
|
|
||||||
let data_sources: Vec<Box<dyn DataSource + Sync + Send>> = vec![
|
let data_sources: Vec<Box<dyn DataSource + Sync + Send>> = vec![
|
||||||
|
@ -108,13 +94,54 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_champ_from_key(champs: &Champion, key: u32) -> Option<String> {
|
fn setup_logging_from_args() {
|
||||||
for champ in champs.data.values() {
|
let args: Vec<String> = env::args().collect();
|
||||||
if key.to_string() == champ.key {
|
let mut level = LevelFilter::Info;
|
||||||
return Some(champ.id.to_owned());
|
for s in &args {
|
||||||
|
if s.eq_ignore_ascii_case("-v") || s.eq_ignore_ascii_case("--verbose") {
|
||||||
|
level = LevelFilter::Debug;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
logsy::set_echo(true);
|
||||||
|
logsy::set_level(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_lol_champs_dir() -> PathBuf {
|
||||||
|
match lol_champ_dir() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to detect LoL champs dir: {e}, using default.");
|
||||||
|
PathBuf::from(DEFAULT_LOL_CHAMPS_DIR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_realm(client: &Agent) -> Result<Realm, Box<dyn std::error::Error>> {
|
||||||
|
let url = "https://ddragon.leagueoflegends.com/realms/euw.json";
|
||||||
|
info!("Fetching realm info from {url}");
|
||||||
|
let realm: Realm = client.get(url).call()?.body_mut().read_json()?;
|
||||||
|
Ok(realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_champions(client: &Agent, realm: &Realm) -> Result<Champion, Box<dyn std::error::Error>> {
|
||||||
|
let url = format!(
|
||||||
|
"https://ddragon.leagueoflegends.com/cdn/{}/data/en_US/champion.json",
|
||||||
|
realm.v
|
||||||
|
);
|
||||||
|
info!("Fetching champion data from {url}");
|
||||||
|
let champion: Champion = client.get(&url).call()?.body_mut().read_json()?;
|
||||||
|
Ok(champion)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_champ_from_key(champs: &Champion, key: u32) -> Option<String> {
|
||||||
|
champs.data.values().find_map(|champ| {
|
||||||
|
if champ.key == key {
|
||||||
|
Some(champ.id.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_data_source(
|
fn execute_data_source(
|
||||||
|
@ -151,31 +178,33 @@ fn get_and_write_item_set(
|
||||||
id: u32,
|
id: u32,
|
||||||
positions: &[String],
|
positions: &[String],
|
||||||
) {
|
) {
|
||||||
if let Some(champ_id) = get_champ_from_key(champion, id) {
|
let champ = match get_champ_from_key(champion, id).and_then(|id| champion.data.get(&id)) {
|
||||||
if let Some(champ) = champion.data.get(&champ_id) {
|
Some(champ) => champ,
|
||||||
if positions.is_empty() {
|
None => {
|
||||||
error!("{}: {} empty positions", data_source.get_alias(), &champ_id);
|
error!("{} not found in LoL champs", &id);
|
||||||
} else {
|
return;
|
||||||
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) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(e) => error!(
|
|
||||||
"{}: Failed to write item set for {} at {}: {}",
|
|
||||||
data_source.get_alias(),
|
|
||||||
champ.name,
|
|
||||||
positions.join(", "),
|
|
||||||
e
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to create directory for {}: {}", champ_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("{} not found in LoL champs", &champ_id);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if positions.is_empty() {
|
||||||
|
error!("{}: {} empty positions", data_source.get_alias(), &champ.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = lol_champs_dir.join(&champ.id).join("Recommended");
|
||||||
|
if let Err(e) = fs::create_dir_all(&path) {
|
||||||
|
error!("Failed to create directory for {}: {}", &champ.id, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = data_source.write_item_set(champ, positions, &path) {
|
||||||
|
error!(
|
||||||
|
"{}: Failed to write item set for {} at {}: {}",
|
||||||
|
data_source.get_alias(),
|
||||||
|
champ.id,
|
||||||
|
positions.join(", "),
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use log::error;
|
use log::{error, warn};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::ChampInfo;
|
use crate::ChampInfo;
|
||||||
use crate::Champion;
|
use crate::Champion;
|
||||||
|
@ -19,25 +20,116 @@ struct MSChampion {
|
||||||
search_terms: String,
|
search_terms: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_champ_from_name(champs: &Champion, name: String) -> Option<u32> {
|
// Compile regexes once for performance
|
||||||
for champ in champs.data.values() {
|
static NUMBER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([0-9]+\.?[0-9]+)").unwrap());
|
||||||
|
static ITEM_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/item/([0-9]+)\.").unwrap());
|
||||||
|
|
||||||
|
fn get_champ_from_name(champs: &Champion, name: &str) -> Option<u32> {
|
||||||
|
champs.data.values().find_map(|champ| {
|
||||||
if name == champ.name || name == champ.id {
|
if name == champ.name || name == champ.id {
|
||||||
return champ.key.parse::<u32>().ok();
|
Some(champ.key)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_next_number(rest: &str) -> f32 {
|
||||||
|
if let Some(cap) = NUMBER_REGEX.captures(rest) {
|
||||||
|
if let Some(matched) = cap.get(1) {
|
||||||
|
return matched.as_str().parse::<f32>().unwrap_or(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_matching_div(html: &str, start_pos: usize) -> Option<(usize, usize)> {
|
||||||
|
let mut open_divs = 0;
|
||||||
|
let mut i = start_pos;
|
||||||
|
let len = html.len();
|
||||||
|
|
||||||
|
while i < len {
|
||||||
|
if html[i..].starts_with("<div") {
|
||||||
|
open_divs += 1;
|
||||||
|
i += 4;
|
||||||
|
} else if html[i..].starts_with("</div>") {
|
||||||
|
open_divs -= 1;
|
||||||
|
i += 6;
|
||||||
|
if open_divs == 0 {
|
||||||
|
return Some((start_pos, i));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_next_number(rest: &str) -> f32 {
|
fn extract_items_from_section(page: &str, section_title: &str) -> Vec<String> {
|
||||||
let re = Regex::new(r"([0-9]+\.?[0-9]+)");
|
if let Some(h3_pos) = page.find(section_title) {
|
||||||
if let Ok(re) = re {
|
// Find the start of the div containing the h3
|
||||||
if let Some(cap) = re.captures(rest) {
|
let div_start = page[..h3_pos].rfind("<div").unwrap_or(0);
|
||||||
if let Some(matched) = cap.get(1) {
|
if let Some((div_start, div_end)) = find_matching_div(page, div_start) {
|
||||||
return matched.as_str().parse::<f32>().unwrap_or(0.0);
|
let div_html = &page[div_start..div_end];
|
||||||
}
|
return ITEM_REGEX
|
||||||
|
.captures_iter(div_html)
|
||||||
|
.map(|cap| cap[1].to_owned())
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Failed to find matching </div> for section '{}'",
|
||||||
|
section_title
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0.0
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_skill_order_from_table(page: &str) -> String {
|
||||||
|
// Find the table containing the skill order (look for Q.png as anchor)
|
||||||
|
let table_start = page
|
||||||
|
.find("Q.png")
|
||||||
|
.and_then(|pos| page[..pos].rfind("<table"))
|
||||||
|
.unwrap_or(0);
|
||||||
|
let table_end = page[table_start..]
|
||||||
|
.find("</table>")
|
||||||
|
.map(|e| table_start + e + 8)
|
||||||
|
.unwrap_or(page.len());
|
||||||
|
let table_html = &page[table_start..table_end];
|
||||||
|
|
||||||
|
// Extract rows
|
||||||
|
let rows: Vec<&str> = table_html
|
||||||
|
.match_indices("<tr")
|
||||||
|
.map(|(i, _)| {
|
||||||
|
let end = table_html[i..]
|
||||||
|
.find("</tr>")
|
||||||
|
.map(|e| i + e + 5)
|
||||||
|
.unwrap_or(table_html.len());
|
||||||
|
&table_html[i..end]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Only process Q/W/E/R rows (skip header)
|
||||||
|
let skills = ["Q", "W", "E", "R"];
|
||||||
|
let mut order = [""; 18];
|
||||||
|
for (i, row) in rows.iter().skip(1).take(4).enumerate() {
|
||||||
|
let mut col = 0;
|
||||||
|
let mut pos = 0;
|
||||||
|
while let Some(td_start) = row[pos..].find("<td") {
|
||||||
|
let td_start = pos + td_start;
|
||||||
|
let td_end = row[td_start..]
|
||||||
|
.find("</td>")
|
||||||
|
.map(|e| td_start + e + 5)
|
||||||
|
.unwrap_or(row.len());
|
||||||
|
let td_html = &row[td_start..td_end];
|
||||||
|
if td_html.contains(&format!(">{}<", skills[i])) && col < 18 {
|
||||||
|
order[col] = skills[i];
|
||||||
|
}
|
||||||
|
col += 1;
|
||||||
|
pos = td_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MSDataSource {
|
impl MSDataSource {
|
||||||
|
@ -67,11 +159,14 @@ impl DataSource for MSDataSource {
|
||||||
.and_then(|mut resp| resp.body_mut().read_json())
|
.and_then(|mut resp| resp.body_mut().read_json())
|
||||||
{
|
{
|
||||||
Ok(champs) => champs,
|
Ok(champs) => champs,
|
||||||
Err(_) => return champs,
|
Err(e) => {
|
||||||
|
error!("Failed to fetch champions from MetaSRC: {}", e);
|
||||||
|
return champs;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for champ in champions {
|
for champ in champions {
|
||||||
if let Some(id) = get_champ_from_name(champion, champ.name.to_owned()) {
|
if let Some(id) = get_champ_from_name(champion, &champ.name) {
|
||||||
let allowed_roles = ["TOP", "ADC", "SUPPORT", "JUNGLE", "MID"];
|
let allowed_roles = ["TOP", "ADC", "SUPPORT", "JUNGLE", "MID"];
|
||||||
let roles = champ
|
let roles = champ
|
||||||
.search_terms
|
.search_terms
|
||||||
|
@ -79,9 +174,11 @@ impl DataSource for MSDataSource {
|
||||||
.map(|s| s.to_uppercase())
|
.map(|s| s.to_uppercase())
|
||||||
.filter(|role| allowed_roles.contains(&role.as_str()))
|
.filter(|role| allowed_roles.contains(&role.as_str()))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
champs.insert(id, roles);
|
if let Some(first_role) = roles.first() {
|
||||||
|
champs.insert(id, vec![first_role.clone()]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("Could not find champ {} in champion data", champ.name);
|
warn!("Could not find champ '{}' in champion data", champ.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,57 +206,48 @@ impl DataSource for MSDataSource {
|
||||||
if let Ok(mut p) = rep {
|
if let Ok(mut p) = rep {
|
||||||
let page = match p.body_mut().read_to_string() {
|
let page = match p.body_mut().read_to_string() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => return builds,
|
Err(e) => {
|
||||||
};
|
warn!("Failed to read page for champ {}: {}", champ.id, e);
|
||||||
|
return builds;
|
||||||
let mut pos = page.find("Patch ");
|
|
||||||
let patch = if let Some(p) = pos {
|
|
||||||
find_next_number(&page[p..]).to_string()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
pos = page.find("Win");
|
|
||||||
let win_rate: f32 = if let Some(p) = pos {
|
|
||||||
find_next_number(&page[p..])
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
pos = page.find("KDA:");
|
|
||||||
let kda: f32 = if let Some(p) = pos {
|
|
||||||
find_next_number(&page[p..])
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
pos = page.find("Games:");
|
|
||||||
let games: u32 = if let Some(p) = pos {
|
|
||||||
find_next_number(&page[p..]) as u32
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut items = vec![];
|
|
||||||
let mut pos: Option<usize> = page.find("/item/");
|
|
||||||
while let Some(mut p) = pos {
|
|
||||||
p += 6;
|
|
||||||
if let Some(dot_pos) = page[p..].find('.') {
|
|
||||||
let i = &page[p..p + dot_pos];
|
|
||||||
items.push(i);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let next = page[p..].find("/item/");
|
// Extract patch, win rate, kda, games
|
||||||
if let Some(n) = next {
|
let patch = page
|
||||||
pos = Some(p + n);
|
.find("Patch ")
|
||||||
} else {
|
.map(|p| find_next_number(&page[p..]).to_string())
|
||||||
pos = None;
|
.unwrap_or_default();
|
||||||
}
|
|
||||||
}
|
let win_rate = page
|
||||||
|
.find("Win")
|
||||||
|
.map(|p| find_next_number(&page[p..]))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
let kda = page
|
||||||
|
.find("KDA:")
|
||||||
|
.map(|p| find_next_number(&page[p..]))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
let games = page
|
||||||
|
.find("Games:")
|
||||||
|
.map(|p| find_next_number(&page[p..]) as u32)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let items = extract_items_from_section(&page, "Item Purchase Order");
|
||||||
|
let starting_items = extract_items_from_section(&page, "Starting Items");
|
||||||
|
|
||||||
builds.push((
|
builds.push((
|
||||||
positions[0].to_owned(),
|
positions[0].to_owned(),
|
||||||
vec![self.make_item_set(items, "Set".to_owned())],
|
vec![
|
||||||
|
self.make_item_set(
|
||||||
|
starting_items,
|
||||||
|
format!(
|
||||||
|
"Starting Items | skillOrder: {}",
|
||||||
|
extract_skill_order_from_table(&page)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
self.make_item_set(items, "Item Purchase Order".to_owned()),
|
||||||
|
],
|
||||||
Stat {
|
Stat {
|
||||||
win_rate,
|
win_rate,
|
||||||
games,
|
games,
|
||||||
|
@ -167,6 +255,11 @@ impl DataSource for MSDataSource {
|
||||||
patch,
|
patch,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Failed to fetch build page for champ {} at position {}",
|
||||||
|
champ.id, positions[0]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
builds
|
builds
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue