chore: initial commit
commit
b538545f67
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
|
@ -0,0 +1,22 @@
|
|||
cargo-features = ["edition"]
|
||||
|
||||
[package]
|
||||
name = "lodestone_scraper"
|
||||
version = "0.1.0"
|
||||
authors = ["Kyle Clemens <git@kyleclemens.com>"]
|
||||
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
reqwest = "0.8"
|
||||
failure = "0.1"
|
||||
lazy_static = "1"
|
||||
url = "1"
|
||||
|
||||
[dependencies.lodestone_parser]
|
||||
git = "https://github.com/jkcclemens/lodestone_parser"
|
||||
|
||||
[dependencies.ffxiv_types]
|
||||
version = "1"
|
||||
default-features = false
|
||||
features = ["worlds", "data_centers", "races", "clans"]
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 Kyle Clemens
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,5 @@
|
|||
pub mod character_search;
|
||||
pub mod free_company_search;
|
||||
|
||||
pub use self::character_search::CharacterSearchBuilder;
|
||||
pub use self::free_company_search::FreeCompanySearchBuilder;
|
|
@ -0,0 +1,135 @@
|
|||
use crate::{
|
||||
LodestoneScraper,
|
||||
error::*,
|
||||
models::ClassJobRole,
|
||||
util::{Either, AsLodestone},
|
||||
};
|
||||
|
||||
use ffxiv_types::{World, Race, Clan, DataCenter};
|
||||
|
||||
use lodestone_parser::models::{
|
||||
GrandCompany,
|
||||
character::Job,
|
||||
search::{
|
||||
Paginated,
|
||||
character::CharacterSearchItem,
|
||||
},
|
||||
};
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CharacterSearchBuilder<'a> {
|
||||
scraper: &'a LodestoneScraper,
|
||||
// q
|
||||
name: Option<&'a str>,
|
||||
// worldname
|
||||
world: Option<Either<World, DataCenter>>,
|
||||
// classjob
|
||||
job: Option<Either<Job, ClassJobRole>>,
|
||||
// race_tribe
|
||||
race: Option<Either<Race, Clan>>,
|
||||
// gcid
|
||||
grand_company: Option<Vec<GrandCompany>>,
|
||||
}
|
||||
|
||||
impl<'a> CharacterSearchBuilder<'a> {
|
||||
pub fn new(scraper: &'a LodestoneScraper) -> Self {
|
||||
CharacterSearchBuilder {
|
||||
scraper,
|
||||
name: None,
|
||||
world: None,
|
||||
job: None,
|
||||
race: None,
|
||||
grand_company: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&mut self, n: &'a str) -> &mut Self {
|
||||
self.name = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn world(&mut self, w: World) -> &mut Self {
|
||||
self.world = Some(Either::Left(w));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data_center(&mut self, dc: DataCenter) -> &mut Self {
|
||||
self.world = Some(Either::Right(dc));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn job(&mut self, j: Job) -> &mut Self {
|
||||
self.job = Some(Either::Left(j));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn role(&mut self, r: ClassJobRole) -> &mut Self {
|
||||
self.job = Some(Either::Right(r));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn race(&mut self, r: Race) -> &mut Self {
|
||||
self.race = Some(Either::Left(r));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clan(&mut self, c: Clan) -> &mut Self {
|
||||
self.race = Some(Either::Right(c));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn grand_company(&mut self, gc: GrandCompany) -> &mut Self {
|
||||
self.grand_company.get_or_insert_with(Default::default).push(gc);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn send(&self) -> Result<Paginated<CharacterSearchItem>> {
|
||||
let text = self.scraper.client
|
||||
.get(self.as_url())
|
||||
.send()
|
||||
.map_err(Error::Net)?
|
||||
.text()
|
||||
.map_err(Error::Net)?;
|
||||
lodestone_parser::parse_character_search(&text).map_err(Error::Parse)
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> Url {
|
||||
let mut url = crate::LODESTONE_URL.join("character/").unwrap();
|
||||
|
||||
{
|
||||
let mut pairs = url.query_pairs_mut();
|
||||
|
||||
if let Some(ref name) = self.name {
|
||||
pairs.append_pair("q", name);
|
||||
}
|
||||
|
||||
match self.world {
|
||||
Some(Either::Left(w)) => { pairs.append_pair("worldname", w.as_str()); },
|
||||
Some(Either::Right(dc)) => { pairs.append_pair("worldname", &dc.as_lodestone()); },
|
||||
_ => {},
|
||||
}
|
||||
|
||||
match self.job {
|
||||
Some(Either::Left(j)) => { pairs.append_pair("classjob", &j.as_lodestone().to_string()); },
|
||||
Some(Either::Right(cjr)) => { pairs.append_pair("classjob", cjr.as_lodestone()); },
|
||||
_ => {},
|
||||
}
|
||||
|
||||
match self.race {
|
||||
Some(Either::Left(r)) => { pairs.append_pair("race_tribe", &format!("race_{}", r.as_lodestone())); },
|
||||
Some(Either::Right(c)) => { pairs.append_pair("race_tribe", &format!("tribe_{}", c.as_lodestone())); },
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(ref gcs) = self.grand_company {
|
||||
for gc in gcs {
|
||||
pairs.append_pair("gcid", &gc.as_lodestone().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
use crate::{
|
||||
LodestoneScraper,
|
||||
error::*,
|
||||
util::{Either, AsLodestone},
|
||||
};
|
||||
|
||||
use ffxiv_types::{World, DataCenter};
|
||||
|
||||
use lodestone_parser::models::{
|
||||
GrandCompany,
|
||||
search::{
|
||||
Paginated,
|
||||
free_company::FreeCompanySearchItem,
|
||||
},
|
||||
};
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FreeCompanySearchBuilder<'a> {
|
||||
scraper: &'a LodestoneScraper,
|
||||
// q
|
||||
name: Option<&'a str>,
|
||||
// worldname
|
||||
world: Option<Either<World, DataCenter>>,
|
||||
// gcid
|
||||
grand_company: Option<Vec<GrandCompany>>,
|
||||
}
|
||||
|
||||
impl<'a> FreeCompanySearchBuilder<'a> {
|
||||
pub fn new(scraper: &'a LodestoneScraper) -> Self {
|
||||
FreeCompanySearchBuilder {
|
||||
scraper,
|
||||
name: None,
|
||||
world: None,
|
||||
grand_company: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&mut self, n: &'a str) -> &mut Self {
|
||||
self.name = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn world(&mut self, w: World) -> &mut Self {
|
||||
self.world = Some(Either::Left(w));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data_center(&mut self, dc: DataCenter) -> &mut Self {
|
||||
self.world = Some(Either::Right(dc));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn grand_company(&mut self, gc: GrandCompany) -> &mut Self {
|
||||
self.grand_company.get_or_insert_with(Default::default).push(gc);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn send(&self) -> Result<Paginated<FreeCompanySearchItem>> {
|
||||
let text = self.scraper.client
|
||||
.get(self.as_url())
|
||||
.send()
|
||||
.map_err(Error::Net)?
|
||||
.text()
|
||||
.map_err(Error::Net)?;
|
||||
lodestone_parser::parse_free_company_search(&text).map_err(Error::Parse)
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> Url {
|
||||
let mut url = crate::LODESTONE_URL.join("freecompany/").unwrap();
|
||||
|
||||
{
|
||||
let mut pairs = url.query_pairs_mut();
|
||||
|
||||
if let Some(ref name) = self.name {
|
||||
pairs.append_pair("q", name);
|
||||
}
|
||||
|
||||
match self.world {
|
||||
Some(Either::Left(w)) => { pairs.append_pair("worldname", w.as_str()); },
|
||||
Some(Either::Right(dc)) => { pairs.append_pair("worldname", &dc.as_lodestone()); },
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(ref gcs) = self.grand_company {
|
||||
for gc in gcs {
|
||||
pairs.append_pair("gcid", &gc.as_lodestone().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "network error: {}", _0)]
|
||||
Net(reqwest::Error),
|
||||
#[fail(display = "url parse error: {}", _0)]
|
||||
Url(url::ParseError),
|
||||
#[fail(display = "lodestone parse error: {}", _0)]
|
||||
Parse(lodestone_parser::error::Error),
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
#[macro_use] extern crate failure;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use lodestone_parser::models::{
|
||||
character::Character,
|
||||
free_company::FreeCompany,
|
||||
};
|
||||
|
||||
use reqwest::Client;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
pub mod builder;
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
crate mod util;
|
||||
|
||||
use crate::error::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LodestoneScraper {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Default for LodestoneScraper {
|
||||
fn default() -> Self {
|
||||
let client = Client::new();
|
||||
LodestoneScraper { client }
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LODESTONE_URL: Url = Url::from_str("https://na.finalfantasyxiv.com/lodestone/").unwrap();
|
||||
}
|
||||
|
||||
impl LodestoneScraper {
|
||||
fn route(s: &str) -> Result<Url> {
|
||||
LODESTONE_URL.join(s).map_err(Error::Url)
|
||||
}
|
||||
|
||||
pub fn character(&self, id: u64) -> Result<Character> {
|
||||
let url = LodestoneScraper::route(&format!("character/{}", id))?;
|
||||
let text = self.client
|
||||
.get(url)
|
||||
.send()
|
||||
.map_err(Error::Net)?
|
||||
.text()
|
||||
.map_err(Error::Net)?;
|
||||
lodestone_parser::parse_character(id, &text).map_err(Error::Parse)
|
||||
}
|
||||
|
||||
pub fn character_search(&self) -> builder::CharacterSearchBuilder {
|
||||
builder::CharacterSearchBuilder::new(self)
|
||||
}
|
||||
|
||||
pub fn free_company(&self, id: u64) -> Result<FreeCompany> {
|
||||
let url = LodestoneScraper::route(&format!("freecompany/{}", id))?;
|
||||
let text = self.client
|
||||
.get(url)
|
||||
.send()
|
||||
.map_err(Error::Net)?
|
||||
.text()
|
||||
.map_err(Error::Net)?;
|
||||
lodestone_parser::parse_free_company(id, &text).map_err(Error::Parse)
|
||||
}
|
||||
|
||||
pub fn free_company_search(&self) -> builder::FreeCompanySearchBuilder {
|
||||
builder::FreeCompanySearchBuilder::new(self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod class_job_role;
|
||||
|
||||
pub use self::class_job_role::ClassJobRole;
|
|
@ -0,0 +1,13 @@
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ClassJobRole {
|
||||
ClassTank,
|
||||
ClassHealer,
|
||||
ClassDps,
|
||||
|
||||
JobTank,
|
||||
JobHealer,
|
||||
JobDps,
|
||||
|
||||
ClassDiscipleOfTheHand,
|
||||
ClassDiscipleOfTheLand,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
crate mod as_lodestone;
|
||||
crate mod either;
|
||||
|
||||
crate use self::as_lodestone::AsLodestone;
|
||||
crate use self::either::Either;
|
|
@ -0,0 +1,131 @@
|
|||
use crate::models::ClassJobRole;
|
||||
|
||||
use ffxiv_types::{DataCenter, Race, Clan};
|
||||
|
||||
use lodestone_parser::models::{
|
||||
GrandCompany,
|
||||
character::Job,
|
||||
};
|
||||
|
||||
crate trait AsLodestone {
|
||||
type Representation;
|
||||
|
||||
fn as_lodestone(&self) -> Self::Representation;
|
||||
}
|
||||
|
||||
impl AsLodestone for DataCenter {
|
||||
type Representation = String;
|
||||
|
||||
fn as_lodestone(&self) -> Self::Representation {
|
||||
format!("_dc_{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsLodestone for ClassJobRole {
|
||||
type Representation = &'static str;
|
||||
|
||||
fn as_lodestone(&self) -> Self::Representation {
|
||||
match *self {
|
||||
ClassJobRole::ClassTank => "_class_TANK",
|
||||
ClassJobRole::ClassHealer => "_class_HEALER",
|
||||
ClassJobRole::ClassDps => "_class_DPS",
|
||||
ClassJobRole::JobTank => "_job_TANK",
|
||||
ClassJobRole::JobHealer => "_job_HEALER",
|
||||
ClassJobRole::JobDps => "_job_DPS",
|
||||
ClassJobRole::ClassDiscipleOfTheHand => "_class_CRAFTER",
|
||||
ClassJobRole::ClassDiscipleOfTheLand => "_class_GATHERER",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsLodestone for Job {
|
||||
type Representation = u8;
|
||||
|
||||
fn as_lodestone(&self) -> Self::Representation {
|
||||
match *self {
|
||||
Job::Gladiator => 1,
|
||||
Job::Pugilist => 2,
|
||||
Job::Marauder => 3,
|
||||
Job::Lancer => 4,
|
||||
Job::Archer => 5,
|
||||
Job::Conjurer => 6,
|
||||
Job::Thaumaturge => 7,
|
||||
Job::Carpenter => 8,
|
||||
Job::Blacksmith => 9,
|
||||
Job::Armorer => 10,
|
||||
Job::Goldsmith => 11,
|
||||
Job::Leatherworker => 12,
|
||||
Job::Weaver => 13,
|
||||
Job::Alchemist => 14,
|
||||
Job::Culinarian => 15,
|
||||
Job::Miner => 16,
|
||||
Job::Botanist => 17,
|
||||
Job::Fisher => 18,
|
||||
Job::Paladin => 19,
|
||||
Job::Monk => 20,
|
||||
Job::Warrior => 21,
|
||||
Job::Dragoon => 22,
|
||||
Job::Bard => 23,
|
||||
Job::WhiteMage => 24,
|
||||
Job::BlackMage => 25,
|
||||
Job::Arcanist => 26,
|
||||
Job::Summoner => 27,
|
||||
Job::Scholar => 28,
|
||||
Job::Rogue => 29,
|
||||
Job::Ninja => 30,
|
||||
Job::Machinist => 31,
|
||||
Job::DarkKnight => 32,
|
||||
Job::Astrologian => 33,
|
||||
Job::Samurai => 34,
|
||||
Job::RedMage => 35,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsLodestone for GrandCompany {
|
||||
type Representation = u8;
|
||||
|
||||
fn as_lodestone(&self) -> Self::Representation {
|
||||
match *self {
|
||||
GrandCompany::Maelstrom => 1,
|
||||
GrandCompany::TwinAdders => 2,
|
||||
GrandCompany::Flames => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsLodestone for Race {
|
||||
type Representation = u8;
|
||||
|
||||
fn as_lodestone(&self) -> Self::Representation {
|
||||
match *self {
|
||||
Race::Hyur => 1,
|
||||
Race::Elezen => 2,
|
||||
Race::Lalafell => 3,
|
||||
Race::Miqote => 4,
|
||||
Race::Roegadyn => 5,
|
||||
Race::AuRa => 6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsLodestone for Clan {
|
||||
type Representation = u8;
|
||||
|
||||
fn as_lodestone(&self) -> Self::Representation {
|
||||
match *self {
|
||||
Clan::Midlander => 1,
|
||||
Clan::Highlander => 2,
|
||||
Clan::Wildwood => 3,
|
||||
Clan::Duskwight => 4,
|
||||
Clan::Plainsfolk => 5,
|
||||
Clan::Dunesfolk => 6,
|
||||
Clan::SeekerOfTheSun => 7,
|
||||
Clan::KeeperOfTheMoon => 8,
|
||||
Clan::SeaWolf => 9,
|
||||
Clan::Hellsguard => 10,
|
||||
Clan::Raen => 11,
|
||||
Clan::Xaela => 12,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
crate enum Either<L, R> {
|
||||
Left(L),
|
||||
Right(R),
|
||||
}
|
||||
|
||||
impl<L, R> std::fmt::Debug for Either<L, R>
|
||||
where L: std::fmt::Debug,
|
||||
R: std::fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
Either::Left(ref l) => f.debug_tuple("Left").field(l).finish(),
|
||||
Either::Right(ref r) => f.debug_tuple("Right").field(r).finish(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue