diff --git a/src/logic/character.rs b/src/logic/character.rs index 7ef7185..0b673e6 100644 --- a/src/logic/character.rs +++ b/src/logic/character.rs @@ -6,16 +6,21 @@ use crate::{ CityState, Gender, GrandCompanyInfo, + Job, + JobInfo, } }; use ffxiv_types::{World, Race, Clan, Guardian}; -use scraper::Html; +use scraper::{Html, ElementRef}; use url::Url; -use std::str::FromStr; +use std::{ + collections::BTreeMap, + str::FromStr, +}; selectors!( PROFILE_FACE => ".frame__chara__face > img"; @@ -30,6 +35,11 @@ selectors!( PROFILE_GRAND_COMPANY => "div.character-block:nth-of-type(4) > .character-block__box > .character-block__name"; PROFILE_FREE_COMPANY => ".character__freecompany__name > h4 > a"; PROFILE_TEXT => ".character__selfintroduction"; + + PROFILE_CLASS => "ul.character__job > li"; + CLASS_NAME => ".character__job__name"; + CLASS_LEVEL => ".character__job__level"; + CLASS_EXP => ".character__job__exp"; ); pub fn parse(id: u64, html: &str) -> Result { @@ -51,6 +61,8 @@ pub fn parse(id: u64, html: &str) -> Result { let profile_text = plain_parse(&html, &*PROFILE_TEXT)?.trim().to_string(); + let jobs = parse_jobs(&html)?; + let face = parse_face(&html)?; let portrait = parse_portrait(&html)?; @@ -68,6 +80,7 @@ pub fn parse(id: u64, html: &str) -> Result { grand_company, free_company_id, profile_text, + jobs, face, portrait, }) @@ -181,3 +194,50 @@ fn parse_portrait(html: &Html) -> Result { .ok_or_else(|| Error::invalid_content("img src attribute", None)) .and_then(|x| Url::parse(x).map_err(Error::InvalidUrl)) } + +fn parse_jobs(html: &Html) -> Result> { + let mut jobs = BTreeMap::new(); + + for job in html.select(&*PROFILE_CLASS) { + let (job, info) = parse_job(job)?; + jobs.insert(job, info); + } + + Ok(jobs) +} + +fn parse_job<'a>(elem: ElementRef<'a>) -> Result<(Job, JobInfo)> { + let job = crate::logic::plain_parse_elem(elem, &*CLASS_NAME) + .and_then(|x| Job::parse(&x).ok_or_else(|| Error::invalid_content("valid job", Some(&x))))?; + + let level_str = crate::logic::plain_parse_elem(elem, &*CLASS_LEVEL)?; + let level: Option = match level_str.as_str() { + "-" => None, + x => Some(x.parse().map_err(Error::InvalidNumber)?), + }; + + let exp_str = crate::logic::plain_parse_elem(elem, &*CLASS_EXP)?; + let mut exp_split = exp_str.split(" / "); + + let first_exp = exp_split.next().unwrap(); // must have first element + let experience: Option = match first_exp { + "-" | "--" => None, + x => Some(x.parse().map_err(Error::InvalidNumber)?), + }; + + let second_exp = exp_split + .next() + .ok_or_else(|| Error::invalid_content("experience split by ` / `", Some(&exp_str)))?; + let next_level_experience: Option = match second_exp { + "-" | "--" => None, + x => Some(x.parse().map_err(Error::InvalidNumber)?), + }; + + let info = JobInfo { + level, + experience, + next_level_experience, + }; + + Ok((job, info)) +} diff --git a/src/models/character.rs b/src/models/character.rs index 92c0c22..a57dceb 100644 --- a/src/models/character.rs +++ b/src/models/character.rs @@ -4,6 +4,8 @@ use ffxiv_types::{World, Race, Clan, Guardian}; use url::Url; +use std::collections::BTreeMap; + #[derive(Debug, Serialize, Deserialize)] pub struct Character { pub id: u64, @@ -24,6 +26,8 @@ pub struct Character { pub profile_text: String, + pub jobs: BTreeMap, + #[serde(with = "url_serde")] pub face: Url, #[serde(with = "url_serde")] @@ -36,6 +40,13 @@ pub struct GrandCompanyInfo { pub rank: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct JobInfo { + pub level: Option, + pub experience: Option, + pub next_level_experience: Option, +} + ffxiv_enum!(Gender { Male => "♂", Female => "♀", @@ -46,3 +57,45 @@ ffxiv_enum!(CityState { LimsaLominsa => "Limsa Lominsa", UlDah => "Ul'dah", }); + +ffxiv_enum!( + #[derive(PartialEq, Eq, PartialOrd, Ord)] + Job { + Gladiator => "Gladiator", + Paladin => "Paladin", + Marauder => "Marauder", + Warrior => "Warrior", + DarkKnight => "Dark Knight", + Conjurer => "Conjurer", + WhiteMage => "White Mage", + Scholar => "Scholar", + Astrologian => "Astrologian", + Pugilist => "Pugilist", + Monk => "Monk", + Lancer => "Lancer", + Dragoon => "Dragoon", + Rogue => "Rogue", + Ninja => "Ninja", + Samurai => "Samurai", + Archer => "Archer", + Bard => "Bard", + Machinist => "Machinist", + Thaumaturge => "Thaumaturge", + BlackMage => "Black Mage", + Arcanist => "Arcanist", + Summoner => "Summoner", + RedMage => "Red Mage", + + Carpenter => "Carpenter", + Blacksmith => "Blacksmith", + Armorer => "Armorer", + Goldsmith => "Goldsmith", + Leatherworker => "Leatherworker", + Weaver => "Weaver", + Alchemist => "Alchemist", + Culinarian => "Culinarian", + Miner => "Miner", + Botanist => "Botanist", + Fisher => "Fisher", + } +);