diff --git a/html/Searches/FreeCompany.html b/html/Searches/FreeCompany.html new file mode 100644 index 0000000..e52489d --- /dev/null +++ b/html/Searches/FreeCompany.html @@ -0,0 +1,1477 @@ + + + +Free Companies | FINAL FANTASY XIV, The Lodestone + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + +
+ + +
+ + + + + + +
+ + + +
+ +
+ + + + + + + + + + + + + + +
+ + + + +

Free Company

+ + + +
+ +
+ +
+

Free Company Search

+

Search Results

+ +
+
+
+ +

Data Center / World

+
+ +
+

Active Members

+
+ +
+ + + +

Focus

+
+ +

Seeking

+
+ + +

Active

+
+ +
+

Recruitment

+
+ +
+

Housing

+
+ +
+

Grand Company

+
+
+ +
+
+ + +
Free Company: a| Data Center/World : All| Active Members: All| Focus: Role-playing, Leveling, Casual, Hardcore, Dungeons, Guildhests, Trials, Raids, PvP, Not specified| Seeking: Tank, Healer, DPS, Crafter, Gatherer, Not specified| Active: All| Recruitment: All| Housing: All| Grand Company: Maelstrom, Order of the Twin Adder, Immortal Flames
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
142743 Total
+
+
+ + + + + +
    + +
  • +
  • + + +
  • Page 1 of 20
  • + + +
  • +
  • + +
+ + + + + +

Immortal Flames

! - Cobra - !

Famfrit

  • 9
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Immortal Flames

! Fairy Tail !

Odin

  • 2
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Order of the Twin Adder

! NYANDESUTO !

Belias

  • 8
  • Estate Built
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Immortal Flames

! Red and Blue !

Ridill

  • 4
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Immortal Flames

! Warmth !

Leviathan

  • 3
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Open
+ + +

Immortal Flames

!! Eternal Vermilion

Unicorn

  • 7
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Order of the Twin Adder

!!!Akatsuki!!!

Louisoix

  • 6
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Immortal Flames

!!!Dragon_Slayers!!!

Zodiark

  • 3
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Order of the Twin Adder

!!--Kindergarten--!!

Kujata

  • 4
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

!!Jean-Vaude-Clan!!

Gilgamesh

  • 4
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Order of the Twin Adder

!!KARAAGE!!

Masamune

  • 1
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Maelstrom

!-_- ! ,,I,,

Gilgamesh

  • 1
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

!-_- ! ,,l,,

Siren

  • 2
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Immortal Flames

!-Dark Oppression-!

Adamantoise

  • 4
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

!_!Voltage!_!

Balmung

  • 1
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Maelstrom

!A!

Shinryu

  • 9
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Immortal Flames

!ABRAKADABRA!

Moogle

  • 2
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Maelstrom

!Alice in Wonderland

Zeromus

  • 1
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Order of the Twin Adder

!Alty!

Ultros

  • 10
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Order of the Twin Adder

!bang

Sargatanas

  • 27
  • Estate Built
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Immortal Flames

!Bl4CK AC3! SQU4D

Louisoix

  • 10
  • No Estate or Plot
  • -
  • Active: Weekends
  • Recruitment: Closed
+ + +

Immortal Flames

!Boymaid Cafe!

Lich

  • 3
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Closed
+ + +

Order of the Twin Adder

!Dance Magic Dance!

Famfrit

  • 2
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Immortal Flames

!Extremely_Mediocre!

Excalibur

  • 4
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

!Friendly Bastards!

Siren

  • 21
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Maelstrom

!l!?!!?l?!!?!l!

Lich

  • 16
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Order of the Twin Adder

!LOVE&PEACE!

Anima

  • 27
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Maelstrom

!maborosinosyokuzai!

Atomos

  • 10
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Order of the Twin Adder

!Mo0 Clan Emperors!

Ramuh

  • 30
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Immortal Flames

!No Name!

Shiva

  • 1
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Order of the Twin Adder

!nsomnia

Shiva

  • 14
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Maelstrom

!nsouciant

Gungnir

  • 3
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Maelstrom

!Optimistic!

Mandragora

  • 2
  • No Estate or Plot
  • -
  • Active: Weekends
  • Recruitment: Open
+ + +

Immortal Flames

!RuTuKuNinA??

Yojimbo

  • 4
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Order of the Twin Adder

!Tantalus!

Exodus

  • 4
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Order of the Twin Adder

!Tantalus!

Lamia

  • 37
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Immortal Flames

!Tantalus!

Jenova

  • 46
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Order of the Twin Adder

!White Fang!

Anima

  • 3
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Order of the Twin Adder

