use super::{ElementExt, Parser}; use crate::data::{Class, GameMode, Honors, Player, TeamMemberShip, TeamRef}; use crate::parser::{select_last_text, select_text, team_id_from_link, DATE_FORMAT}; use crate::{ParseError, Result}; use scraper::{Html, Selector}; use std::iter::repeat; use std::str::FromStr; use steamid_ng::SteamID; use time::Date; const SELECTOR_PLAYER_NAME: &str = ".container .col-md-4 > h3 > b"; const SELECTOR_PLAYER_ID: &str = r#"a[href*="steam://friends/add"]"#; const SELECTOR_PLAYER_FLAG: &str = r#"img[data-cfsrc*="/images/flags/"]"#; const SELECTOR_PLAYER_HONORS_GROUP: &str = ".container .col-md-6:nth-child(2) .white-row-small .row-fluid"; const SELECTOR_PLAYER_HONORS_HEADER: &str = "h5"; const SELECTOR_PLAYER_HONORS_LEAGUE: &str = "li div"; const SELECTOR_PLAYER_HONORS_TEAM: &str = "li small a"; const SELECTOR_PLAYER_TEAM_GROUP: &str = ".container .col-md-6:nth-child(1) .white-row-small .row-fluid"; const SELECTOR_PLAYER_TEAM_LINK: &str = "p a"; const SELECTOR_PLAYER_TEAM_NAME: &str = "span.text-primary b"; const SELECTOR_PLAYER_TEAM_LEAGUE: &str = "small"; const SELECTOR_PLAYER_TEAM_SINCE: &str = "small"; const SELECTOR_AVATAR: &str = r#"a[href*="https://www.ugcleague.com/players_page.cfm?player_id="] img.img-responsive"#; const SELECTOR_CLASS: &str = r#"img.img-rounded[src*="images/tf2/icon/"], img.img-rounded[data-cfsrc*="images/tf2/icon/"]"#; pub struct PlayerParser { selector_name: Selector, selector_id: Selector, selector_flag: Selector, selector_honors_header: Selector, selector_honors_group: Selector, selector_honors_league: Selector, selector_honors_team: Selector, selector_team_group: Selector, selector_team_link: Selector, selector_team_name: Selector, selector_team_league: Selector, selector_team_since: Selector, selector_avatar: Selector, selector_class: Selector, } impl Default for PlayerParser { fn default() -> Self { Self::new() } } impl PlayerParser { pub fn new() -> Self { PlayerParser { selector_name: Selector::parse(SELECTOR_PLAYER_NAME).unwrap(), selector_id: Selector::parse(SELECTOR_PLAYER_ID).unwrap(), selector_flag: Selector::parse(SELECTOR_PLAYER_FLAG).unwrap(), selector_honors_header: Selector::parse(SELECTOR_PLAYER_HONORS_HEADER).unwrap(), selector_honors_group: Selector::parse(SELECTOR_PLAYER_HONORS_GROUP).unwrap(), selector_honors_league: Selector::parse(SELECTOR_PLAYER_HONORS_LEAGUE).unwrap(), selector_honors_team: Selector::parse(SELECTOR_PLAYER_HONORS_TEAM).unwrap(), selector_team_group: Selector::parse(SELECTOR_PLAYER_TEAM_GROUP).unwrap(), selector_team_link: Selector::parse(SELECTOR_PLAYER_TEAM_LINK).unwrap(), selector_team_name: Selector::parse(SELECTOR_PLAYER_TEAM_NAME).unwrap(), selector_team_league: Selector::parse(SELECTOR_PLAYER_TEAM_LEAGUE).unwrap(), selector_team_since: Selector::parse(SELECTOR_PLAYER_TEAM_SINCE).unwrap(), selector_avatar: Selector::parse(SELECTOR_AVATAR).unwrap(), selector_class: Selector::parse(SELECTOR_CLASS).unwrap(), } } } impl Parser for PlayerParser { type Output = Player; fn parse(&self, document: &str) -> Result { let document = Html::parse_document(document); let name = document .select(&self.selector_name) .next() .ok_or(ParseError::ElementNotFound { selector: SELECTOR_PLAYER_NAME, role: "player name", })? .first_text() .unwrap_or_default() .to_string(); let id = document .select(&self.selector_id) .next() .ok_or(ParseError::ElementNotFound { selector: SELECTOR_PLAYER_ID, role: "player steam id", })? .attr("href") .unwrap_or_default() .strip_prefix("steam://friends/add/") .unwrap_or_default() .to_string(); let country = document .select(&self.selector_flag) .next() .and_then(|e| e.attr("title")) .map(String::from); let avatar_element = document .select(&self.selector_avatar) .next() .ok_or(ParseError::ElementNotFound { selector: SELECTOR_AVATAR, role: "player avatar", })?; let avatar = avatar_element .attr("src") .or_else(|| avatar_element.attr("data-cfsrc")) .unwrap_or_default() .to_string(); let honors = document .select(&self.selector_honors_group) .flat_map(|group| { let format = select_text(group, &self.selector_honors_header) .ok_or(ParseError::ElementNotFound { selector: SELECTOR_PLAYER_HONORS_HEADER, role: "player honors format", }) .map(|format| format.trim_end_matches(" Medals")); let leagues = group.select(&self.selector_honors_league); let teams = group.select(&self.selector_honors_team); repeat(format).zip(leagues).zip(teams) }) .map(|((format_res, season), team)| { let format = format_res?; let game_mode = GameMode::from_str(format).map_err(|_| ParseError::InvalidText { text: format.into(), role: "player honors format", })?; let division = season.first_text().ok_or(ParseError::EmptyText { selector: SELECTOR_PLAYER_HONORS_LEAGUE, role: "player honors division", })?; let season = division .split(' ') .nth(1) .unwrap_or_default() .parse() .map_err(|_| ParseError::InvalidText { text: division.into(), role: "player honors season", })?; let team_name = team.first_text().unwrap_or_default().to_string(); let team_link = team.attr("href").unwrap_or_default(); let team_id: u32 = team_link .rsplit('=') .next() .unwrap_or_default() .parse() .map_err(|_| ParseError::InvalidLink { link: team_link.into(), role: "player honors team", })?; Ok(Honors { format: game_mode, division: division.splitn(3, ' ').last().unwrap_or_default().into(), season, team: TeamRef { id: team_id, name: team_name, }, }) }) .collect::>>()?; let favorite_classes = document .select(&self.selector_class) .filter_map(|class| class.attr("src").or_else(|| class.attr("data-cfsrc"))) .filter_map(|img| { img.strip_prefix("images/tf2/icon/") .and_then(|class| class.strip_suffix(".jpg")) }) .filter_map(|class| Class::from_str(class).ok()) .collect::>(); let teams = document .select(&self.selector_team_group) .filter(|item| item.select(&self.selector_team_link).next().is_some()) .map(move |item| { let link = item .select(&self.selector_team_link) .next() .ok_or(ParseError::ElementNotFound { selector: SELECTOR_PLAYER_TEAM_LINK, role: "players team link", })? .attr("href") .unwrap_or_default(); let name = select_text(item, &self.selector_team_name).unwrap_or_default(); let league = select_text(item, &self.selector_team_league).ok_or( ParseError::ElementNotFound { selector: SELECTOR_PLAYER_TEAM_LEAGUE, role: "players team league", }, )?; let since = select_last_text(item, &self.selector_team_since).ok_or( ParseError::ElementNotFound { selector: SELECTOR_PLAYER_TEAM_SINCE, role: "players team joined", }, )?; let id = team_id_from_link(link)?; let since = match since.rsplit_once(['\n', ' ', '\t']) { Some((_, since)) => { let since = since.trim(); Date::parse(since, DATE_FORMAT).map_err(|_| ParseError::InvalidDate { role: "team join date", date: since.to_string(), })? } _ => { return Err(ParseError::InvalidDate { role: "team join date", date: since.to_string(), } .into()) } }; Ok(TeamMemberShip { team: TeamRef { name: name.to_string(), id, }, league: league.to_string(), since, }) }) .collect::>>()?; Ok(Player { name, avatar, steam_id: SteamID::from_str(id.as_str()) .unwrap_or_else(|_| SteamID::from_steam64(0).unwrap()), honors, teams, favorite_classes, country, }) } }