Icon support, adding table render method, moving sox schedule to api instead of local file.

This commit is contained in:
calcu1on 2025-04-05 18:39:04 -04:00
parent 3c7e5e1d4c
commit d3f6691ca9
6 changed files with 13023 additions and 56 deletions

19
Cargo.lock generated
View File

@ -569,6 +569,15 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iso8601"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5c177cff824ab21a6f41079a4c401241c4e8be14f316c4c6b07d5fca351c98d"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -658,6 +667,15 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.7" version = "0.36.7"
@ -812,6 +830,7 @@ name = "pulse"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"colored", "colored",
"iso8601",
"reqwest", "reqwest",
"serde", "serde",
"serde_alias", "serde_alias",

View File

@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
colored = "3.0.0" colored = "3.0.0"
iso8601 = "0.6.2"
reqwest = { version = "0.12.15", features = ["blocking"] } reqwest = { version = "0.12.15", features = ["blocking"] }
serde = {version = "1.0.219", features = ["derive"]} serde = {version = "1.0.219", features = ["derive"]}
serde_alias = "0.0.2" serde_alias = "0.0.2"

12899
assets/mlb-response.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))]
mod weather; mod weather;
mod redsox; mod redsox;
use colored::Colorize; // use colored::Colorize;
use tabled::{Table, Tabled}; use tabled::{Table, Tabled};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tabled::settings::{ use tabled::settings::{
@ -20,27 +20,24 @@ struct TableRow {
#[allow(unreachable_code)] #[allow(unreachable_code)]
fn main() { fn main() {
let baseball_diamond = '\u{f0852}';
// Set the weather location here. // Set the weather location here.
let location = weather::WeatherOfficeLocation { let location = weather::WeatherOfficeLocation {
x: 75, x: 75,
y: 59, y: 59,
code: "GYX".to_string(), code: "GYX".to_string(),
}; };
// @todo - add a way to configure which teams to add
// @todo - add a way to get news articles?
let entire_forecast: Vec<weather::WeatherPeriod> = weather::get_full_forecast(location); let entire_forecast: Vec<weather::WeatherPeriod> = weather::get_full_forecast(location);
let sox_games: Vec<redsox::Game> = redsox::get_upcoming_games(); let sox_games: Vec<redsox::GameInfo> = redsox::get_schedule();
let baseball_diamond = '\u{f0852}';
let mut table_rows: Vec<TableRow> = vec![]; let mut table_rows: Vec<TableRow> = vec![];
for i in 0..entire_forecast.len() { for i in 0..entire_forecast.len() {
let item = &entire_forecast[i]; let forecast_period = &entire_forecast[i];
let date = &item.start_time[0..10]; let yyyy_mm_dd = &forecast_period.start_time[0..10];
let mut sox_status = String::new(); let mut sox_status = String::new();
// Check if there is a sox game and print opp. // Check if there is a sox game and print opp.
for sox_game in &sox_games { for sox_game in &sox_games {
if sox_game.date == date { if sox_game.date == yyyy_mm_dd {
let mut opp_str = String::new(); let mut opp_str = String::from(baseball_diamond);
opp_str.push(baseball_diamond);
opp_str.push_str(" "); opp_str.push_str(" ");
opp_str.push_str(&sox_game.opponent); opp_str.push_str(&sox_game.opponent);
sox_status = opp_str; sox_status = opp_str;
@ -48,23 +45,26 @@ fn main() {
} }
} }
let row = TableRow { let row = TableRow {
date: date.to_string(), date: yyyy_mm_dd.to_string(),
time_of_day: item.name.clone(), time_of_day: forecast_period.name.clone(),
temp: item.temperature, temp: forecast_period.temperature,
red_sox: sox_status, red_sox: sox_status,
forecast: item.detailed_forecast.to_string(), forecast: forecast_period.detailed_forecast.to_string(),
}; };
table_rows.push(row); table_rows.push(row);
} }
render_table(&table_rows);
// here is where we actually render the table. }
let mut table = Table::new(table_rows);
table.with(Style::modern()); fn render_table(rows: &Vec<TableRow>) {
table.with(( // here is where we actually render the table.
Width::wrap(210).priority(Priority::max(true)), let mut table = Table::new(rows);
Width::increase(50).priority(Priority::min(true)), table.with(Style::modern());
)); table.with((
table.modify(Columns::first(), Alignment::right()); Width::wrap(210).priority(Priority::max(true)),
println!("{}", table); Width::increase(50).priority(Priority::min(true)),
));
table.modify(Columns::first(), Alignment::right());
println!("{}", table);
} }

View File

@ -1,22 +1,34 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_alias::serde_alias; use serde_alias::serde_alias;
use std::fs; use std::fs;
use iso8601;
const SOX_ID: i32 = 111; const SOX_ID: i32 = 111;
const SOX_SCHEDULE_URL: &str = "https://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=111&startDate=2025-04-01&endDate=2025-09-30";
const SOX_SCHEDULE_LOCAL: &str = "/Users/danchadwick/Projects/rust/pulse/assets/mlb-response.json";
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde_alias(CamelCase,SnakeCase)] #[serde_alias(CamelCase,SnakeCase)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Schedule { pub struct Schedule {
pub dates: Vec<Date>,
}
#[serde_alias(CamelCase,SnakeCase)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Date {
pub date: String,
pub games: Vec<Game>, pub games: Vec<Game>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde_alias(CamelCase,SnakeCase)] #[serde_alias(CamelCase,SnakeCase)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Game { pub struct Game {
// pub teams: Teams, pub season: String,
pub date: String, pub teams: Teams,
// pub date: String,
pub official_date: String,
// pub start_time: String, // pub start_time: String,
pub opponent: String, // pub opponent: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -26,47 +38,71 @@ pub struct Teams {
home: Team, home: Team,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde_alias(CamelCase,SnakeCase)] #[serde_alias(CamelCase,SnakeCase)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Team { pub struct Team {
team: TeamInfo, team: TeamInfo,
league_record: TeamRecord, league_record: TeamRecord,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde_alias(CamelCase,SnakeCase)] #[serde_alias(CamelCase,SnakeCase)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TeamInfo { pub struct TeamInfo {
id: i32, id: i32,
name: String, name: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde_alias(CamelCase,SnakeCase)] #[serde_alias(CamelCase,SnakeCase)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TeamRecord { pub struct TeamRecord {
wins: i32, wins: i32,
losses: i32, losses: i32,
} }
pub fn get_upcoming_games() -> Vec<Game> { pub struct GameInfo {
// @todo - change this to be a dynamic request from the API endpoint instead of a local file. pub opponent: String,
let schedule_json: String = fs::read_to_string("/Users/danchadwick/Projects/rust/weather/assets/sox-schedule.json").expect("Unable to read file").to_owned(); pub date: String,
let json: Vec<Game> = serde_json::from_str(&schedule_json).expect("Something not good?"); // pub time: String,
let upcoming_games: &Vec<Game> = &json.into_iter().collect();
upcoming_games.to_owned()
} }
// Gets the full forecast from the response. // Gets the full forecast from the response.
pub fn get_schedule() -> Schedule { pub fn get_schedule() -> Vec<GameInfo> {
let url = "https://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=111&startDate=2025-04-01&endDate=2025-09-30".to_string();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let schedule_string: String = client.get(&url).send().expect("Unable to get data").text().unwrap().to_string(); // let schedule_json: String = fs::read_to_string(SOX_SCHEDULE_LOCAL).expect("Unable to read file").to_owned();
let json: Schedule = serde_json::from_str(&schedule_string).expect("JSON was not well-formatted"); let schedule_json: String = client.get(SOX_SCHEDULE_URL).send().expect("Unable to get data").text().unwrap().to_string();
let schedule: Schedule = serde_json::from_str(&schedule_json).expect("JSON was not well-formatted");
dbg!(json); // Iterate over the schedule, extract datapoints to create new struct
let schedule = Schedule { // Return a vec of the new structs.
games: vec![], let mut full_schedule: Vec<GameInfo> = vec![];
}; let dates = schedule.dates;
for date in dates {
schedule for game in date.games {
// json let facing = extract_opponent(&game.teams);
let game_info = GameInfo {
opponent: facing,
date: game.official_date,
};
full_schedule.push(game_info);
}
}
full_schedule
}
pub fn extract_opponent(teams: &Teams) -> String {
if teams.home.team.name == "Boston Red Sox" {
teams.away.team.name.to_string()
}
else {
teams.home.team.name.to_string()
}
}
#[cfg(test)]
mod sox_tests {
use super::*;
#[test]
fn check_schedule_retrieval() {
get_schedule();
}
} }

