use std::sync::LazyLock; use anyhow::{Ok, Result}; use scraper::{ElementRef, Selector, selectable::Selectable}; use serde::Deserialize; use time_rs::OffsetDateTime; use crate::{ClientScraper, Event, EventScraper, Fight, Fighter, text_content}; pub enum ONE {} const URL: &str = "https://www.onefc.com/events/"; static EVENT_LIST: LazyLock = LazyLock::new(|| Selector::parse("#upcoming-events-section .is-event").unwrap()); impl EventScraper for ONE { async fn scrape(client: &C) -> Result> { let page = client.get(URL).await?; let mut events = Vec::new(); for elem in page.select(&EVENT_LIST) { if let Some(event) = get_event(client, elem).await { events.push(event); }; } Ok(events) } } #[derive(Deserialize)] struct Location { name: String, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct EventData { name: String, image: Vec, location: Location, #[serde(with = "time_rs::serde::rfc3339")] start_date: OffsetDateTime, } static EVENT_CHECK: LazyLock = LazyLock::new(|| Selector::parse("a.smart-link").unwrap()); static EVENT_URL: LazyLock = LazyLock::new(|| Selector::parse("a.is-image-zoom[href]").unwrap()); static EVENT_DATA: LazyLock = LazyLock::new(|| Selector::parse("#site-main script[type=\"application/ld+json\"]").unwrap()); static EVENT_FIGHTS: LazyLock = LazyLock::new(|| Selector::parse(".event-matchup").unwrap()); async fn get_event(client: &C, elem: ElementRef<'_>) -> Option { elem.select(&EVENT_CHECK) .next() .map(|e| e.attr("href")) .flatten()?; let url = elem .select(&EVENT_URL) .next() .map(|e| e.attr("href")) .flatten()?; let page = client.get(url).await.ok()?; let data_elem = page.select(&EVENT_DATA).next()?; let data_str: String = data_elem.text().collect(); let data: EventData = serde_json::from_str(&data_str).ok()?; let mut fights = Vec::new(); for elem in page.select(&EVENT_FIGHTS) { if let Some(fight) = get_fight(elem) { fights.push(fight); } } if fights.len() == 0 { return None; } let event = Event { organization: "ONE".into(), name: data.name, location: data.location.name, date: data.start_date.unix_timestamp(), image: data .image .get(2) .map(|s| s.clone()) .unwrap_or_else(String::default), url: url.to_string(), slug: url.replace(URL, "").replace("/", ""), fights, }; Some(event) } static FIGHT_WEIGHT: LazyLock = LazyLock::new(|| Selector::parse(".title").unwrap()); fn get_fight(elem: ElementRef<'_>) -> Option { let fighter_a = get_fighter::(elem)?; let fighter_b = get_fighter::(elem)?; let weight = text_content(elem, &FIGHT_WEIGHT)?; Some(Fight { weight, fighter_a, fighter_b, }) } static FIGHTER_A_NAME: LazyLock = LazyLock::new(|| Selector::parse("tr.vs :nth-child(1) a").unwrap()); static FIGHTER_A_COUNTRY: LazyLock = LazyLock::new(|| Selector::parse("tr.vs + tr :nth-child(1)").unwrap()); static FIGHTER_A_IMAGE: LazyLock = LazyLock::new(|| Selector::parse(".face1 img[src]").unwrap()); static FIGHTER_B_NAME: LazyLock = LazyLock::new(|| Selector::parse("tr.vs :nth-child(3) a").unwrap()); static FIGHTER_B_COUNTRY: LazyLock = LazyLock::new(|| Selector::parse("tr.vs + tr :nth-child(3)").unwrap()); static FIGHTER_B_IMAGE: LazyLock = LazyLock::new(|| Selector::parse(".face2 img[src]").unwrap()); fn get_fighter(elem: ElementRef<'_>) -> Option { let name = text_content( elem, if IS_A { &FIGHTER_A_NAME } else { &FIGHTER_B_NAME }, )?; let country = text_content( elem, if IS_A { &FIGHTER_A_COUNTRY } else { &FIGHTER_B_COUNTRY }, )?; let image = elem .select(if IS_A { &FIGHTER_A_IMAGE } else { &FIGHTER_B_IMAGE }) .next() .map(|e| e.attr("src")) .flatten() .map(|s| s.to_string())?; Some(Fighter { name, image, country, link: "".into(), }) } #[cfg(test)] mod tests { use super::*; use std::{collections::HashMap, sync::LazyLock}; use crate::tests::DummyClient; static CLIENT: LazyLock = LazyLock::new(|| { DummyClient::new(HashMap::from([ ( "https://www.onefc.com/events/", include_str!("./testdata/one-event-list.html"), ), ( "https://www.onefc.com/events/one-friday-fights-97/", include_str!("./testdata/one-friday-fights-97.html"), ), ( "https://www.onefc.com/events/one171/", include_str!("./testdata/one-171.html"), ), ( "https://www.onefc.com/events/onefightnight29/", include_str!("./testdata/one-fn-29.html"), ), ( "https://www.onefc.com/events/one172/", include_str!("./testdata/one-172.html"), ), ])) }); #[tokio::test] async fn event_list() { let events = ONE::scrape(&*CLIENT).await.unwrap(); assert!(events.len() == 4); println!("{}", serde_json::to_string_pretty(&events).unwrap()); } }