summaryrefslogtreecommitdiff
path: root/rust/scraper/src
diff options
context:
space:
mode:
authorGravatar Anshul Gupta <ansg191@yahoo.com> 2022-08-15 15:24:06 -0700
committerGravatar Anshul Gupta <ansg191@yahoo.com> 2022-08-15 15:25:55 -0700
commitedc5985b1744dcffb8bf1b41ce273508573589fe (patch)
treeb57bf381454957e991ffde347cafae4e2d4d9deb /rust/scraper/src
parentb26ffd01b821047b692e5235e4f7f4f8f535f432 (diff)
downloadtouchpad-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.rs19
-rw-r--r--rust/scraper/src/touchpad/error.rs13
-rw-r--r--rust/scraper/src/touchpad/mod.rs168
-rw-r--r--rust/scraper/src/touchpad/request_response.rs127
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,
+}