CGGItemSets/src/kb_data_source.rs
nyyu cabde1d433
All checks were successful
ci/woodpecker/push/linux Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/mingw Pipeline was successful
refactor: enhance data handling in data sources
2025-06-29 09:31:45 +02:00

346 lines
9.5 KiB
Rust

use crate::ChampInfo;
use crate::Champion as ChampionLoL;
use crate::data_source::{Data, DataSource, Item, Stat};
use indexmap::IndexMap;
use log::error;
use serde_derive::Deserialize;
pub struct KBDataSource {
client: ureq::Agent,
token: Option<String>,
}
#[derive(Deserialize)]
struct ChampionResponse {
//patches: Vec<Patch>,
champions: Vec<Champion>,
}
#[derive(Deserialize)]
struct Patch {
//enabled: bool,
#[serde(rename = "patchVersion")]
patch_version: String,
//patchid: u32,
//start: String,
}
#[derive(Deserialize)]
struct Champion {
id: u32,
//name: String,
//#[serde(rename = "className")]
//classname: String,
builds: Option<Position>,
}
#[derive(Deserialize)]
struct Position {
bot: u16,
jungle: u16,
mid: u16,
support: u16,
top: u16,
}
#[derive(Deserialize)]
struct BuildResponse {
builds3: Vec<KBBuild>,
}
#[derive(Deserialize)]
struct KBBuild {
position: PositionResponse,
#[serde(rename = "itemSets")]
item_sets: Vec<ItemSets>,
#[serde(rename = "strItemSets")]
str_item_sets: Vec<StrItemSets>,
#[serde(rename = "skillSets")]
skill_sets: Vec<SkillSet>,
wins: u32,
games: u32,
kda: f32,
summoner: Summoner,
patch: Patch,
}
#[derive(Deserialize)]
struct PositionResponse {
#[serde(rename = "positionName")]
position_name: String,
}
#[derive(Deserialize)]
struct SkillSet {
#[serde(rename = "skillOrder")]
skill_order: String,
}
#[derive(Deserialize)]
struct ItemSets {
item0: KBItem,
item1: KBItem,
item2: KBItem,
item3: KBItem,
item4: KBItem,
item5: KBItem,
}
#[derive(Deserialize)]
struct StrItemSets {
#[serde(rename = "strItem0")]
str_item0: KBItem,
#[serde(rename = "strItem1")]
str_item1: KBItem,
#[serde(rename = "strItem2")]
str_item2: KBItem,
#[serde(rename = "strItem3")]
str_item3: KBItem,
#[serde(rename = "strItem4")]
str_item4: KBItem,
#[serde(rename = "strItem5")]
str_item5: KBItem,
}
#[derive(Deserialize)]
struct KBItem {
#[serde(rename = "itemId")]
item_id: u32,
//name: String,
}
#[derive(Deserialize)]
struct Summoner {
name: String,
}
impl KBDataSource {
pub fn new(client: &ureq::Agent) -> Self {
Self {
client: client.clone(),
token: Self::fetch_auth_token(client),
}
}
fn fetch_auth_token(client: &ureq::Agent) -> Option<String> {
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<String> {
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_owned())
}
fn get_champion_response(&self) -> Option<ChampionResponse> {
if let Some(token) = &self.token {
return match self
.client
.get("https://api.koreanbuilds.net/champions?patchid=-1")
.header("Accept", "application/json")
.header("Authorization", token.as_str())
.call()
{
Ok(mut resp) => resp.body_mut().read_json().unwrap_or_default(),
Err(_) => None,
};
}
None
}
fn get_positions(position: Option<Position>) -> Vec<String> {
let positions = vec!["TOP", "JUNGLE", "MID", "BOT", "SUPPORT"];
if let Some(pos) = position {
positions
.into_iter()
.zip([pos.top, pos.jungle, pos.mid, pos.bot, pos.support])
.filter_map(|(name, count)| {
if count > 0 {
Some(name.to_owned())
} else {
None
}
})
.collect()
} else {
positions.into_iter().map(|s| s.to_owned()).collect()
}
}
fn get_build(&self, build: &KBBuild) -> Data {
let mut starting_items: Vec<Item> = vec![];
let mut blocks = vec![];
for i in 0..6 {
let item_id = match i {
0 => build.str_item_sets[0].str_item0.item_id,
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,
});
}
}
blocks.push(self.make_item_set(
starting_items.iter().map(|item| item.id.clone()).collect(),
format!(
"Early game items | skillOrder : {}",
build.skill_sets[0].skill_order
),
));
let mut final_items: Vec<Item> = vec![];
for item in [
&build.item_sets[0].item0,
&build.item_sets[0].item1,
&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,
});
}
}
blocks.push(self.make_item_set(
final_items.iter().map(|item| item.id.clone()).collect(),
format!(
"Item order by time finished | Summoner : {}",
build.summoner.name
),
));
Data {
position: build.position.position_name.to_uppercase(),
items: blocks,
stat: Stat {
win_rate: (build.wins as f32 / build.games as f32) * 100f32,
games: build.games,
kda: build.kda,
patch: build.patch.patch_version.to_owned(),
},
}
}
}
impl DataSource for KBDataSource {
fn get_alias(&self) -> &str {
"KB"
}
fn get_champs_with_positions(&self, _champion: &ChampionLoL) -> IndexMap<u32, Vec<String>> {
let mut champions = IndexMap::new();
let data: ChampionResponse = match self.get_champion_response() {
Some(val) => val,
None => {
return champions;
}
};
for champ in data.champions {
champions.insert(champ.id, KBDataSource::get_positions(champ.builds));
}
champions
}
fn get_champ_data_with_win_pourcentage(
&self,
champ: &ChampInfo,
position: &[String],
) -> Vec<Data> {
let mut champ_data = vec![];
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
.client
.get(&url)
.header("Accept", "application/json")
.header("Authorization", token.as_str())
.call()
{
Ok(mut resp) => match resp.body_mut().read_json() {
Ok(val) => val,
Err(x) => {
error!("Cant json: {x}");
return vec![];
}
},
Err(x) => {
error!("Call failed for URL: {url}, error: {x}");
return vec![];
}
};
for pos in position {
for b in &data.builds3 {
if b.position.position_name.to_uppercase() == *pos {
champ_data.push(self.get_build(b));
break;
}
}
}
}
champ_data
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::create_http_client;
use std::sync::LazyLock;
static DATASOURCE: LazyLock<KBDataSource> =
LazyLock::new(|| KBDataSource::new(&create_http_client()));
#[test]
fn test_get_auth_token() {
match &DATASOURCE.token {
Some(token) => assert!(token.len() > 0),
None => assert!(false),
};
}
#[test]
fn test_get_champs_with_positions_and_patch() {
let champion = ChampionLoL {
data: IndexMap::new(),
};
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 champ = ChampInfo {
id: String::from("Annie"),
name: String::from("Annie"),
key: 1,
};
let result =
DATASOURCE.get_champ_data_with_win_pourcentage(&champ, &vec!["MID".to_owned()]);
assert!(!result.is_empty());
assert!(!result[0].items.is_empty());
assert!(result[0].stat.win_rate > 0.);
}
}