diff options
author | 2022-08-15 15:24:06 -0700 | |
---|---|---|
committer | 2022-08-15 15:25:55 -0700 | |
commit | edc5985b1744dcffb8bf1b41ce273508573589fe (patch) | |
tree | b57bf381454957e991ffde347cafae4e2d4d9deb /rust/scraper/src | |
parent | b26ffd01b821047b692e5235e4f7f4f8f535f432 (diff) | |
download | touchpad-edc5985b1744dcffb8bf1b41ce273508573589fe.tar.gz touchpad-edc5985b1744dcffb8bf1b41ce273508573589fe.tar.zst touchpad-edc5985b1744dcffb8bf1b41ce273508573589fe.zip |
Adds rust touchpad scraper
Implements meet, events, and swimmers api in touchpad live. Also
implements protobuf generation into rust crate.
Diffstat (limited to 'rust/scraper/src')
-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 |
4 files changed, 327 insertions, 0 deletions
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, +} |