!ZodiakElit3s!

Ultros

  • 4
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Immortal Flames

& Darling, Im yours!

Goblin

  • 6
  • Estate Built
  • -
  • Active: Not specified
  • Recruitment: Open
+ + +

Order of the Twin Adder

& home party

Ifrit

  • 11
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Immortal Flames

&--Pandora's Box--&

Odin

  • 1
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Closed
+ + +

Maelstrom

&_YAYO_&

Valefor

  • 1
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Immortal Flames

&banque&

Moogle

  • 2
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

&COMMANDO&

Titan

  • 5
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

&Nocturn.Dear_Lotas&

Valefor

  • 11
  • Estate Built
  • -
  • Active: Always
  • Recruitment: Open
+ + +

Order of the Twin Adder

&ZIPANG&

Ultima

  • 3
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Immortal Flames

'' .What Else. ''

Goblin

  • 5
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

'' The Clan ''

Ragnarok

  • 1
  • No Estate or Plot
  • -
  • Active: Not specified
  • Recruitment: Closed
+ + +

Maelstrom

'' Vana'Diel ''

Excalibur

  • 4
  • No Estate or Plot
  • -
  • Active: Always
  • Recruitment: Open
+ + +
    + +
  • +
  • + + +
  • Page 1 of 20
  • + + +
  • +
  • + +
+ + + + + +
+
+ +
+ + + + +
+ ForumsMog StationOfficial Blog +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + +
+ + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + + + +
+ + + +
+

Community Wall

+ +
+
+

Recent Activity

+ +
+

+ Filter which items are to be displayed below.
+ + * Notifications for standings updates are shared across all Worlds.
+ * Notifications for PvP team formations are shared for all languages.
+ * Notifications for free company formations are shared for all languages. +
+

+ + +
+
Sort by
Data Center / World
Primary language
Displaying
+
+ + +
+
+ + + + + +
+ + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + +
+ +

Cookie Policy

+

