mirror of
https://github.com/Xevion/banner.git
synced 2025-12-15 12:11:11 -06:00
chore: solve lints, improve formatting
This commit is contained in:
@@ -5,7 +5,6 @@ use crate::banner::Course;
|
||||
use anyhow::Result;
|
||||
use redis::AsyncCommands;
|
||||
use redis::Client;
|
||||
use serde_json;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -31,7 +30,7 @@ impl AppState {
|
||||
pub async fn get_course_or_fetch(&self, term: &str, crn: &str) -> Result<Course> {
|
||||
let mut conn = self.redis.get_multiplexed_async_connection().await?;
|
||||
|
||||
let key = format!("class:{}", crn);
|
||||
let key = format!("class:{crn}");
|
||||
if let Some(serialized) = conn.get::<_, Option<String>>(&key).await? {
|
||||
let course: Course = serde_json::from_str(&serialized)?;
|
||||
return Ok(course);
|
||||
@@ -44,6 +43,6 @@ impl AppState {
|
||||
return Ok(course);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Course not found for CRN {}", crn))
|
||||
Err(anyhow::anyhow!("Course not found for CRN {crn}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ impl BannerApi {
|
||||
("searchTerm", search),
|
||||
("offset", &page.to_string()),
|
||||
("max", &max_results.to_string()),
|
||||
("_", ×tamp_nonce()),
|
||||
("_", &SessionManager::nonce()),
|
||||
];
|
||||
|
||||
let response = self
|
||||
@@ -104,7 +104,7 @@ impl BannerApi {
|
||||
("offset", &offset.to_string()),
|
||||
("max", &max_results.to_string()),
|
||||
("uniqueSessionId", &session_id),
|
||||
("_", ×tamp_nonce()),
|
||||
("_", &SessionManager::nonce()),
|
||||
];
|
||||
|
||||
let response = self
|
||||
@@ -143,7 +143,7 @@ impl BannerApi {
|
||||
("offset", &offset.to_string()),
|
||||
("max", &max_results.to_string()),
|
||||
("uniqueSessionId", &session_id),
|
||||
("_", ×tamp_nonce()),
|
||||
("_", &SessionManager::nonce()),
|
||||
];
|
||||
|
||||
let response = self
|
||||
@@ -182,7 +182,7 @@ impl BannerApi {
|
||||
("offset", &offset.to_string()),
|
||||
("max", &max_results.to_string()),
|
||||
("uniqueSessionId", &session_id),
|
||||
("_", ×tamp_nonce()),
|
||||
("_", &SessionManager::nonce()),
|
||||
];
|
||||
|
||||
let response = self
|
||||
@@ -382,15 +382,6 @@ impl BannerApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a timestamp-based nonce.
|
||||
fn timestamp_nonce() -> String {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Returns a browser-like user agent string.
|
||||
fn user_agent() -> &'static str {
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
|
||||
@@ -402,13 +393,9 @@ fn parse_json_with_context<T: serde::de::DeserializeOwned>(body: &str) -> Result
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => {
|
||||
let (line, column) = (err.line(), err.column());
|
||||
let snippet = build_error_snippet(body, line as usize, column as usize, 120);
|
||||
let snippet = build_error_snippet(body, line, column, 120);
|
||||
Err(anyhow::anyhow!(
|
||||
"{} at line {}, column {}\nSnippet:\n{}",
|
||||
err,
|
||||
line,
|
||||
column,
|
||||
snippet
|
||||
"{err} at line {line}, column {column}\nSnippet:\n{snippet}",
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -430,5 +417,5 @@ fn build_error_snippet(body: &str, line: usize, column: usize, max_len: usize) -
|
||||
indicator.push('^');
|
||||
}
|
||||
|
||||
format!("{}\n{}", slice, indicator)
|
||||
format!("{slice}\n{indicator}")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bitflags::{Flags, bitflags};
|
||||
use chrono::{DateTime, NaiveDate, NaiveTime, Timelike, Utc};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::{cmp::Ordering, str::FromStr};
|
||||
use std::{cmp::Ordering, fmt::Display, str::FromStr};
|
||||
|
||||
use super::terms::Term;
|
||||
|
||||
@@ -148,30 +148,17 @@ pub enum DayOfWeek {
|
||||
|
||||
impl DayOfWeek {
|
||||
/// Convert to short string representation
|
||||
pub fn to_short_string(&self) -> &'static str {
|
||||
pub fn to_short_string(self) -> &'static str {
|
||||
match self {
|
||||
DayOfWeek::Monday => "M",
|
||||
DayOfWeek::Monday => "Mo",
|
||||
DayOfWeek::Tuesday => "Tu",
|
||||
DayOfWeek::Wednesday => "W",
|
||||
DayOfWeek::Wednesday => "We",
|
||||
DayOfWeek::Thursday => "Th",
|
||||
DayOfWeek::Friday => "F",
|
||||
DayOfWeek::Friday => "Fr",
|
||||
DayOfWeek::Saturday => "Sa",
|
||||
DayOfWeek::Sunday => "Su",
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to full string representation
|
||||
pub fn to_string(&self) -> &'static str {
|
||||
match self {
|
||||
DayOfWeek::Monday => "Monday",
|
||||
DayOfWeek::Tuesday => "Tuesday",
|
||||
DayOfWeek::Wednesday => "Wednesday",
|
||||
DayOfWeek::Thursday => "Thursday",
|
||||
DayOfWeek::Friday => "Friday",
|
||||
DayOfWeek::Saturday => "Saturday",
|
||||
DayOfWeek::Sunday => "Sunday",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MeetingDays> for DayOfWeek {
|
||||
@@ -196,10 +183,9 @@ impl TryFrom<MeetingDays> for DayOfWeek {
|
||||
});
|
||||
}
|
||||
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot convert multiple days to a single day: {:?}",
|
||||
days
|
||||
));
|
||||
Err(anyhow::anyhow!(
|
||||
"Cannot convert multiple days to a single day: {days:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,15 +238,8 @@ impl TimeRange {
|
||||
let hour = time.hour();
|
||||
let minute = time.minute();
|
||||
|
||||
if hour == 0 {
|
||||
format!("12:{:02}AM", minute)
|
||||
} else if hour < 12 {
|
||||
format!("{}:{:02}AM", hour, minute)
|
||||
} else if hour == 12 {
|
||||
format!("12:{:02}PM", minute)
|
||||
} else {
|
||||
format!("{}:{:02}PM", hour - 12, minute)
|
||||
}
|
||||
let meridiem = if hour < 12 { "AM" } else { "PM" };
|
||||
format!("{hour}:{minute:02}{meridiem}")
|
||||
}
|
||||
|
||||
/// Get duration in minutes
|
||||
@@ -376,15 +355,20 @@ impl MeetingLocation {
|
||||
is_online,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to formatted string
|
||||
pub fn to_string(&self) -> String {
|
||||
impl Display for MeetingLocation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.is_online {
|
||||
"Online".to_string()
|
||||
write!(f, "Online")
|
||||
} else {
|
||||
format!(
|
||||
"{} | {} | {} {}",
|
||||
self.campus, self.building_description, self.building, self.room
|
||||
write!(
|
||||
f,
|
||||
"{campus} | {building_name} | {building_code} {room}",
|
||||
campus = self.campus,
|
||||
building_name = self.building_description,
|
||||
building_code = self.building,
|
||||
room = self.room
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Term {
|
||||
/// Returns the current term status for a specific date
|
||||
pub fn get_status_for_date(date: NaiveDate) -> TermPoint {
|
||||
let literal_year = date.year() as u32;
|
||||
let day_of_year = date.ordinal() as u32;
|
||||
let day_of_year = date.ordinal();
|
||||
let ranges = Self::get_season_ranges(literal_year);
|
||||
|
||||
// If we're past the end of the summer term, we're 'in' the next school year.
|
||||
@@ -115,22 +115,22 @@ impl Term {
|
||||
fn get_season_ranges(year: u32) -> SeasonRanges {
|
||||
let spring_start = NaiveDate::from_ymd_opt(year as i32, 1, 14)
|
||||
.unwrap()
|
||||
.ordinal() as u32;
|
||||
.ordinal();
|
||||
let spring_end = NaiveDate::from_ymd_opt(year as i32, 5, 1)
|
||||
.unwrap()
|
||||
.ordinal() as u32;
|
||||
.ordinal();
|
||||
let summer_start = NaiveDate::from_ymd_opt(year as i32, 5, 25)
|
||||
.unwrap()
|
||||
.ordinal() as u32;
|
||||
.ordinal();
|
||||
let summer_end = NaiveDate::from_ymd_opt(year as i32, 8, 15)
|
||||
.unwrap()
|
||||
.ordinal() as u32;
|
||||
.ordinal();
|
||||
let fall_start = NaiveDate::from_ymd_opt(year as i32, 8, 18)
|
||||
.unwrap()
|
||||
.ordinal() as u32;
|
||||
.ordinal();
|
||||
let fall_end = NaiveDate::from_ymd_opt(year as i32, 12, 10)
|
||||
.unwrap()
|
||||
.ordinal() as u32;
|
||||
.ordinal();
|
||||
|
||||
SeasonRanges {
|
||||
spring: YearDayRange {
|
||||
@@ -179,10 +179,15 @@ struct YearDayRange {
|
||||
end: u32,
|
||||
}
|
||||
|
||||
impl ToString for Term {
|
||||
impl std::fmt::Display for Term {
|
||||
/// Returns the term in the format YYYYXX, where YYYY is the year and XX is the season code
|
||||
fn to_string(&self) -> String {
|
||||
format!("{}{}", self.year, self.season.to_str())
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{year}{season}",
|
||||
year = self.year,
|
||||
season = self.season.to_str()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +220,7 @@ impl FromStr for Season {
|
||||
"10" => Season::Fall,
|
||||
"20" => Season::Spring,
|
||||
"30" => Season::Summer,
|
||||
_ => return Err(anyhow::anyhow!("Invalid season: {}", s)),
|
||||
_ => return Err(anyhow::anyhow!("Invalid season: {s}")),
|
||||
};
|
||||
Ok(season)
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ impl std::fmt::Display for SearchQuery {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if let Some(ref subject) = self.subject {
|
||||
parts.push(format!("subject={}", subject));
|
||||
parts.push(format!("subject={subject}"));
|
||||
}
|
||||
if let Some(ref title) = self.title {
|
||||
parts.push(format!("title={}", title.trim()));
|
||||
@@ -296,21 +296,21 @@ impl std::fmt::Display for SearchQuery {
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
parts.push(format!("instructor={}", instructor_str));
|
||||
parts.push(format!("instructor={instructor_str}"));
|
||||
}
|
||||
if let Some(start_time) = self.start_time {
|
||||
let (hour, minute, meridiem) = format_time_parameter(start_time);
|
||||
parts.push(format!("startTime={}:{}:{}", hour, minute, meridiem));
|
||||
parts.push(format!("startTime={hour}:{minute}:{meridiem}"));
|
||||
}
|
||||
if let Some(end_time) = self.end_time {
|
||||
let (hour, minute, meridiem) = format_time_parameter(end_time);
|
||||
parts.push(format!("endTime={}:{}:{}", hour, minute, meridiem));
|
||||
parts.push(format!("endTime={hour}:{minute}:{meridiem}"));
|
||||
}
|
||||
if let Some(min_credits) = self.min_credits {
|
||||
parts.push(format!("minCredits={}", min_credits));
|
||||
parts.push(format!("minCredits={min_credits}"));
|
||||
}
|
||||
if let Some(max_credits) = self.max_credits {
|
||||
parts.push(format!("maxCredits={}", max_credits));
|
||||
parts.push(format!("maxCredits={max_credits}"));
|
||||
}
|
||||
if let Some(ref range) = self.course_number_range {
|
||||
parts.push(format!("courseNumberRange={}-{}", range.low, range.high));
|
||||
|
||||
@@ -39,7 +39,7 @@ impl CourseScraper {
|
||||
.context("Failed to get subjects for scraping")?;
|
||||
|
||||
if subjects.is_empty() {
|
||||
return Err(anyhow::anyhow!("No subjects found for term {}", term));
|
||||
return Err(anyhow::anyhow!("no subjects found for term {term}"));
|
||||
}
|
||||
|
||||
// Categorize subjects
|
||||
@@ -58,15 +58,17 @@ impl CourseScraper {
|
||||
}
|
||||
|
||||
info!(
|
||||
"scraping {} subjects for term {}",
|
||||
expired_subjects.len(),
|
||||
term
|
||||
"scraping {count} subjects for term {term}",
|
||||
count = expired_subjects.len()
|
||||
);
|
||||
|
||||
// Scrape each expired subject
|
||||
for subject in expired_subjects {
|
||||
if let Err(e) = self.scrape_subject(&subject.code, term).await {
|
||||
error!("failed to scrape subject {}: {}", subject.code, e);
|
||||
error!(
|
||||
"failed to scrape subject {subject}: {e}",
|
||||
subject = subject.code
|
||||
);
|
||||
}
|
||||
|
||||
// Rate limiting between subjects
|
||||
@@ -87,7 +89,7 @@ impl CourseScraper {
|
||||
let mut expired = Vec::new();
|
||||
|
||||
for subject in subjects {
|
||||
let key = format!("scraped:{}:{}", subject.code, term);
|
||||
let key = format!("scraped:{code}:{term}", code = subject.code);
|
||||
let scraped: Option<String> = conn
|
||||
.get(&key)
|
||||
.await
|
||||
@@ -121,16 +123,12 @@ impl CourseScraper {
|
||||
.search(term, &query, "subjectDescription", false)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to search for subject {} at offset {}",
|
||||
subject, offset
|
||||
)
|
||||
format!("failed to search for subject {subject} at offset {offset}")
|
||||
})?;
|
||||
|
||||
if !result.success {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Search marked unsuccessful for subject {}",
|
||||
subject
|
||||
"search marked unsuccessful for subject {subject}"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -138,16 +136,16 @@ impl CourseScraper {
|
||||
total_courses += course_count;
|
||||
|
||||
debug!(
|
||||
"retrieved {} courses for subject {} at offset {}",
|
||||
course_count, subject, offset
|
||||
"retrieved {count} courses for subject {subject} at offset {offset}",
|
||||
count = course_count
|
||||
);
|
||||
|
||||
// Store each course in Redis
|
||||
for course in result.data.unwrap_or_default() {
|
||||
if let Err(e) = self.store_course(&course).await {
|
||||
error!(
|
||||
"failed to store course {}: {}",
|
||||
course.course_reference_number, e
|
||||
"failed to store course {crn}: {e}",
|
||||
crn = course.course_reference_number
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -156,16 +154,14 @@ impl CourseScraper {
|
||||
if course_count >= MAX_PAGE_SIZE {
|
||||
if course_count > MAX_PAGE_SIZE {
|
||||
warn!(
|
||||
"course count {} exceeds max page size {}",
|
||||
course_count, MAX_PAGE_SIZE
|
||||
"course count {count} exceeds max page size {max_page_size}",
|
||||
count = course_count,
|
||||
max_page_size = MAX_PAGE_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
offset += MAX_PAGE_SIZE;
|
||||
debug!(
|
||||
"continuing to next page for subject {} at offset {}",
|
||||
subject, offset
|
||||
);
|
||||
debug!("continuing to next page for subject {subject} at offset {offset}");
|
||||
|
||||
// Rate limiting between pages
|
||||
time::sleep(Duration::from_secs(3)).await;
|
||||
@@ -176,8 +172,8 @@ impl CourseScraper {
|
||||
}
|
||||
|
||||
info!(
|
||||
"scraped {} total courses for subject {}",
|
||||
total_courses, subject
|
||||
"scraped {count} total courses for subject {subject}",
|
||||
count = total_courses
|
||||
);
|
||||
|
||||
// Mark subject as scraped with expiry
|
||||
@@ -195,7 +191,7 @@ impl CourseScraper {
|
||||
.await
|
||||
.context("Failed to get Redis connection")?;
|
||||
|
||||
let key = format!("class:{}", course.course_reference_number);
|
||||
let key = format!("class:{crn}", crn = course.course_reference_number);
|
||||
let serialized = serde_json::to_string(course).context("Failed to serialize course")?;
|
||||
|
||||
let _: () = conn
|
||||
@@ -219,19 +215,21 @@ impl CourseScraper {
|
||||
.await
|
||||
.context("Failed to get Redis connection")?;
|
||||
|
||||
let key = format!("scraped:{}:{}", subject, term);
|
||||
let key = format!("scraped:{subject}:{term}", subject = subject);
|
||||
let expiry = self.calculate_expiry(subject, course_count);
|
||||
|
||||
let value = if course_count == 0 { -1 } else { course_count };
|
||||
|
||||
let _: () = conn
|
||||
.set_ex(&key, value, expiry.as_secs() as u64)
|
||||
.set_ex(&key, value, expiry.as_secs())
|
||||
.await
|
||||
.context("Failed to mark subject as scraped")?;
|
||||
|
||||
debug!(
|
||||
"marked subject {} as scraped with {} courses, expiry: {:?}",
|
||||
subject, course_count, expiry
|
||||
"marked subject {subject} as scraped with {count} courses, expiry: {expiry:?}",
|
||||
subject = subject,
|
||||
count = course_count,
|
||||
expiry = expiry
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -251,7 +249,7 @@ impl CourseScraper {
|
||||
|
||||
// Priority subjects get shorter expiry (more frequent updates)
|
||||
if PRIORITY_MAJORS.contains(&subject) {
|
||||
base_expiry = base_expiry / 3;
|
||||
base_expiry /= 3;
|
||||
}
|
||||
|
||||
// Add random variance (±15%)
|
||||
@@ -276,7 +274,7 @@ impl CourseScraper {
|
||||
.await
|
||||
.context("Failed to get Redis connection")?;
|
||||
|
||||
let key = format!("class:{}", crn);
|
||||
let key = format!("class:{crn}");
|
||||
let serialized: Option<String> = conn
|
||||
.get(&key)
|
||||
.await
|
||||
|
||||
@@ -38,16 +38,16 @@ impl SessionManager {
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut session_guard = self.current_session.lock().unwrap();
|
||||
|
||||
if let Some(ref session) = *session_guard {
|
||||
if session.created_at.elapsed() < Self::SESSION_EXPIRY {
|
||||
let elapsed = start_time.elapsed();
|
||||
debug!(
|
||||
session_id = session.session_id,
|
||||
elapsed = format!("{:.2?}", elapsed),
|
||||
"reusing existing banner session"
|
||||
);
|
||||
return Ok(session.session_id.clone());
|
||||
}
|
||||
if let Some(ref session) = *session_guard
|
||||
&& session.created_at.elapsed() < Self::SESSION_EXPIRY
|
||||
{
|
||||
let elapsed = start_time.elapsed();
|
||||
debug!(
|
||||
session_id = session.session_id,
|
||||
elapsed = format!("{:.2?}", elapsed),
|
||||
"reusing existing banner session"
|
||||
);
|
||||
return Ok(session.session_id.clone());
|
||||
}
|
||||
|
||||
// Generate new session
|
||||
@@ -58,7 +58,7 @@ impl SessionManager {
|
||||
});
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
info!(
|
||||
debug!(
|
||||
session_id = session_id,
|
||||
elapsed = format!("{:.2?}", elapsed),
|
||||
"generated new banner session"
|
||||
@@ -87,7 +87,7 @@ impl SessionManager {
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.query(&[("_", timestamp_nonce())])
|
||||
.query(&[("_", Self::nonce())])
|
||||
.header("User-Agent", user_agent())
|
||||
.send()
|
||||
.await?;
|
||||
@@ -185,15 +185,15 @@ impl SessionManager {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a timestamp-based nonce
|
||||
fn timestamp_nonce() -> String {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
.to_string()
|
||||
/// Generates a timestamp-based nonce
|
||||
pub fn nonce() -> String {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a browser-like user agent string
|
||||
|
||||
@@ -103,7 +103,7 @@ fn parse_course_code(input: &str) -> Result<(i32, i32), Error> {
|
||||
// Handle single course code
|
||||
if input.len() == 4 {
|
||||
let code: i32 = input.parse()?;
|
||||
if code < 1000 || code > 9999 {
|
||||
if !(1000..=9999).contains(&code) {
|
||||
return Err("Course codes must be between 1000 and 9999".into());
|
||||
}
|
||||
return Ok((code, code));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Service;
|
||||
use crate::web::routes::{BannerState, create_banner_router};
|
||||
use crate::web::{BannerState, create_banner_router};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
Reference in New Issue
Block a user