Skip to content

Commit 1f8989a

Browse files
authored
Merge pull request #11 from sakihet/develop
Develop
2 parents bacfe53 + e1a8c94 commit 1f8989a

35 files changed

+1293
-1171
lines changed

.gemini/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"tools": {
3+
"allowed": [
4+
"run_shell_command(cargo)",
5+
"run_shell_command(git diff)",
6+
"run_shell_command(make)"
7+
]
8+
}
9+
}

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ reqwest = { version = "0.12.24", features = ["json"] }
1818
serde = { version = "1.0.228", features = ["derive"] }
1919
serde_json = "1.0.145"
2020
textplots = "0.8.7"
21+
thiserror = "2.0"
2122
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
2223
toml = "0.9.11"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Commands:
3232
config Manage configuration
3333
dream-team Show dream team
3434
fixture Show upcoming fixtures
35-
fixture-difficulty-rating Show fixture difficulty rating
35+
fixture-difficulty-rating Show fixture difficulty rating [aliases: fdr]
3636
gameweek Show gameweeks
3737
history Show manager's season history
3838
live Show live player stats for a specific event
@@ -44,6 +44,7 @@ Commands:
4444
status Show status
4545
table Show league table
4646
team Show teams
47+
team-form Show team form based on total player form
4748
team-perf Show team performance based on player points per GW
4849
transfer Show popular transfers
4950
help Print this message or the help of the given subcommand(s)

src/api.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use serde::de::DeserializeOwned;
22

