init or smth
This commit is contained in:
commit
e0b465eaa5
9 changed files with 2638 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.showUnlinkedFileNotification": false
|
||||
}
|
2354
Cargo.lock
generated
Normal file
2354
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "last_chance_stalker"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
spreadsheet-ods = "0.22.1"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_json = "1.0.114"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
scraper = "0.19.0"
|
||||
futures = "0.3.30"
|
1
src/auth.txt
Normal file
1
src/auth.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Basic MTA4MzkwMzg6NjA1NTRmZjY4Njg0NDhkNjRkYzJiNGIxM2UzZTRlNTI=
|
41
src/main.rs
Normal file
41
src/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
mod nsda;
|
||||
mod tabroom;
|
||||
mod types;
|
||||
|
||||
use std::pin::pin;
|
||||
|
||||
use futures::future;
|
||||
|
||||
use crate::nsda::NSDASearchResponse;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("Fetching entries...");
|
||||
|
||||
let entries = tabroom::fetch_entries(30061, 279207).await;
|
||||
println!("Fetched entries, searching with NSDA...");
|
||||
let mut searches = future::join_all(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| Box::pin(nsda::search_member(entry.entry))),
|
||||
)
|
||||
.await;
|
||||
for (search_results, entry) in searches.iter_mut().zip(entries.into_iter()) {
|
||||
if let NSDASearchResponse::Many(results) = search_results {
|
||||
let filtered = nsda::filter_search_results(
|
||||
*results,
|
||||
entry.location.replace("/US", ""),
|
||||
entry.institution,
|
||||
);
|
||||
match filtered {
|
||||
NSDASearchResponse::None => {
|
||||
eprintln!("Unable to narrow down search results for entry {}, none remained after school filter", entry.entry);
|
||||
std::process::exit(1);
|
||||
}
|
||||
NSDASearchResponse::Many(filtered) => {
|
||||
eprintln!("Unable to narrow down search results for entry {}, multiple were left after school fitler: {}", entry.entry, results.into_iter().map(|r| r.id.to_string()).collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
src/nsda.rs
Normal file
58
src/nsda.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use crate::types::NSDAMemberSearchResult;
|
||||
|
||||
const NSDA_AUTH: &str = include_str!("auth.txt");
|
||||
|
||||
pub enum NSDASearchResponse {
|
||||
One(NSDAMemberSearchResult),
|
||||
Many(Vec<NSDAMemberSearchResult>),
|
||||
None,
|
||||
}
|
||||
|
||||
pub async fn search_member<S: AsRef<str>>(query: S) -> NSDASearchResponse {
|
||||
let mut results = reqwest::Client::new()
|
||||
.get("https://api.speechanddebate.org/v2/search")
|
||||
.query(&[("q", query.as_ref()), ("type", "members")])
|
||||
.header("Authorization", NSDA_AUTH)
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Vec<NSDAMemberSearchResult>>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match results.len() {
|
||||
0 => NSDASearchResponse::None,
|
||||
1 => NSDASearchResponse::One(results.remove(0)),
|
||||
_ => NSDASearchResponse::Many(results),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_search_results<S: AsRef<str>>(
|
||||
mut results: Vec<NSDAMemberSearchResult>,
|
||||
state: S,
|
||||
school: S,
|
||||
) -> NSDASearchResponse {
|
||||
// First, filter by HS vs. MS results and stats==1 (which appears to be true for non-duplicates)
|
||||
results.retain(|result| result.realm == "hs" && result.status == 1);
|
||||
if results.len() == 1 {
|
||||
return NSDASearchResponse::One(results.remove(0));
|
||||
}
|
||||
// If that doesn't work, filter by state
|
||||
results.retain(|result| &result.school_state == state.as_ref());
|
||||
if results.len() == 1 {
|
||||
return NSDASearchResponse::One(results.remove(0));
|
||||
}
|
||||
// And finally, as a last case scenario
|
||||
results.retain(|result| {
|
||||
result
|
||||
.school_name
|
||||
.to_lowercase()
|
||||
.contains(&school.as_ref().to_lowercase())
|
||||
});
|
||||
match results.len() {
|
||||
0 => NSDASearchResponse::None,
|
||||
1 => NSDASearchResponse::One(results.remove(0)),
|
||||
_ => NSDASearchResponse::Many(results),
|
||||
}
|
||||
}
|
30
src/tabroom.rs
Normal file
30
src/tabroom.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use crate::types::TabroomEntry;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
pub async fn fetch_entries(tournament_id: usize, event_id: usize) -> Vec<TabroomEntry> {
|
||||
let html = reqwest::Client::new()
|
||||
.get("https://www.tabroom.com/index/tourn/fields.mhtml")
|
||||
.query(&[("tourn_id", tournament_id), ("event_id", event_id)])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let parsed = Html::parse_document(&html);
|
||||
let selector = Selector::parse("tr").unwrap();
|
||||
|
||||
Vec::from_iter(
|
||||
parsed.select(&selector).map(|row| {
|
||||
let mut children = row.child_elements();
|
||||
|
||||
TabroomEntry {
|
||||
institution: children.next().unwrap().inner_html().trim().to_string(),
|
||||
location: children.next().unwrap().inner_html().trim().to_string(),
|
||||
entry: children.next().unwrap().inner_html().trim().to_string(),
|
||||
code: children.next().unwrap().inner_html().trim().to_string(),
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
135
src/types.rs
Normal file
135
src/types.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
/// A USX entry on tabroom
|
||||
#[derive(Debug)]
|
||||
pub struct TabroomEntry {
|
||||
pub institution: String,
|
||||
pub location: String,
|
||||
pub entry: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NSDAMemberSearchResult {
|
||||
pub id: usize,
|
||||
pub first: String,
|
||||
pub last: String,
|
||||
pub role: String,
|
||||
pub school_id: usize,
|
||||
pub status: usize,
|
||||
pub realm: String,
|
||||
pub school_name: String,
|
||||
pub school_state: String,
|
||||
pub r#type: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NSDAMember {
|
||||
pub person_id: usize,
|
||||
pub first: String,
|
||||
pub middle: Option<String>,
|
||||
pub last: String,
|
||||
pub disabled: usize,
|
||||
pub created_at: String,
|
||||
pub points: usize,
|
||||
pub points_service: usize,
|
||||
pub points_service_this_year: usize,
|
||||
pub points_this_year: usize,
|
||||
pub points_last_year: usize,
|
||||
pub last_points_entry: String,
|
||||
pub citation_points: usize,
|
||||
pub degree_id: usize,
|
||||
pub degree_name: String,
|
||||
pub to_next_degree: usize,
|
||||
pub diamonds: Option<usize>,
|
||||
#[serde(rename = "3_diamond")]
|
||||
pub n3_diamond: bool,
|
||||
pub hof: bool,
|
||||
pub aaa: bool,
|
||||
pub active: bool,
|
||||
pub paid: bool,
|
||||
pub paid_latest: String,
|
||||
pub school_paid: bool,
|
||||
pub active_student: bool,
|
||||
pub degrees: Vec<Degree>,
|
||||
pub honors: Vec<Honor>,
|
||||
pub citations: Vec<Value>,
|
||||
pub districts_eligible: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Degree {
|
||||
pub member_honor_id: usize,
|
||||
pub person_id: usize,
|
||||
pub honor_id: usize,
|
||||
pub description: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: String,
|
||||
pub school_id: usize,
|
||||
pub school_name: String,
|
||||
pub district_id: Option<String>,
|
||||
pub district_name: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub points: usize,
|
||||
pub start: String,
|
||||
pub end: Option<String>,
|
||||
pub status: String,
|
||||
pub recognized: Value,
|
||||
pub note: Option<String>,
|
||||
pub created_at: String,
|
||||
pub created_by_id: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Honor {
|
||||
pub member_honor_id: usize,
|
||||
pub person_id: usize,
|
||||
pub honor_id: usize,
|
||||
pub description: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: String,
|
||||
pub school_id: usize,
|
||||
pub school_name: String,
|
||||
pub district_id: Option<usize>,
|
||||
pub district_name: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub points: Option<usize>,
|
||||
pub start: String,
|
||||
pub end: Option<String>,
|
||||
pub status: String,
|
||||
pub recognized: Value,
|
||||
pub note: Option<String>,
|
||||
pub created_at: String,
|
||||
pub created_by_id: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NSDAMemberPointEntry {
|
||||
pub id: usize,
|
||||
pub student_id: usize,
|
||||
pub student_name: String,
|
||||
pub coach_id: usize,
|
||||
pub coach_name: String,
|
||||
pub school_id: usize,
|
||||
pub category_id: usize,
|
||||
pub points: usize,
|
||||
pub result: String,
|
||||
pub description: Option<String>,
|
||||
pub autopost: bool,
|
||||
pub districts: bool,
|
||||
pub nationals: bool,
|
||||
pub tourn_id: usize,
|
||||
pub tourn_name: String,
|
||||
pub location: String,
|
||||
pub state: String,
|
||||
pub start: String,
|
||||
pub end: String,
|
||||
pub source: String,
|
||||
pub status: usize,
|
||||
pub created_at: String,
|
||||
pub created_by_id: Option<usize>,
|
||||
pub created_by: String,
|
||||
}
|
Loading…
Reference in a new issue