feat: add character search
This commit is contained in:
parent
8e358c80d6
commit
480ebe983e
41
src/logic.rs
41
src/logic.rs
@ -1,6 +1,15 @@
|
|||||||
use crate::error::*;
|
use crate::{
|
||||||
|
error::*,
|
||||||
|
models::{
|
||||||
|
GrandCompany,
|
||||||
|
character::GrandCompanyInfo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use scraper::Html;
|
use scraper::{
|
||||||
|
Html,
|
||||||
|
node::Element,
|
||||||
|
};
|
||||||
|
|
||||||
macro_rules! selectors {
|
macro_rules! selectors {
|
||||||
($($name:ident => $selector:expr);+$(;)?) => {
|
($($name:ident => $selector:expr);+$(;)?) => {
|
||||||
@ -14,6 +23,7 @@ macro_rules! selectors {
|
|||||||
|
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod free_company;
|
pub mod free_company;
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String> {
|
crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String> {
|
||||||
let string = html
|
let string = html
|
||||||
@ -24,3 +34,30 @@ crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String>
|
|||||||
.collect();
|
.collect();
|
||||||
Ok(string)
|
Ok(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crate fn parse_id<'a>(a: &'a Element) -> Result<u64> {
|
||||||
|
let href = a.attr("href").ok_or_else(|| Error::invalid_content("href on link", None))?;
|
||||||
|
let last = href
|
||||||
|
.split('/')
|
||||||
|
.filter(|x| !x.is_empty())
|
||||||
|
.last()
|
||||||
|
.ok_or_else(|| Error::invalid_content("href separated by `/`", Some(&href)))?;
|
||||||
|
last.parse().map_err(Error::InvalidNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
crate fn parse_grand_company(text: &str) -> Result<GrandCompanyInfo> {
|
||||||
|
let mut x = text.split(" / ");
|
||||||
|
let gc_str = x
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::invalid_content("gc/rank separated by `/`", Some(&text)))?;
|
||||||
|
let grand_company = GrandCompany::parse(gc_str)
|
||||||
|
.ok_or_else(|| Error::invalid_content("valid grand company", Some(&text)))?;
|
||||||
|
let rank = x
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::invalid_content("gc/rank separated by `/`", Some(&text)))?
|
||||||
|
.to_string();
|
||||||
|
Ok(GrandCompanyInfo {
|
||||||
|
grand_company,
|
||||||
|
rank,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
error::*,
|
error::*,
|
||||||
logic::plain_parse,
|
logic::plain_parse,
|
||||||
models::{
|
models::character::{
|
||||||
GrandCompany,
|
|
||||||
character::{
|
|
||||||
Character,
|
Character,
|
||||||
CityState,
|
CityState,
|
||||||
Gender,
|
Gender,
|
||||||
GrandCompanyInfo,
|
GrandCompanyInfo,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -138,20 +135,7 @@ fn parse_grand_company(html: &Html) -> Result<Option<GrandCompanyInfo>> {
|
|||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
let mut x = text.split(" / ");
|
crate::logic::parse_grand_company(&text).map(Some)
|
||||||
let gc_str = x
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| Error::invalid_content("gc/rank separated by `/`", Some(&text)))?;
|
|
||||||
let grand_company = GrandCompany::parse(gc_str)
|
|
||||||
.ok_or_else(|| Error::invalid_content("valid grand company", Some(&text)))?;
|
|
||||||
let rank = x
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| Error::invalid_content("gc/rank separated by `/`", Some(&text)))?
|
|
||||||
.to_string();
|
|
||||||
Ok(Some(GrandCompanyInfo {
|
|
||||||
grand_company,
|
|
||||||
rank,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_free_company_id(html: &Html) -> Result<Option<u64>> {
|
fn parse_free_company_id(html: &Html) -> Result<Option<u64>> {
|
||||||
@ -162,11 +146,5 @@ fn parse_free_company_id(html: &Html) -> Result<Option<u64>> {
|
|||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
let href = elem.value().attr("href").ok_or_else(|| Error::invalid_content("href on FC link", None))?;
|
crate::logic::parse_id(elem.value()).map(Some)
|
||||||
let last = href
|
|
||||||
.split('/')
|
|
||||||
.filter(|x| !x.is_empty())
|
|
||||||
.last()
|
|
||||||
.ok_or_else(|| Error::invalid_content("href separated by `/`", Some(&href)))?;
|
|
||||||
last.parse().map(Some).map_err(Error::InvalidNumber)
|
|
||||||
}
|
}
|
||||||
|
44
src/logic/search.rs
Normal file
44
src/logic/search.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use crate::{
|
||||||
|
error::*,
|
||||||
|
models::search::Pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
use scraper::Html;
|
||||||
|
|
||||||
|
pub mod character;
|
||||||
|
|
||||||
|
selectors!(
|
||||||
|
PAGINATION_TOTAL => ".parts__total";
|
||||||
|
PAGINATION_PAGES => ".btn__pager__current";
|
||||||
|
);
|
||||||
|
|
||||||
|
crate fn parse_pagination(html: &Html) -> Result<Pagination> {
|
||||||
|
let total_str = crate::logic::plain_parse(&html, &*PAGINATION_TOTAL)?;
|
||||||
|
let total_results: u64 = total_str
|
||||||
|
.split(' ')
|
||||||
|
.next()
|
||||||
|
.unwrap() // will always have a first element
|
||||||
|
.parse()
|
||||||
|
.map_err(Error::InvalidNumber)?;
|
||||||
|
|
||||||
|
let pages_str = crate::logic::plain_parse(&html, &*PAGINATION_PAGES)?;
|
||||||
|
let mut pages_split = pages_str.split(' ');
|
||||||
|
|
||||||
|
let current_page: u64 = pages_split
|
||||||
|
.nth(1)
|
||||||
|
.ok_or_else(|| Error::invalid_content("4 items in pages string", None))?
|
||||||
|
.parse()
|
||||||
|
.map_err(Error::InvalidNumber)?;
|
||||||
|
|
||||||
|
let total_pages: u64 = pages_split
|
||||||
|
.nth(1)
|
||||||
|
.ok_or_else(|| Error::invalid_content("4 items in pages string", None))?
|
||||||
|
.parse()
|
||||||
|
.map_err(Error::InvalidNumber)?;
|
||||||
|
|
||||||
|
Ok(Pagination {
|
||||||
|
current_page,
|
||||||
|
total_pages,
|
||||||
|
total_results,
|
||||||
|
})
|
||||||
|
}
|
126
src/logic/search/character.rs
Normal file
126
src/logic/search/character.rs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
use crate::{
|
||||||
|
error::*,
|
||||||
|
models::{
|
||||||
|
search::{
|
||||||
|
Paginated,
|
||||||
|
character::CharacterSearchItem,
|
||||||
|
},
|
||||||
|
character::GrandCompanyInfo,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
use ffxiv_types::World;
|
||||||
|
|
||||||
|
use scraper::{Html, ElementRef};
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
selectors!(
|
||||||
|
ITEM_ENTRY => ".ldst__window .entry";
|
||||||
|
|
||||||
|
ITEM_ID => ".entry__link";
|
||||||
|
ITEM_FACE => ".entry__chara__face > img";
|
||||||
|
ITEM_NAME => ".entry__name";
|
||||||
|
ITEM_WORLD => ".entry__world";
|
||||||
|
ITEM_GRAND_COMPANY => ".entry__chara_info .js__tooltip";
|
||||||
|
ITEM_FREE_COMPANY => ".entry__freecompany__link";
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn parse(html: &str) -> Result<Paginated<CharacterSearchItem>> {
|
||||||
|
let html = Html::parse_document(html);
|
||||||
|
|
||||||
|
let pagination = crate::logic::search::parse_pagination(&html)?;
|
||||||
|
|
||||||
|
let results: Vec<CharacterSearchItem> = html
|
||||||
|
.select(&*ITEM_ENTRY)
|
||||||
|
.map(|x| parse_single(x))
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
|
Ok(Paginated {
|
||||||
|
pagination,
|
||||||
|
results,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_single<'a>(html: ElementRef<'a>) -> Result<CharacterSearchItem> {
|
||||||
|
let id = parse_id(html)?;
|
||||||
|
|
||||||
|
let name = plain_parse(html, &*ITEM_NAME)?;
|
||||||
|
let world = parse_world(html)?;
|
||||||
|
|
||||||
|
let grand_company = parse_grand_company(html)?;
|
||||||
|
|
||||||
|
let free_company_id = parse_free_company_id(html)?;
|
||||||
|
|
||||||
|
let face = parse_face(html)?;
|
||||||
|
|
||||||
|
Ok(CharacterSearchItem {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
world,
|
||||||
|
grand_company,
|
||||||
|
free_company_id,
|
||||||
|
face,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plain_parse<'a>(html: ElementRef<'a>, select: &scraper::Selector) -> Result<String> {
|
||||||
|
let string = html
|
||||||
|
.select(select)
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::missing_element(select))?
|
||||||
|
.text()
|
||||||
|
.collect();
|
||||||
|
Ok(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_id<'a>(html: ElementRef<'a>) -> Result<u64> {
|
||||||
|
let e = html
|
||||||
|
.select(&*ITEM_ID)
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::missing_element(&*ITEM_ID))?;
|
||||||
|
crate::logic::parse_id(e.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_world<'a>(html: ElementRef<'a>) -> Result<World> {
|
||||||
|
let world_str = plain_parse(html, &*ITEM_WORLD)?;
|
||||||
|
World::from_str(&world_str)
|
||||||
|
.map_err(|_| Error::invalid_content("valid world", Some(&world_str)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_free_company_id<'a>(html: ElementRef<'a>) -> Result<Option<u64>> {
|
||||||
|
let elem = match html
|
||||||
|
.select(&*ITEM_FREE_COMPANY)
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
Some(e) => e,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
crate::logic::parse_id(elem.value()).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_grand_company<'a>(html: ElementRef<'a>) -> Result<Option<GrandCompanyInfo>> {
|
||||||
|
let text = html
|
||||||
|
.select(&*ITEM_GRAND_COMPANY)
|
||||||
|
.next()
|
||||||
|
.and_then(|x| x.value().attr("data-tooltip"));
|
||||||
|
let text = match text {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
crate::logic::parse_grand_company(text).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_face<'a>(html: ElementRef<'a>) -> Result<Url> {
|
||||||
|
let face_elem = html
|
||||||
|
.select(&*ITEM_FACE)
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::missing_element(&*ITEM_FACE))?;
|
||||||
|
let src = face_elem
|
||||||
|
.value()
|
||||||
|
.attr("src")
|
||||||
|
.ok_or_else(|| Error::invalid_content("src on face img element", None))?;
|
||||||
|
Url::from_str(src).map_err(Error::InvalidUrl)
|
||||||
|
}
|
@ -26,6 +26,7 @@ macro_rules! ffxiv_enum {
|
|||||||
|
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod free_company;
|
pub mod free_company;
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
ffxiv_enum!(
|
ffxiv_enum!(
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
14
src/models/search.rs
Normal file
14
src/models/search.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
pub mod character;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Pagination {
|
||||||
|
pub current_page: u64,
|
||||||
|
pub total_pages: u64,
|
||||||
|
pub total_results: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Paginated<T> {
|
||||||
|
pub pagination: Pagination,
|
||||||
|
pub results: Vec<T>,
|
||||||
|
}
|
16
src/models/search/character.rs
Normal file
16
src/models/search/character.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use crate::models::character::GrandCompanyInfo;
|
||||||
|
|
||||||
|
use ffxiv_types::World;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct CharacterSearchItem {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub world: World,
|
||||||
|
pub grand_company: Option<GrandCompanyInfo>,
|
||||||
|
pub free_company_id: Option<u64>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub face: Url,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user