use crate::{ error::*, logic::plain_parse_elem as plain_parse, models::{ GrandCompany, search::{ Paginated, free_company::{FreeCompanySearchItem, Active, RecruitmentStatus}, }, }, }; use chrono::{DateTime, Local, TimeZone, Utc}; use ffxiv_types::World; use scraper::{Html, ElementRef}; use url::Url; use std::str::FromStr; selectors!( ITEM_ENTRY => ".ldst__window > .entry"; ITEM_ID => ".entry__block"; ITEM_GRAND_COMPANY => ".entry__freecompany__box > p.entry__world:nth-of-type(1)"; ITEM_NAME => ".entry__freecompany__box > .entry__name"; ITEM_WORLD => ".entry__freecompany__box > p.entry__world:nth-of-type(3)"; ITEM_CREST => ".entry__freecompany__crest__image > img"; ITEM_ACTIVE_MEMBERS => ".entry__freecompany__fc-member"; ITEM_ESTATE_BUILT => ".entry__freecompany__fc-housing"; ITEM_FORMED => ".entry__freecompany__fc-day > script"; ITEM_ACTIVE => ".entry__freecompany__fc-data > li.entry__freecompany__fc-active:nth-of-type(4)"; ITEM_RECRUITMENT => ".entry__freecompany__fc-data > li.entry__freecompany__fc-active:nth-of-type(5)"; ); pub fn parse(s: &str) -> Result> { let html = Html::parse_document(s); let pagination = crate::logic::search::parse_pagination(&html)?; // has results but requested an invalid page if pagination.total_results != 0 && pagination.current_page == 0 { return Err(Error::InvalidPage(pagination.total_pages)); } let results: Vec = html .select(&*ITEM_ENTRY) .map(parse_single) .collect::>()?; Ok(Paginated { pagination, results, }) } fn parse_single(html: ElementRef) -> Result { let id = parse_id(html)?; let grand_company = parse_grand_company(html)?; let name = plain_parse(html, &*ITEM_NAME)?; let world = parse_world(html)?; let crest = parse_crest(html)?; let active_members = parse_active_members(html)?; let estate_built = parse_estate_built(html)?; let formed = parse_formed(html)?; let active = parse_active(html)?; let recruitment = parse_recruitment(html)?; Ok(FreeCompanySearchItem { id, grand_company, name, world, crest, active_members, estate_built, formed, active, recruitment, }) } fn parse_id(html: ElementRef) -> Result { let e = html .select(&*ITEM_ID) .next() .ok_or_else(|| Error::missing_element(&*ITEM_ID))?; crate::logic::parse_id(e.value()) } fn parse_grand_company(html: ElementRef) -> Result { let gc_str = plain_parse(html, &*ITEM_GRAND_COMPANY)?; GrandCompany::parse(&gc_str) .ok_or_else(|| Error::invalid_content("valid grand company", Some(&gc_str))) } fn parse_world(html: ElementRef) -> Result { let parts_str = plain_parse(html, &*ITEM_WORLD)?; let mut parts = parts_str.split(" ["); let world_str = parts.next() .ok_or_else(|| Error::invalid_content("world with data centre in parens", Some(&parts_str)))?; World::from_str(world_str) .map_err(|_| Error::invalid_content("valid world", Some(&world_str))) } fn parse_crest(html: ElementRef) -> Result> { html.select(&*ITEM_CREST) .filter_map(|x| x.value().attr("src")) .map(|x| Url::from_str(x).map_err(Error::InvalidUrl)) .collect() } fn parse_active_members(html: ElementRef) -> Result { plain_parse(html, &*ITEM_ACTIVE_MEMBERS) .and_then(|x| x.parse().map_err(Error::InvalidNumber)) } fn parse_estate_built(html: ElementRef) -> Result { let estate_built = plain_parse(html, &*ITEM_ESTATE_BUILT)?; let built = match estate_built.as_str() { "Estate Built" => true, "No Estate or Plot" => false, "Plot Only" => false, // FIXME: use enum for this _ => return Err(Error::invalid_content("`Estate Built` or `No Estate or Plot`", Some(&estate_built))), }; Ok(built) } fn parse_formed(html: ElementRef) -> Result> { let script = html .select(&*ITEM_FORMED) .next() .ok_or_else(|| Error::missing_element(&*ITEM_FORMED))? .inner_html(); let timestamp = script .split("strftime(") .nth(1) .ok_or_else(|| Error::invalid_content("strftime call", Some(&script)))? .split(',') .next() .ok_or_else(|| Error::invalid_content("comma-separated strftime call", Some(&script)))?; let timestamp: i64 = timestamp.parse().map_err(Error::InvalidNumber)?; let utc = Local.timestamp(timestamp, 0).with_timezone(&Utc); Ok(utc) } fn parse_active(html: ElementRef) -> Result { plain_parse(html, &*ITEM_ACTIVE) .and_then(|x| x .split(": ") .nth(1) .map(ToString::to_string) .ok_or_else(|| Error::invalid_content("activity split by `: `", Some(&x)))) .and_then(|x| Active::parse(&x) .ok_or_else(|| Error::invalid_content("valid activity", Some(&x)))) } fn parse_recruitment(html: ElementRef) -> Result { plain_parse(html, &*ITEM_RECRUITMENT) .and_then(|x| x .split(": ") .nth(1) .map(ToString::to_string) .ok_or_else(|| Error::invalid_content("recruitment status split by `: `", Some(&x)))) .and_then(|x| RecruitmentStatus::parse(&x) .ok_or_else(|| Error::invalid_content("valid recruitment status", Some(&x)))) }