diff options
Diffstat (limited to 'rust/scraper')
-rw-r--r-- | rust/scraper/Cargo.toml | 17 | ||||
-rw-r--r-- | rust/scraper/src/main.rs | 19 | ||||
-rw-r--r-- | rust/scraper/src/touchpad/error.rs | 13 | ||||
-rw-r--r-- | rust/scraper/src/touchpad/mod.rs | 168 | ||||
-rw-r--r-- | rust/scraper/src/touchpad/request_response.rs | 127 |
5 files changed, 344 insertions, 0 deletions
diff --git a/rust/scraper/Cargo.toml b/rust/scraper/Cargo.toml new file mode 100644 index 0000000..7300844 --- /dev/null +++ b/rust/scraper/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "scraper" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.21" +futures = "0.3.21" +parking_lot = "0.12.1" +pbjson-types = "0.4.0" +proto = { path = "../proto" } +reqwest = { version = "0.11.11", features = ["json", "rustls-tls"] } +serde = { version = "1.0.143", features = ["derive"] } +thiserror = "1.0.32" +tokio = { version = "1.20.1", features = ["full"] } diff --git a/rust/scraper/src/main.rs b/rust/scraper/src/main.rs new file mode 100644 index 0000000..80a2f8f --- /dev/null +++ b/rust/scraper/src/main.rs @@ -0,0 +1,19 @@ +use touchpad::TouchpadLiveClient; + +mod touchpad; + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error>> { + let client = TouchpadLiveClient::new(); + + let meet_info = client.meet_info(18618).await?; + println!("{:?}", meet_info); + + let events = client.events(18618).await?; + println!("{:?}", events); + + let swimmers = client.swimmers(18618).await?; + println!("{:?}", swimmers); + + Ok(()) +}
\ No newline at end of file diff --git a/rust/scraper/src/touchpad/error.rs b/rust/scraper/src/touchpad/error.rs new file mode 100644 index 0000000..57c2eda --- /dev/null +++ b/rust/scraper/src/touchpad/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TouchpadLiveError { + #[error("Reqwest error")] + RequestError(#[from] reqwest::Error), + #[error("Date Parse Error")] + DateParseError(#[from] chrono::ParseError), + #[error("Abort Error")] + AbortError(#[from] tokio::task::JoinError), + #[error("Integer Parse Error")] + ParseError(#[from] std::num::ParseIntError) +}
\ No newline at end of file diff --git a/rust/scraper/src/touchpad/mod.rs b/rust/scraper/src/touchpad/mod.rs new file mode 100644 index 0000000..fdbd591 --- /dev/null +++ b/rust/scraper/src/touchpad/mod.rs @@ -0,0 +1,168 @@ +mod error; +mod request_response; + +use futures::{future, TryFutureExt}; +use proto::touchpad::common::v1; +use reqwest::Client; +use std::collections::HashMap; + +pub use error::TouchpadLiveError; +use proto::ProtoTimestamp; + +use crate::touchpad::request_response::{ + EventsResponse, ParticipantResponse, TeamInfoResponse, +}; +use request_response::MeetInfoResponse; + +const BASE_URL: &'static str = "https://www.touchpadlive.com/rest/touchpadlive"; + +#[derive(Clone)] +pub struct TouchpadLiveClient { + client: Client, +} + +impl TouchpadLiveClient { + pub fn new() -> Self { + TouchpadLiveClient { + client: Client::new(), + } + } + + pub async fn meet_info(&self, id: u32) -> Result<v1::SwimMeet, TouchpadLiveError> { + let client1 = self.client.clone(); + let meet_info_handle = tokio::spawn(async move { + client1 + .get(format!("{}/meets/{}", BASE_URL, id)) + .send() + .await? + .json::<MeetInfoResponse>() + .await + }); + + let client2 = self.client.clone(); + let team_info_handle = tokio::spawn(async move { + client2 + .get(format!("{}/meets/{}/teams", BASE_URL, id)) + .send() + .await? + .json::<Vec<TeamInfoResponse>>() + .await + }); + + let (resp_results, teams_result) = future::join(meet_info_handle, team_info_handle).await; + + let resp = resp_results??; + let teams = teams_result??; + + Ok(v1::SwimMeet { + id, + meet_name: resp.meet_name, + start: Some(ProtoTimestamp::from_touchpad(&resp.start_date)?.into()), + end: Some(ProtoTimestamp::from_touchpad(&resp.end_date)?.into()), + teams: teams + .into_iter() + .map(|t| v1::Team { + id: t.team_id, + name: t.name, + }) + .collect(), + points: HashMap::new(), + }) + } + + pub async fn events(&self, id: u32) -> Result<Vec<v1::Event>, TouchpadLiveError> { + let events = get_events(self.client.clone(), id).await?; + + events.into_iter() + .map(|ev| Ok(v1::Event { + id: ev.id, + age_hi: ev.age_hi as u32, + age_lo: ev.age_lo as u32, + distance: ev.distance, + event: None, + event_num: ev.event_number.parse()?, + meet_id: id, + stroke: v1::Stroke::from(ev.stroke.as_str()) as i32, + })) + .collect() + } + + pub async fn swimmers(&self, id: u32) -> Result<Vec<v1::Swimmer>, TouchpadLiveError> { + let (swimmers, swimmer_map, events) = { + let swimmers = self + .client + .get(format!("{}/meets/{}/participants", BASE_URL, id)) + .send() + .await? + .json::<Vec<ParticipantResponse>>() + .map_err(|e| e.into()); + + let swimmer_map = self + .client + .get(format!("{}/meets/{}/swimmerEventMap", BASE_URL, id)) + .send() + .await? + .json::<HashMap<u32, Vec<u32>>>() + .map_err(|e| e.into()); + + let events = get_events(self.client.clone(), id); + + future::try_join3(swimmers, swimmer_map, events) + } + .await?; + + Ok(process_swimmers(swimmers, &swimmer_map, events)) + } +} + +async fn get_events(client: Client, id: u32) -> Result<Vec<EventsResponse>, TouchpadLiveError> { + Ok(client + .get(format!("{}/meets/{}/events", BASE_URL, id)) + .send() + .await? + .json() + .await?) +} + +fn events_to_map<I: IntoIterator<Item=EventsResponse>>(events: I) -> HashMap<u32, EventsResponse> { + events.into_iter().map(|ev| (ev.id, ev)).collect() +} + +fn process_swimmers( + swimmers: Vec<ParticipantResponse>, + swimmer_map: &HashMap<u32, Vec<u32>>, + events: Vec<EventsResponse>, +) -> Vec<v1::Swimmer> { + let event_map = events_to_map(events); + + let mut proto_swimmers = Vec::with_capacity(swimmers.len()); + for swimmer in swimmers { + let swimmer_events = swimmer_map.get(&swimmer.id); + if swimmer_events.is_none() { + continue; + } + + let mut gender = v1::Gender::Unspecified; + for event in swimmer_events.unwrap() { + let ev_info_result = event_map.get(event); + if let Some(ev_info) = ev_info_result { + gender = ev_info.gender.as_str().into(); + if gender != v1::Gender::Unspecified { + break; + } + } + } + + if gender == v1::Gender::Unspecified { + continue; + } else { + proto_swimmers.push(v1::Swimmer { + id: swimmer.id, + name: swimmer.name, + gender: gender as i32, + }); + } + } + + proto_swimmers +} diff --git a/rust/scraper/src/touchpad/request_response.rs b/rust/scraper/src/touchpad/request_response.rs new file mode 100644 index 0000000..67b8a0b --- /dev/null +++ b/rust/scraper/src/touchpad/request_response.rs @@ -0,0 +1,127 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MeetInfoResponse { + pub course_name: String, + pub course_order: String, + pub end_date: String, + pub events: i32, + pub females: i32, + pub host_team_name: String, + pub id: u32, + pub is_ended: bool, + pub is_ended_24_hrs: bool, + pub is_finals_meet: bool, + pub is_prelim_meet: bool, + pub is_started: bool, + pub localized_start_date: String, + pub localized_end_date: String, + pub males: i32, + pub meet_name: String, + pub start_date: String, + pub team_count: i32, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TeamInfoResponse { + pub name: String, + pub n_events: u32, + pub n_swimmers: u32, + pub points: f32, + #[serde(rename = "teamID")] + pub team_id: u32, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EventsResponse { + #[serde(rename = "age_hi")] + pub age_hi: u8, + #[serde(rename = "age_low")] + pub age_lo: u8, + pub age_group: String, + pub day: String, + pub distance: u32, + pub event_number: String, + pub gender: String, + pub id: u32, + pub relay: bool, + pub rounds: i32, + pub session: i32, + pub sponsor: String, + pub status: u8, + pub stroke: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EventTimeResponse { + pub age: Option<u8>, + #[serde(rename = "eventID")] + pub event_id: Option<u32>, + pub final_formatted_time: String, + pub finals_heat: u32, + pub finals_lane: u32, + pub finals_points: f32, + pub finals_rank: i32, + pub finals_time: u64, + pub first_name: Option<String>, + pub formatted_seed_time: String, + pub last_name: Option<String>, + pub seed_course: String, + pub seed_time: i64, + #[serde(rename = "seedTime2Compare")] + pub seed_time2compare: u64, + #[serde(rename = "swimmerID")] + pub swimmer_id: Option<u32>, + #[serde(rename = "teamID")] + pub team_id: u32, + pub name: Option<String>, +} + +impl EventTimeResponse { + pub fn into_proto(self, event_id: u32) -> proto::touchpad::common::v1::EventTime { + let time = core::time::Duration::from_millis(self.finals_time * 10); + + let seed = if self.seed_time >= 0 { + core::time::Duration::from_millis((self.seed_time as u64) * 10) + } else { + core::time::Duration::new(0, 0) + }; + + let result = { + let result = proto::touchpad::common::v1::EventTimeResult::from( + self.final_formatted_time.as_str(), + ); + + if result == proto::touchpad::common::v1::EventTimeResult::Unspecified + && self.finals_time > 0 + { + proto::touchpad::common::v1::EventTimeResult::Ok + } else { + result + } + }; + + proto::touchpad::common::v1::EventTime { + event_id, + team_id: self.team_id, + heat: self.finals_heat, + lane: self.finals_lane, + result: result as i32, + time: Some(time.into()), + seed: Some(seed.into()), + rank: self.finals_rank, + points: self.finals_points, + } + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ParticipantResponse { + pub id: u32, + pub name: String, +} |