init or smth
This commit is contained in:
commit
1db4251825
10 changed files with 2836 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
src/auth.txt
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||||
|
"rust-analyzer.check.command": "clippy"
|
||||||
|
}
|
2429
Cargo.lock
generated
Normal file
2429
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[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.12.0", 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"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
icu_locid = "1.4.0"
|
||||||
|
chrono = "0.4.35"
|
BIN
out.ods
Normal file
BIN
out.ods
Normal file
Binary file not shown.
71
src/main.rs
Normal file
71
src/main.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
mod nsda;
|
||||||
|
mod tabroom;
|
||||||
|
mod types;
|
||||||
|
mod spreadsheet;
|
||||||
|
|
||||||
|
use futures::future;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use crate::{nsda::NSDASearchResponse, types::EntryData};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref CLIENT: reqwest::Client = reqwest::Client::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// println!("Fetching entries...");
|
||||||
|
|
||||||
|
// let entries = tabroom::fetch_entries(30061, 279207).await;
|
||||||
|
// println!("Fetched entries, searching with NSDA...");
|
||||||
|
// let searches = future::join_all(
|
||||||
|
// entries
|
||||||
|
// .iter()
|
||||||
|
// .map(|entry| Box::pin(nsda::search_member(&entry.entry))),
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
// // If many were found, filter, if one was found, use it, if none were found, quietly error
|
||||||
|
// let filtered = searches.into_iter().zip(entries.into_iter()).filter_map(|(search_results, entry)| {
|
||||||
|
// match search_results {
|
||||||
|
// NSDASearchResponse::Many(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 filtering", entry.entry);
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
// NSDASearchResponse::Many(filtered) => {
|
||||||
|
// eprintln!("Unable to narrow down search results for entry {}, multiple were left after fitlering: {}", entry.entry, filtered.into_iter().map(|r| r.id.to_string()).collect::<Vec<_>>().join(", "));
|
||||||
|
// None
|
||||||
|
// },
|
||||||
|
// NSDASearchResponse::One(result) => {
|
||||||
|
// Some((entry, *result))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// NSDASearchResponse::None => {
|
||||||
|
// eprintln!("Unable to find any search results for entry {} without filtering", entry.entry);
|
||||||
|
// None
|
||||||
|
// },
|
||||||
|
// NSDASearchResponse::One(result) => Some((entry, *result))
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// println!("Searching done, looking up member information and point records...");
|
||||||
|
// let queried = future::join_all(filtered.map(|(entry, search_result)| async {
|
||||||
|
// EntryData {
|
||||||
|
// member: nsda::fetch_member(search_result.id).await,
|
||||||
|
// points: nsda::fetch_member_points(search_result.id).await,
|
||||||
|
// entry,
|
||||||
|
// search_result,
|
||||||
|
// }
|
||||||
|
// }))
|
||||||
|
// .await;
|
||||||
|
|
||||||
|
// Format to spreadsheet or shit
|
||||||
|
let mut workbook = spreadsheet::generate_spreadsheet(/*queried*/);
|
||||||
|
spreadsheet_ods::write_ods(&mut workbook, "./out.ods").unwrap();
|
||||||
|
}
|
89
src/nsda.rs
Normal file
89
src/nsda.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use crate::{
|
||||||
|
types::{NSDAMember, NSDAMemberPointEntry, NSDAMemberSearchResult},
|
||||||
|
CLIENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NSDA_AUTH: &str = include_str!("auth.txt");
|
||||||
|
|
||||||
|
pub enum NSDASearchResponse {
|
||||||
|
One(Box<NSDAMemberSearchResult>),
|
||||||
|
Many(Vec<NSDAMemberSearchResult>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_member<S: AsRef<str>>(query: S) -> NSDASearchResponse {
|
||||||
|
let mut results = CLIENT
|
||||||
|
.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_or_else(|_| { panic!("{}", query.as_ref().to_string()) });
|
||||||
|
|
||||||
|
match results.len() {
|
||||||
|
0 => NSDASearchResponse::None,
|
||||||
|
1 => NSDASearchResponse::One(Box::new(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 == Some("hs".to_string()) && result.status == Some(1));
|
||||||
|
if results.len() == 1 {
|
||||||
|
return NSDASearchResponse::One(Box::new(results.remove(0)));
|
||||||
|
}
|
||||||
|
// If that doesn't work, filter by state
|
||||||
|
results.retain(|result| result.school_state.as_ref().is_some_and(|s| s == state.as_ref()));
|
||||||
|
if results.len() == 1 {
|
||||||
|
return NSDASearchResponse::One(Box::new(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(Box::new(results.remove(0))),
|
||||||
|
_ => NSDASearchResponse::Many(results),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_member(id: usize) -> NSDAMember {
|
||||||
|
CLIENT
|
||||||
|
.get(format!("https://api.speechanddebate.org/v2/members/{id}"))
|
||||||
|
.header("Authorization", NSDA_AUTH)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<NSDAMember>()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_member_points(id: usize) -> Vec<NSDAMemberPointEntry> {
|
||||||
|
CLIENT
|
||||||
|
.get(format!(
|
||||||
|
"https://api.speechanddebate.org/v2/members/{id}/points"
|
||||||
|
))
|
||||||
|
.header("Authorization", NSDA_AUTH)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<Vec<NSDAMemberPointEntry>>()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| { panic!("{}", id.to_string()) })
|
||||||
|
}
|
52
src/spreadsheet.rs
Normal file
52
src/spreadsheet.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use crate::types::EntryData;
|
||||||
|
use chrono::Utc;
|
||||||
|
use icu_locid::locale;
|
||||||
|
use spreadsheet_ods::{defaultstyles::DefaultFormat, format::ValueFormatTrait, style::units::{FontSize, WrapOption}, CellStyle, Length, Sheet, Value, ValueFormatDateTime, WorkBook};
|
||||||
|
|
||||||
|
pub fn generate_spreadsheet(/*data: Vec<EntryData>*/) -> WorkBook {
|
||||||
|
let mut workbook = WorkBook::new(locale!("en_US"));
|
||||||
|
|
||||||
|
// Setup styles
|
||||||
|
let mut large_style = CellStyle::new("largestyle", &DefaultFormat::default());
|
||||||
|
large_style.set_font_bold();
|
||||||
|
large_style.set_font_size(FontSize::Length(spreadsheet_ods::Length::Pt(16f64)));
|
||||||
|
large_style.set_wrap_option(WrapOption::Wrap);
|
||||||
|
let large_style_ref = large_style.style_ref();
|
||||||
|
workbook.add_cellstyle(large_style);
|
||||||
|
|
||||||
|
let mut bold_style = CellStyle::new("boldstyle", &DefaultFormat::default());
|
||||||
|
bold_style.set_font_bold();
|
||||||
|
bold_style.set_wrap_option(WrapOption::Wrap);
|
||||||
|
let bold_style_ref = bold_style.style_ref();
|
||||||
|
workbook.add_cellstyle(bold_style);
|
||||||
|
|
||||||
|
// let mut datetime_value_format = ValueFormatDateTime::new_named("datetimevalueformat");
|
||||||
|
// datetime_value_format.part_day_of_week().build();
|
||||||
|
// datetime_value_format.part_text(" ").build();
|
||||||
|
// datetime_value_format.part_month().build();
|
||||||
|
// datetime_value_format.part_text("/").build();
|
||||||
|
// datetime_value_format.part_day().build();
|
||||||
|
// let datetime_value_format_ref = datetime_value_format.format_ref();
|
||||||
|
// workbook.add_datetime_format(datetime_value_format);
|
||||||
|
// let datetime_format = CellStyle::new("datetimeformat", &datetime_value_format_ref);
|
||||||
|
// let datetime_format_ref = datetime_format.style_ref();
|
||||||
|
let mut format = workbook.datetime_format("datetime1").unwrap().clone();
|
||||||
|
format.part_text("hi").build();
|
||||||
|
workbook.remove_datetime_format("datetime1");
|
||||||
|
workbook.add_datetime_format(format);
|
||||||
|
|
||||||
|
|
||||||
|
// Main info spreadsheet
|
||||||
|
let mut info = Sheet::new("Information");
|
||||||
|
info.set_col_width(0, Length::In(4f64));
|
||||||
|
info.set_styled_value(0, 0, "This data is fetched and calculated automatically, though updates must be entered into google sheets manually (for now? who knows)", &large_style_ref);
|
||||||
|
info.set_value(1, 0, "If its outdated yell at Tyler or smth");
|
||||||
|
info.set_value(2, 0, "You can see the data by switching sheets below.");
|
||||||
|
info.set_styled_value(0, 1, "Last update:", &bold_style_ref);
|
||||||
|
info.set_value(1, 1, Utc::now().naive_utc());
|
||||||
|
|
||||||
|
// Add all sheets together
|
||||||
|
workbook.push_sheet(info);
|
||||||
|
|
||||||
|
workbook
|
||||||
|
}
|
28
src/tabroom.rs
Normal file
28
src/tabroom.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use crate::{types::TabroomEntry, CLIENT};
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
pub async fn fetch_entries(tournament_id: usize, event_id: usize) -> Vec<TabroomEntry> {
|
||||||
|
let html = CLIENT
|
||||||
|
.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(),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
143
src/types.rs
Normal file
143
src/types.rs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EntryData {
|
||||||
|
pub entry: TabroomEntry,
|
||||||
|
pub search_result: NSDAMemberSearchResult,
|
||||||
|
pub member: NSDAMember,
|
||||||
|
pub points: Vec<NSDAMemberPointEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A USX entry on tabroom
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TabroomEntry {
|
||||||
|
pub institution: String,
|
||||||
|
pub location: String,
|
||||||
|
pub entry: String,
|
||||||
|
pub code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NSDAMemberSearchResult {
|
||||||
|
pub id: usize,
|
||||||
|
pub first: String,
|
||||||
|
pub last: String,
|
||||||
|
pub role: Option<String>,
|
||||||
|
pub school_id: Option<usize>,
|
||||||
|
pub status: Option<usize>,
|
||||||
|
pub realm: Option<String>,
|
||||||
|
pub school_name: String,
|
||||||
|
pub school_state: Option<String>,
|
||||||
|
pub r#type: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, 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(Debug, 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(Debug, 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(Debug, Deserialize)]
|
||||||
|
pub struct NSDAMemberPointEntry {
|
||||||
|
pub id: usize,
|
||||||
|
pub student_id: usize,
|
||||||
|
pub student_name: String,
|
||||||
|
pub coach_id: Option<usize>,
|
||||||
|
pub coach_name: Option<String>,
|
||||||
|
pub school_id: usize,
|
||||||
|
pub category_id: usize,
|
||||||
|
pub points: usize,
|
||||||
|
pub result: Option<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