View File

@ -52,13 +52,18 @@ pub fn get_full_forecast(location: WeatherOfficeLocation) -> Vec<WeatherPeriod>
.send() .send()
.expect("Unable to get data") .expect("Unable to get data")
.text().unwrap().to_string(); .text().unwrap().to_string();
let json: ForecastWrapper = serde_json::from_str(&forecast).expect("JSON was not well-formatted"); let ForecastWrapper { properties: Properties { mut periods } } = serde_json::from_str(&forecast).expect("JSON was not well-formatted");
let mut weather_periods: Vec<WeatherPeriod> = json.properties.periods.into_iter().collect(); // let json: ForecastWrapper = serde_json::from_str(&forecast).expect("JSON was not well-formatted");
for period in weather_periods.iter_mut() { // let mut weather_periods: Vec<WeatherPeriod> = json.properties.periods.into_iter().collect();
let icon = detect_icon(&period.short_forecast).unwrap(); for period in periods.iter_mut() {
period.detailed_forecast = format!("{} {}", icon, &period.detailed_forecast); match detect_icon(&period.short_forecast) {
None => println!("There was an issue detecting the correct icon!!!"),
Some(icon) => {
period.detailed_forecast = format!("{} {}", icon, &period.detailed_forecast);
}
};
} }
weather_periods periods
} }
pub fn detect_icon(short_forecast: &String) -> Option<char> { pub fn detect_icon(short_forecast: &String) -> Option<char> {
@ -97,6 +102,13 @@ pub fn detect_icon(short_forecast: &String) -> Option<char> {
let icon_code = icon.get_icon(); let icon_code = icon.get_icon();
icon_code icon_code
} }
else if short_forecast.contains("Clear") {
let icon = nerdfont::NerdFontIcon {
icon_code: "e30d".to_string(),
};
let icon_code = icon.get_icon();
icon_code
}
else { else {
None None
} }