1use std::fmt::{Display, Formatter};
4use bon::Builder;
5use derive_more::{Deref, DerefMut};
6use fxhash::FxHashMap;
7use serde::Deserialize;
8use serde::de::IgnoredAny;
9use crate::game::{Boxscore, Decisions, DoubleHeaderKind, GameDateTime, GameId, GameInfo, GameStatLeaders, GameTags, PlayAbout, Plays, ResourceUsage, TeamReviewData, TeamChallengeData, SimplifiedTimestamp, WeatherConditions};
10use crate::game::linescore::Linescore;
11use crate::meta::{GameStatus, GameType};
12use crate::meta::LogicalEventId;
13use crate::person::{Ballplayer, NamedPerson, PersonId};
14use crate::request::RequestURL;
15use crate::season::SeasonId;
16use crate::team::Team;
17use crate::{Copyright, HomeAway};
18use crate::venue::{Venue, VenueId};
19
20#[derive(Debug, Deserialize, PartialEq, Clone)]
22#[serde(rename_all = "camelCase")]
23#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
24pub struct LiveFeedResponse {
25 pub copyright: Copyright,
26 #[serde(rename = "gamePk")]
27 pub id: GameId,
28 #[serde(rename = "metaData")]
29 pub meta: LiveFeedMetadata,
30 #[serde(rename = "gameData")]
31 pub data: LiveFeedData,
32 #[serde(rename = "liveData")]
33 pub live: LiveFeedLiveData,
34
35 #[doc(hidden)]
36 #[serde(rename = "link", default)]
37 pub __link: IgnoredAny,
38}
39
40#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
42#[serde(rename_all = "camelCase")]
43#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
44pub struct LiveFeedMetadata {
45 #[serde(rename = "wait")]
47 pub recommended_poll_rate: u32,
48 pub game_events: Vec<String>,
50 pub logical_events: Vec<LogicalEventId>,
51
52 #[serde(rename = "timeStamp")]
53 pub timestamp: SimplifiedTimestamp,
54}
55
56#[derive(Debug, Deserialize, PartialEq, Clone, Deref, DerefMut)]
58#[serde(rename_all = "camelCase")]
59#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
60pub struct LiveFeedData {
61 #[deref]
62 #[deref_mut]
63 #[serde(rename = "game")]
64 game: LiveFeedDataMeta,
65 pub datetime: GameDateTime,
66 pub status: GameStatus,
67 pub teams: HomeAway<Team<()>>,
68 #[serde(deserialize_with = "super::deserialize_players_cache")]
69 pub players: FxHashMap<PersonId, Ballplayer<()>>,
70 pub venue: Venue,
71 pub official_venue: VenueId,
72 pub weather: WeatherConditions,
73 #[serde(rename = "gameInfo")]
74 pub info: GameInfo,
75 pub review: TeamReviewData,
76 #[serde(rename = "flags")]
77 pub live_tags: GameTags,
78 pub probable_pitchers: HomeAway<Option<NamedPerson>>,
79 pub official_scorer: Option<NamedPerson>,
80 pub primary_datacaster: Option<NamedPerson>,
81 pub secondary_datacaster: Option<NamedPerson>,
82 pub mound_visits: HomeAway<ResourceUsage>,
83 #[serde(default)]
84 pub abs_challenges: TeamChallengeData,
85
86 #[doc(hidden)]
87 #[serde(rename = "alerts", default)]
88 pub __alerts: IgnoredAny,
89}
90
91#[derive(Debug, Deserialize, PartialEq, Clone)]
93#[serde(rename_all = "camelCase")]
94#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
95pub struct LiveFeedDataMeta {
96 #[serde(rename = "pk")]
97 pub id: GameId,
98 #[serde(rename = "type")]
99 pub game_type: GameType,
100 pub double_header: DoubleHeaderKind,
101 #[serde(deserialize_with = "crate::from_yes_no")]
102 pub tiebreaker: bool,
103 pub game_number: u32,
105 pub season: SeasonId,
106 #[serde(rename = "seasonDisplay")]
107 pub displayed_season: SeasonId,
108
109 #[doc(hidden)]
110 #[serde(rename = "id", default)]
111 pub __id: IgnoredAny,
112 #[doc(hidden)]
113 #[serde(rename = "calendarEventID", default)]
114 pub __calender_event_id: IgnoredAny,
115 #[doc(hidden)]
116 #[serde(rename = "gamedayType", default)]
117 pub __gameday_type: IgnoredAny,
118}
119
120#[derive(Debug, Deserialize, PartialEq, Clone)]
124#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
125pub struct LiveFeedLiveData {
126 pub linescore: Linescore,
127 pub boxscore: Boxscore,
128 pub decisions: Option<Decisions>,
129 pub leaders: GameStatLeaders,
130 pub plays: Plays,
131}
132
133#[derive(Builder)]
135#[builder(derive(Into))]
136pub struct LiveFeedRequest {
137 #[builder(into)]
138 id: GameId,
139}
140
141impl<S: live_feed_request_builder::State + live_feed_request_builder::IsComplete> crate::request::RequestURLBuilderExt for LiveFeedRequestBuilder<S> {
142 type Built = LiveFeedRequest;
143}
144
145impl Display for LiveFeedRequest {
146 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147 write!(f, "http://statsapi.mlb.com/api/v1.1/game/{}/feed/live", self.id)
148 }
149}
150
151impl RequestURL for LiveFeedRequest {
152 type Response = LiveFeedResponse;
153}
154
155#[cfg(test)]
156mod tests {
157 use crate::TEST_YEAR;
158use crate::game::LiveFeedRequest;
159 use crate::meta::GameType;
160use crate::request::RequestURLBuilderExt;
161 use crate::schedule::ScheduleRequest;
162 use crate::season::{Season, SeasonsRequest};
163 use crate::sport::SportId;
164
165 #[tokio::test]
166 async fn ws_gm7_2025_live_feed() {
167 dbg!(LiveFeedRequest::builder().id(813_024).build().to_string());
168 let response = LiveFeedRequest::builder().id(813_024).build_and_get().await.unwrap();
169 dbg!(response);
170 }
171
172 #[tokio::test]
173 async fn todays_games_live_feed() {
174 let games = ScheduleRequest::<()>::builder().sport_id(SportId::MLB).build_and_get().await.unwrap().dates.into_iter().flat_map(|date| date.games);
175 let mut has_errors = false;
176 for game in games {
177 if let Err(e) = LiveFeedRequest::builder().id(game.game_id).build_and_get().await {
178 dbg!(e);
179 has_errors = true;
180 }
181 }
182 assert!(!has_errors, "Has errors.");
183 }
184
185 #[tokio::test]
186 async fn postseason_live_feed() {
187 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
188 let postseason = season.postseason.expect("Expected the MLB to have a postseason");
189 let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
190 let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type.is_postseason()).map(|game| game.game_id).collect::<Vec<_>>();
191 let mut has_errors = false;
192 for game in games {
193 if let Err(e) = LiveFeedRequest::builder().id(game).build_and_get().await {
194 dbg!(e);
195 has_errors = true;
196 }
197 }
198 assert!(!has_errors, "Has errors.");
199 }
200
201 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
202 #[tokio::test]
203 async fn regular_season_live_feed() {
204 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
205 let regular_season = season.regular_season;
206 let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
207 let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
208 let mut has_errors = false;
209 for game in games {
210 if let Err(e) = LiveFeedRequest::builder().id(game.game_id).build_and_get().await {
211 dbg!(e);
212 has_errors = true;
213 }
214 }
215 assert!(!has_errors, "Has errors.");
216 }
217}