2018-09-02 19:12:52 +00:00
|
|
|
use crate::{
|
|
|
|
error::*,
|
2018-09-03 19:32:17 +00:00
|
|
|
logic::plain_parse,
|
|
|
|
models::GrandCompany,
|
|
|
|
models::free_company::{FreeCompany, PvpRankings, Estate},
|
2018-09-02 19:12:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
use chrono::{DateTime, Local, TimeZone, Utc};
|
|
|
|
|
|
|
|
use ffxiv_types::World;
|
|
|
|
|
|
|
|
use scraper::Html;
|
|
|
|
|
|
|
|
use url::Url;
|
|
|
|
|
2018-09-03 18:41:01 +00:00
|
|
|
use std::{
|
|
|
|
collections::BTreeMap,
|
|
|
|
str::FromStr,
|
|
|
|
};
|
2018-09-02 19:12:52 +00:00
|
|
|
|
|
|
|
selectors!(
|
2018-09-03 18:41:01 +00:00
|
|
|
FC_GRAND_COMPANY => "p.entry__freecompany__gc:nth-of-type(1)";
|
2018-09-02 19:12:52 +00:00
|
|
|
FC_NAME => ".entry__freecompany__name";
|
|
|
|
FC_WORLD => "p.entry__freecompany__gc:nth-of-type(3)";
|
|
|
|
FC_SLOGAN => ".freecompany__text__message.freecompany__text";
|
|
|
|
FC_TAG => ".freecompany__text__tag.freecompany__text";
|
|
|
|
FC_CREST => ".entry__freecompany__crest__image > img";
|
|
|
|
FC_FORMED => "p.freecompany__text:nth-of-type(5) > script";
|
|
|
|
FC_ACTIVE_MEMBERS => "p.freecompany__text:nth-of-type(6)";
|
|
|
|
FC_RANK => "p.freecompany__text:nth-of-type(7)";
|
2018-09-03 18:41:01 +00:00
|
|
|
|
|
|
|
FC_REPUTATION => ".freecompany__reputation__data";
|
|
|
|
FC_REPUTATION_NAME => ".freecompany__reputation__gcname";
|
|
|
|
FC_REPUTATION_RANK => ".freecompany__reputation__rank";
|
|
|
|
|
2018-09-02 19:12:52 +00:00
|
|
|
FC_WEEKLY_RANKING => ".character__ranking__data tr:nth-of-type(1) > th";
|
|
|
|
FC_MONTHLY_RANKING => ".character__ranking__data tr:nth-of-type(2) > th";
|
|
|
|
|
|
|
|
FC_ESTATE_MISSING => ".freecompany__estate__none";
|
|
|
|
FC_ESTATE_NAME => ".freecompany__estate__name";
|
|
|
|
FC_ESTATE_ADDRESS => ".freecompany__estate__text";
|
|
|
|
FC_ESTATE_GREETING => ".freecompany__estate__greeting";
|
|
|
|
);
|
|
|
|
|
|
|
|
pub fn parse(id: u64, html: &str) -> Result<FreeCompany> {
|
|
|
|
let html = Html::parse_document(html);
|
|
|
|
|
2018-09-03 18:41:01 +00:00
|
|
|
let grand_company = parse_grand_company(&html)?;
|
2018-09-02 19:12:52 +00:00
|
|
|
let name = plain_parse(&html, &*FC_NAME)?;
|
|
|
|
let world = parse_world(&html)?;
|
|
|
|
let slogan = plain_parse(&html, &*FC_SLOGAN)?;
|
|
|
|
let crest = parse_crest(&html)?;
|
|
|
|
let active_members = parse_active_members(&html)?;
|
|
|
|
let rank = parse_rank(&html)?;
|
|
|
|
let pvp_rankings = PvpRankings {
|
|
|
|
weekly: parse_pvp_rank(&html, &*FC_WEEKLY_RANKING)?,
|
|
|
|
monthly: parse_pvp_rank(&html, &*FC_MONTHLY_RANKING)?,
|
|
|
|
};
|
|
|
|
let formed = parse_formed(&html)?;
|
|
|
|
let estate = parse_estate(&html)?;
|
2018-09-03 18:41:01 +00:00
|
|
|
let reputation = parse_reputation(&html)?;
|
2018-09-02 19:12:52 +00:00
|
|
|
|
|
|
|
Ok(FreeCompany {
|
2018-09-03 19:32:17 +00:00
|
|
|
id,
|
2018-09-02 19:12:52 +00:00
|
|
|
name,
|
|
|
|
world,
|
|
|
|
slogan,
|
|
|
|
crest,
|
2018-09-03 18:41:01 +00:00
|
|
|
grand_company,
|
2018-09-02 19:12:52 +00:00
|
|
|
active_members,
|
|
|
|
rank,
|
|
|
|
pvp_rankings,
|
|
|
|
formed,
|
|
|
|
estate,
|
2018-09-03 18:41:01 +00:00
|
|
|
reputation,
|
2018-09-02 19:12:52 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_world(html: &Html) -> Result<World> {
|
2019-04-24 23:26:38 +00:00
|
|
|
let parts_str = plain_parse(html, &*FC_WORLD)?;
|
2022-06-16 14:10:03 +00:00
|
|
|
let mut parts = parts_str.split(" [");
|
2019-04-24 23:26:38 +00:00
|
|
|
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.trim())
|
|
|
|
.map_err(|_| Error::invalid_content("valid world", Some(&world_str)))
|
2018-09-02 19:12:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_active_members(html: &Html) -> Result<u16> {
|
|
|
|
plain_parse(&html, &*FC_ACTIVE_MEMBERS)
|
|
|
|
.and_then(|x| x.parse().map_err(Error::InvalidNumber))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_rank(html: &Html) -> Result<u8> {
|
|
|
|
plain_parse(&html, &*FC_RANK)
|
|
|
|
.and_then(|x| x.parse().map_err(Error::InvalidNumber))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_pvp_rank(html: &Html, select: &scraper::Selector) -> Result<Option<u64>> {
|
|
|
|
let rank_str = plain_parse(html, select)?;
|
|
|
|
|
|
|
|
let rank = rank_str
|
2019-04-04 22:58:06 +00:00
|
|
|
.split(':')
|
2018-09-02 19:12:52 +00:00
|
|
|
.nth(1)
|
|
|
|
.ok_or_else(|| Error::invalid_content("colon-separated text", Some(&rank_str)))
|
|
|
|
.and_then(|x| x
|
2019-04-04 22:58:06 +00:00
|
|
|
.split(' ')
|
2018-09-02 19:12:52 +00:00
|
|
|
.next()
|
|
|
|
.ok_or_else(|| Error::invalid_content("space-separated text", Some(&rank_str))))?;
|
|
|
|
|
|
|
|
if rank == "--" {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
rank
|
|
|
|
.parse()
|
|
|
|
.map(Some)
|
|
|
|
.map_err(Error::InvalidNumber)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_formed(html: &Html) -> Result<DateTime<Utc>> {
|
|
|
|
let script = html
|
|
|
|
.select(&*FC_FORMED)
|
|
|
|
.next()
|
2019-04-04 22:58:06 +00:00
|
|
|
.ok_or_else(|| Error::missing_element(&*FC_FORMED))?
|
2018-09-02 19:12:52 +00:00
|
|
|
.inner_html();
|
|
|
|
|
|
|
|
let timestamp = script
|
|
|
|
.split("strftime(")
|
|
|
|
.nth(1)
|
2019-04-04 22:58:06 +00:00
|
|
|
.ok_or_else(|| Error::invalid_content("strftime call", Some(&script)))?
|
|
|
|
.split(',')
|
2018-09-02 19:12:52 +00:00
|
|
|
.next()
|
2019-04-04 22:58:06 +00:00
|
|
|
.ok_or_else(|| Error::invalid_content("comma-separated strftime call", Some(&script)))?;
|
2018-09-02 19:12:52 +00:00
|
|
|
let timestamp: i64 = timestamp.parse().map_err(Error::InvalidNumber)?;
|
|
|
|
|
|
|
|
let utc = Local.timestamp(timestamp, 0).with_timezone(&Utc);
|
|
|
|
|
|
|
|
Ok(utc)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_estate(html: &Html) -> Result<Option<Estate>> {
|
|
|
|
if html.select(&*FC_ESTATE_MISSING).next().is_some() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
let name = plain_parse(html, &*FC_ESTATE_NAME)?;
|
|
|
|
let address = plain_parse(html, &*FC_ESTATE_ADDRESS)?;
|
|
|
|
let greeting = plain_parse(html, &*FC_ESTATE_GREETING)?;
|
|
|
|
|
|
|
|
Ok(Some(Estate {
|
|
|
|
name,
|
|
|
|
address,
|
|
|
|
greeting,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_crest(html: &Html) -> Result<Vec<Url>> {
|
|
|
|
html.select(&*FC_CREST)
|
|
|
|
.filter_map(|x| x.value().attr("src"))
|
|
|
|
.map(|x| Url::parse(x).map_err(Error::InvalidUrl))
|
|
|
|
.collect()
|
|
|
|
}
|
2018-09-03 18:41:01 +00:00
|
|
|
|
|
|
|
fn parse_grand_company(html: &Html) -> Result<GrandCompany> {
|
|
|
|
let text = plain_parse(html, &*FC_GRAND_COMPANY)?;
|
|
|
|
let name = text
|
|
|
|
.split(" <")
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| Error::invalid_content("grand company and reputation", Some(&text)))?;
|
|
|
|
GrandCompany::parse(&name)
|
|
|
|
.ok_or_else(|| Error::invalid_content("valid grand company", Some(&name)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_reputation(html: &Html) -> Result<BTreeMap<GrandCompany, u8>> {
|
|
|
|
let mut reps = BTreeMap::new();
|
|
|
|
|
2019-04-04 22:58:06 +00:00
|
|
|
for elem in html.select(&*FC_REPUTATION) {
|
2018-09-03 18:41:01 +00:00
|
|
|
let name: String = elem
|
|
|
|
.select(&*FC_REPUTATION_NAME)
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| Error::missing_element(&*FC_REPUTATION_NAME))?
|
|
|
|
.text()
|
|
|
|
.collect();
|
|
|
|
let gc = GrandCompany::parse(&name)
|
|
|
|
.ok_or_else(|| Error::invalid_content("valid grand company", Some(&name)))?;
|
|
|
|
let rank_elem = elem
|
|
|
|
.select(&*FC_REPUTATION_RANK)
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| Error::missing_element(&*FC_REPUTATION_RANK))?;
|
|
|
|
let color_class = rank_elem
|
|
|
|
.value()
|
|
|
|
.classes()
|
|
|
|
.find(|x| x.starts_with("color_"))
|
|
|
|
.ok_or_else(|| Error::invalid_content("color_## class", None))?;
|
|
|
|
let rank: u8 = color_class
|
|
|
|
.split("color_")
|
|
|
|
.nth(1)
|
|
|
|
.ok_or_else(|| Error::invalid_content("color_## class", Some(&color_class)))
|
|
|
|
.and_then(|x| x.parse().map_err(Error::InvalidNumber))?;
|
|
|
|
reps.insert(gc, rank);
|
|
|
|
}
|
|
|
|
|
|
|
|
if reps.len() != 3 {
|
|
|
|
return Err(Error::invalid_content("three grand companies with reputation", None));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(reps)
|
|
|
|
}
|