3+
use crate::error::Result;
34
use crate::models::{
45
BootstrapStatic, DreamTeam, Fixture, LiveData, ManagerHistory, ManagerPicks, PlayerSummary,
56
SetPieceNotes,
@@ -10,49 +11,42 @@ const BASE_URL: &str = "https://fantasy.premierleague.com/api";
1011
pub struct FplClient;
1112

1213
impl FplClient {
13-
async fn fetch<T: DeserializeOwned>(endpoint: &str) -> Result<T, Box<dyn std::error::Error>> {
14+
async fn fetch<T: DeserializeOwned>(endpoint: &str) -> Result<T> {
1415
let url = format!("{}{}", BASE_URL, endpoint);
1516
let response = reqwest::get(&url).await?;
1617
let json: T = response.json().await?;
1718
Ok(json)
1819
}
1920

20-
pub async fn fetch_bootstrap_static() -> Result<BootstrapStatic, Box<dyn std::error::Error>> {
21+
pub async fn fetch_bootstrap_static() -> Result<BootstrapStatic> {
2122
Self::fetch("/bootstrap-static/").await
2223
}
2324

24-
pub async fn fetch_dream_team(event_id: u32) -> Result<DreamTeam, Box<dyn std::error::Error>> {
25+
pub async fn fetch_dream_team(event_id: u32) -> Result<DreamTeam> {
2526
Self::fetch(&format!("/dream-team/{}/", event_id)).await
2627
}
2728

28-
pub async fn fetch_fixtures() -> Result<Vec<Fixture>, Box<dyn std::error::Error>> {
29+
pub async fn fetch_fixtures() -> Result<Vec<Fixture>> {
2930
Self::fetch("/fixtures/").await
3031
}
3132

32-
pub async fn fetch_live(event_id: u32) -> Result<LiveData, Box<dyn std::error::Error>> {
33+
pub async fn fetch_live(event_id: u32) -> Result<LiveData> {
3334
Self::fetch(&format!("/event/{}/live/", event_id)).await
3435
}
3536

36-
pub async fn fetch_manager_picks(
37-
manager_id: u64,
38-
event_id: u32,
39-
) -> Result<ManagerPicks, Box<dyn std::error::Error>> {
37+
pub async fn fetch_manager_picks(manager_id: u64, event_id: u32) -> Result<ManagerPicks> {
4038
Self::fetch(&format!("/entry/{}/event/{}/picks/", manager_id, event_id)).await
4139
}
4240

43-
pub async fn fetch_player_summary(
44-
player_id: u64,
45-
) -> Result<PlayerSummary, Box<dyn std::error::Error>> {
41+
pub async fn fetch_player_summary(player_id: u64) -> Result<PlayerSummary> {
4642
Self::fetch(&format!("/element-summary/{}/", player_id)).await
4743
}
4844

49-
pub async fn fetch_manager_history(
50-
manager_id: u64,
51-
) -> Result<ManagerHistory, Box<dyn std::error::Error>> {
45+
pub async fn fetch_manager_history(manager_id: u64) -> Result<ManagerHistory> {
5246
Self::fetch(&format!("/entry/{}/history/", manager_id)).await
5347
}
5448

55-
pub async fn fetch_set_piece_notes() -> Result<SetPieceNotes, Box<dyn std::error::Error>> {
49+
pub async fn fetch_set_piece_notes() -> Result<SetPieceNotes> {
5650
Self::fetch("/team/set-piece-notes/").await
5751
}
5852
}

src/commands/availability.rs

Lines changed: 96 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,109 @@
11
use crate::api::FplClient;
2+
use crate::error::Result;
23
use crate::models::{Element, Position};
34
use crate::utils::team_helpers::{create_team_map, find_team_ids_by_name};
45
use owo_colors::OwoColorize;
56

6-
pub async fn handle_availability(team: Option<String>, all: bool, limit: usize) {
7-
match FplClient::fetch_bootstrap_static().await {
8-
Ok(data) => {
9-
let team_map = create_team_map(&data.teams);
10-
let target_team_ids = if let Some(ref team_name) = team {
11-
find_team_ids_by_name(&data.teams, team_name)
7+
pub async fn handle_availability(team: Option<String>, all: bool, limit: usize) -> Result<()> {
8+
let data = FplClient::fetch_bootstrap_static().await?;
9+
10+
let team_map = create_team_map(&data.teams);
11+
let target_team_ids = if let Some(ref team_name) = team {
12+
find_team_ids_by_name(&data.teams, team_name)
13+
} else {
14+
Vec::new()
15+
};
16+
17+
let mut players: Vec<Element> = data
18+
.elements
19+
.into_iter()
20+
.filter(|player| {
21+
let team_match = if team.is_some() {
22+
target_team_ids.contains(&player.team)
1223
} else {
13-
Vec::new()
24+
true
1425
};
1526

16-
let mut players: Vec<Element> = data
17-
.elements
18-
.into_iter()
19-
.filter(|player| {
20-
let team_match = if team.is_some() {
21-
target_team_ids.contains(&player.team)
22-
} else {
23-
true
24-
};
25-
26-
if !team_match {
27-
return false;
28-
}
29-
30-
// If not "all", only show players with news or non-available status
31-
if !all && player.status == "a" && player.news.is_empty() {
32-
return false;
33-
}
34-
35-
true
36-
})
37-
.collect();
38-
39-
if players.is_empty() {
40-
println!("No availability issues found.");
41-
return;
27+
if !team_match {
28+
return false;
4229
}
4330

44-
// Sort players by news_added (latest first)
45-
players.sort_by(|a, b| b.news_added.cmp(&a.news_added));
46-
47-
println!(
48-
"{:<4} {:<20} {:<16} {:<4} {:<10} {:<8} {:<18} {:<30}",
49-
"ID", "Name", "Team", "Pos", "Status", "Chance", "News Added", "News"
50-
);
51-
52-
for player in players.iter().take(limit) {
53-
let team_name = team_map
54-
.get(&player.team)
55-
.map(|s| s.as_str())
56-
.unwrap_or("Unknown");
57-
58-
let status_desc = match player.status.as_str() {
59-
"a" => "Available",
60-
"i" => "Injured",
61-
"d" => "Doubtful",
62-
"s" => "Suspended",
63-
"n" => "Not Available",
64-
"u" => "Unavail",
65-
_ => &player.status,
66-
};
67-
68-
let chance_val = player.chance_of_playing_next_round;
69-
let chance_str = chance_val
70-
.map(|c| format!("{}%", c))
71-
.unwrap_or_else(|| "N/A".to_string());
72-
73-
let chance_padding = " ".repeat(8usize.saturating_sub(chance_str.len()));
74-
75-
let colored_chance = match chance_val {
76-
Some(75) => chance_str.yellow().to_string(),
77-
Some(50) => chance_str.truecolor(255, 165, 0).to_string(),
78-
Some(0) => chance_str.red().to_string(),
79-
_ => chance_str,
80-
};
81-
82-
let news_added = player
83-
.news_added
84-
.as_ref()
85-
.map(|s| {
86-
if s.len() >= 16 {
87-
s[..16].replace('T', " ")
88-
} else {
89-
s.clone()
90-
}
91-
})
92-
.unwrap_or_else(|| "N/A".to_string());
93-
94-
println!(
95-
"{:<4} {:<20} {:<16} {:<4} {:<10} {}{} {:<18} {:<30}",
96-
player.id,
97-
player.web_name,
98-
team_name,
99-
Position::from_element_type_id(player.element_type)
100-
.map(|p| p.display_name().to_string())
101-
.unwrap_or_default(),
102-
status_desc,
103-
chance_padding,
104-
colored_chance,
105-
news_added,
106-
player.news
107-
);
31+
// If not "all", only show players with news or non-available status
32+
if !all
33+
&& player
34+
.status
35+
.is_available(player.chance_of_playing_next_round)
36+
&& player.news.is_empty()
37+
{
38+
return false;
10839
}
109-
}
110-
Err(e) => {
111-
eprintln!("Error: {}", e);
112-
}
40+
41+
true
42+
})
43+
.collect();
44+
45+
if players.is_empty() {
46+
println!("No availability issues found.");
47+
return Ok(());
11348
}
49+
50+
// Sort players by news_added (latest first)
51+
players.sort_by(|a, b| b.news_added.cmp(&a.news_added));
52+
53+
println!(
54+
"{:<4} {:<20} {:<16} {:<4} {:<10} {:<8} {:<18} {:<30}",
55+
"ID", "Name", "Team", "Pos", "Status", "Chance", "News Added", "News"
56+
);
57+
58+
for player in players.iter().take(limit) {
59+
let team_name = team_map
60+
.get(&player.team)
61+
.map(|s| s.as_str())
62+
.unwrap_or("Unknown");
63+
64+
let status_desc = player.status.display_name();
65+
66+
let chance_val = player.chance_of_playing_next_round;
67+
let chance_str = chance_val
68+
.map(|c| format!("{}%", c))
69+
.unwrap_or_else(|| "N/A".to_string());
70+
71+
let chance_padding = " ".repeat(8usize.saturating_sub(chance_str.len()));
72+
73+
let colored_chance = match chance_val {
74+
Some(75) => chance_str.yellow().to_string(),
75+
Some(50) => chance_str.truecolor(255, 165, 0).to_string(),
76+
Some(0) => chance_str.red().to_string(),
77+
_ => chance_str,
78+
};
79+
80+
let news_added = player
81+
.news_added
82+
.as_ref()
83+
.map(|s| {
84+
if s.len() >= 16 {
85+
s[..16].replace('T', " ")
86+
} else {
87+
s.clone()
88+
}
89+
})
90+
.unwrap_or_else(|| "N/A".to_string());
91+
92+
println!(
93+
"{:<4} {:<20} {:<16} {:<4} {:<10} {}{} {:<18} {:<30}",
94+
player.id,
95+
player.web_name,
96+
team_name,
97+
Position::from_element_type_id(player.element_type)
98+
.map(|p| p.display_name().to_string())
99+
.unwrap_or_default(),
100+
status_desc,
101+
chance_padding,
102+
colored_chance,
103+
news_added,
104+
player.news
105+
);
106+
}
107+
108+
Ok(())
114109
}

src/commands/config.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::config::Config;
2+
use crate::error::{FplrError, Result};
23
use clap::{Args, Subcommand};
34

45
#[derive(Debug, Args)]
@@ -25,7 +26,7 @@ pub enum ConfigCommands {
2526
List,
2627
}
2728

28-
pub fn handle_config(args: ConfigArgs) {
29+
pub fn handle_config(args: ConfigArgs) -> Result<()> {
2930
match args.command {
3031
ConfigCommands::Set { key, value } => {
3132
let mut config = Config::load().unwrap_or_default();
@@ -35,16 +36,11 @@ pub fn handle_config(args: ConfigArgs) {
3536
let mut user = config.user.unwrap_or_default();
3637
user.manager_id = Some(value.clone());
3738
config.user = Some(user);
38-
39-
if let Err(e) = config.save() {
40-
eprintln!("Failed to save config: {}", e);
41-
} else {
42-
println!("Successfully updated manager-id to {}", value);
43-
}
39+
config.save()?;
40+
println!("Successfully updated manager-id to {}", value);
4441
}
4542
_ => {
46-
eprintln!("Unknown configuration key: {}", key);
47-
eprintln!("Available keys: manager-id");
43+
return Err(FplrError::UnknownConfigKey(key));
4844
}
4945
}
5046
}
@@ -63,7 +59,7 @@ pub fn handle_config(args: ConfigArgs) {
6359
}
6460
}
6561
_ => {
66-
eprintln!("Unknown configuration key: {}", key);
62+
return Err(FplrError::UnknownConfigKey(key));
6763
}
6864
}
6965
}
@@ -84,4 +80,5 @@ pub fn handle_config(args: ConfigArgs) {
8480
}
8581
}
8682
}
83+
Ok(())
8784
}

0 commit comments

Comments
 (0)