This website uses cookies. If you do not wish us to set cookies on your device, please do not use the website. Please read the Square Enix cookies policy for more information. Your use of the website is also subject to the terms in the Square Enix website terms of use and privacy policy and by using the website you are accepting those terms. The Square Enix terms of use, privacy policy and cookies policy can also be found through links at the bottom of the page.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 27cab38..eab0f0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,3 +10,4 @@ pub mod error; pub mod logic; pub mod models; +crate mod util; diff --git a/src/logic.rs b/src/logic.rs index dafeed8..d8cf0d4 100644 --- a/src/logic.rs +++ b/src/logic.rs @@ -8,6 +8,7 @@ use crate::{ use scraper::{ Html, + ElementRef, node::Element, }; @@ -35,6 +36,16 @@ crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result Ok(string) } +crate fn plain_parse_elem<'a>(html: ElementRef<'a>, select: &scraper::Selector) -> Result { + let string = html + .select(select) + .next() + .ok_or(Error::missing_element(select))? + .text() + .collect(); + Ok(string) +} + crate fn parse_id<'a>(a: &'a Element) -> Result { let href = a.attr("href").ok_or_else(|| Error::invalid_content("href on link", None))?; let last = href diff --git a/src/logic/search/character.rs b/src/logic/search/character.rs index 08f4a90..2595621 100644 --- a/src/logic/search/character.rs +++ b/src/logic/search/character.rs @@ -1,5 +1,6 @@ use crate::{ error::*, + logic::plain_parse_elem as plain_parse, models::{ search::{ Paginated, @@ -66,16 +67,6 @@ fn parse_single<'a>(html: ElementRef<'a>) -> Result { }) } -fn plain_parse<'a>(html: ElementRef<'a>, select: &scraper::Selector) -> Result { - let string = html - .select(select) - .next() - .ok_or(Error::missing_element(select))? - .text() - .collect(); - Ok(string) -} - fn parse_id<'a>(html: ElementRef<'a>) -> Result { let e = html .select(&*ITEM_ID) diff --git a/src/logic/search/free_company.rs b/src/logic/search/free_company.rs new file mode 100644 index 0000000..f0b74d9 --- /dev/null +++ b/src/logic/search/free_company.rs @@ -0,0 +1,163 @@ +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)?; + + let results: Vec = html + .select(&*ITEM_ENTRY) + .map(|x| parse_single(x)) + .collect::>()?; + + Ok(Paginated { + pagination, + results, + }) +} + +fn parse_single<'a>(html: ElementRef<'a>) -> 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<'a>(html: ElementRef<'a>) -> 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<'a>(html: ElementRef<'a>) -> 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<'a>(html: ElementRef<'a>) -> Result { + 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_crest<'a>(html: ElementRef<'a>) -> 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<'a>(html: ElementRef<'a>) -> Result { + plain_parse(html, &*ITEM_ACTIVE_MEMBERS) + .and_then(|x| x.parse().map_err(Error::InvalidNumber)) +} + +fn parse_estate_built<'a>(html: ElementRef<'a>) -> 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, + _ => return Err(Error::invalid_content("`Estate Built` or `No Estate or Plot`", Some(&estate_built))), + }; + Ok(built) +} + +fn parse_formed<'a>(html: ElementRef<'a>) -> Result> { + let script = html + .select(&*ITEM_FORMED) + .next() + .ok_or(Error::missing_element(&*ITEM_FORMED))? + .inner_html(); + + let timestamp = script + .split("strftime(") + .nth(1) + .ok_or(Error::invalid_content("strftime call", Some(&script)))? + .split(",") + .next() + .ok_or(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<'a>(html: ElementRef<'a>) -> 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<'a>(html: ElementRef<'a>) -> 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)))) +} diff --git a/src/models/free_company.rs b/src/models/free_company.rs index b12dba3..c1ede91 100644 --- a/src/models/free_company.rs +++ b/src/models/free_company.rs @@ -14,7 +14,7 @@ pub struct FreeCompany { pub name: String, pub world: World, pub slogan: String, - #[serde(with = "multi_url")] + #[serde(with = "crate::util::serde::multi_url")] pub crest: Vec, pub grand_company: GrandCompany, pub active_members: u16, @@ -37,30 +37,3 @@ pub struct Estate { pub address: String, pub greeting: String, } - -mod multi_url { - use serde::{Deserializer, Deserialize, Serializer, ser::SerializeSeq}; - - use url::Url; - - crate fn serialize(urls: &Vec, serializer: S) -> Result - where S: Serializer, - { - let mut seq = serializer.serialize_seq(Some(urls.len()))?; - for url in urls { - seq.serialize_element(&url_serde::Ser::new(url))?; - } - seq.end() - } - - crate fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where D: Deserializer<'de> - { - - #[derive(Deserialize)] - struct Wrapper(#[serde(with = "url_serde")] Url); - - let urls = Vec::deserialize(deserializer)?; - Ok(urls.into_iter().map(|Wrapper(u)| u).collect()) - } -} diff --git a/src/models/search/free_company.rs b/src/models/search/free_company.rs new file mode 100644 index 0000000..0d57cbe --- /dev/null +++ b/src/models/search/free_company.rs @@ -0,0 +1,35 @@ +use crate::models::GrandCompany; + +use chrono::{DateTime, Utc}; + +use ffxiv_types::World; + +use url::Url; + +#[derive(Debug, Serialize, Deserialize)] +pub struct FreeCompanySearchItem { + pub id: u64, + pub name: String, + pub world: World, + #[serde(with = "crate::util::serde::multi_url")] + pub crest: Vec, + pub grand_company: GrandCompany, + pub active_members: u16, + pub estate_built: bool, + pub formed: DateTime, + pub active: Active, + pub recruitment: RecruitmentStatus, +} + +ffxiv_enum!(Active { + Always => "Always", + Weekdays => "Weekdays", + Weekends => "Weekends", + Never => "Never", + NotSpecified => "Not specified", +}); + +ffxiv_enum!(RecruitmentStatus { + Open => "Open", + Closed => "Closed", +}); diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..82b2d09 --- /dev/null +++ b/src/util.rs @@ -0,0 +1 @@ +pub mod serde; diff --git a/src/util/serde.rs b/src/util/serde.rs new file mode 100644 index 0000000..e4b2468 --- /dev/null +++ b/src/util/serde.rs @@ -0,0 +1 @@ +pub mod multi_url; diff --git a/src/util/serde/multi_url.rs b/src/util/serde/multi_url.rs new file mode 100644 index 0000000..c47a5a4 --- /dev/null +++ b/src/util/serde/multi_url.rs @@ -0,0 +1,24 @@ +use serde::{Deserializer, Deserialize, Serializer, ser::SerializeSeq}; + +use url::Url; + +crate fn serialize(urls: &Vec, serializer: S) -> Result + where S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(urls.len()))?; + for url in urls { + seq.serialize_element(&url_serde::Ser::new(url))?; + } + seq.end() +} + +crate fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> +{ + + #[derive(Deserialize)] + struct Wrapper(#[serde(with = "url_serde")] Url); + + let urls = Vec::deserialize(deserializer)?; + Ok(urls.into_iter().map(|Wrapper(u)| u).collect()) +}