Skip to main content

mlb_api/requests/game/
live_feed.rs

1//! A general feed of a game. Includes plays, linescore, etc. Typically your request unless you want to get more specific.
2
3use 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/// See [`self`]
21#[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/// Metadata about the game, often not useful.
41#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
42#[serde(rename_all = "camelCase")]
43#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
44pub struct LiveFeedMetadata {
45	/// Recommended duration to send new requests (in seconds). Often 10.
46	#[serde(rename = "wait")]
47	pub recommended_poll_rate: u32,
48	pub game_events: Vec<String>, // todo: what is this type
49	pub logical_events: Vec<LogicalEventId>,
50
51    #[serde(rename = "timeStamp")]
52	pub timestamp: SimplifiedTimestamp,
53}
54
55/// General information about the game
56#[derive(Debug, Deserialize, PartialEq, Clone, Deref, DerefMut)]
57#[serde(rename_all = "camelCase")]
58#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
59pub struct LiveFeedData {
60	#[deref]
61	#[deref_mut]
62	#[serde(rename = "game")]
63	game: LiveFeedDataMeta,
64	pub datetime: GameDateTime,
65	pub status: GameStatus,
66	pub teams: HomeAway<Team<()>>,
67	#[serde(deserialize_with = "super::deserialize_players_cache")]
68	pub players: FxHashMap<PersonId, Ballplayer<()>>,
69	pub venue: Venue,
70	pub official_venue: VenueId,
71	pub weather: WeatherConditions,
72	#[serde(rename = "gameInfo")]
73	pub info: GameInfo,
74	pub review: TeamReviewData,
75	#[serde(rename = "flags")]
76	pub live_tags: GameTags,
77	pub probable_pitchers: Option<HomeAway<NamedPerson>>,
78	pub official_scorer: Option<NamedPerson>,
79	pub primary_datacaster: Option<NamedPerson>,
80	pub mound_visits: HomeAway<ResourceUsage>,
81	#[serde(default)]
82	pub abs_challenges: TeamChallengeData,
83
84    #[doc(hidden)]
85    #[serde(rename = "alerts", default)]
86	pub __alerts: IgnoredAny,
87}
88
89/// More specific information about the "game", child of [`LiveFeedData`]
90#[derive(Debug, Deserialize, PartialEq, Clone)]
91#[serde(rename_all = "camelCase")]
92#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
93pub struct LiveFeedDataMeta {
94	#[serde(rename = "pk")]
95	pub id: GameId,
96	#[serde(rename = "type")]
97	pub game_type: GameType,
98	pub double_header: DoubleHeaderKind,
99	#[serde(deserialize_with = "crate::from_yes_no")]
100	pub tiebreaker: bool,
101	/// No clue what this means
102	pub game_number: u32,
103	pub season: SeasonId,
104	#[serde(rename = "seasonDisplay")]
105	pub displayed_season: SeasonId,
106
107	#[doc(hidden)]
108	#[serde(rename = "id", default)]
109	pub __id: IgnoredAny,
110	#[doc(hidden)]
111	#[serde(rename = "calendarEventID", default)]
112	pub __calender_event_id: IgnoredAny,
113	#[doc(hidden)]
114	#[serde(rename = "gamedayType", default)]
115	pub __gameday_type: IgnoredAny,
116}
117
118/// Live data about the game -- i.e. stuff that changes as the game goes on.
119/// 
120/// Includes a lot of sub-requests within it, such as the [`super::PlayByPlay`] and [`super::Linescore`].
121#[derive(Debug, Deserialize, PartialEq, Clone)]
122#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
123pub struct LiveFeedLiveData {
124	pub linescore: Linescore,
125	pub boxscore: Boxscore,
126	pub decisions: Option<Decisions>,
127	pub leaders: GameStatLeaders,
128	pub plays: Plays,
129}
130
131/// Returns a [`LiveFeedResponse`]
132#[derive(Builder)]
133#[builder(derive(Into))]
134pub struct LiveFeedRequest {
135	#[builder(into)]
136	id: GameId,
137}
138
139impl<S: live_feed_request_builder::State + live_feed_request_builder::IsComplete> crate::request::RequestURLBuilderExt for LiveFeedRequestBuilder<S> {
140	type Built = LiveFeedRequest;
141}
142
143impl Display for LiveFeedRequest {
144	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
145		write!(f, "http://statsapi.mlb.com/api/v1.1/game/{}/feed/live", self.id)
146	}
147}
148
149impl RequestURL for LiveFeedRequest {
150	type Response = LiveFeedResponse;
151}
152
153#[cfg(test)]
154mod tests {
155	use crate::TEST_YEAR;
156use crate::game::LiveFeedRequest;
157	use crate::meta::GameType;
158use crate::request::RequestURLBuilderExt;
159    use crate::schedule::ScheduleRequest;
160    use crate::season::{Season, SeasonsRequest};
161    use crate::sport::SportId;
162
163	#[tokio::test]
164	async fn ws_gm7_2025_live_feed() {
165		dbg!(LiveFeedRequest::builder().id(813_024).build().to_string());
166		let response = LiveFeedRequest::builder().id(813_024).build_and_get().await.unwrap();
167		dbg!(response);
168	}
169
170	#[tokio::test]
171	async fn todays_games_live_feed() {
172		let games = ScheduleRequest::<()>::builder().sport_id(SportId::MLB).build_and_get().await.unwrap().dates.into_iter().flat_map(|date| date.games);
173		let mut has_errors = false;
174		for game in games {
175			if let Err(e) = LiveFeedRequest::builder().id(game.game_id).build_and_get().await {
176				dbg!(e);
177				has_errors = true;
178			}
179		}
180		assert!(!has_errors, "Has errors.");
181	}
182
183	#[tokio::test]
184	async fn postseason_live_feed() {
185		let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
186		let postseason = season.postseason.expect("Expected the MLB to have a postseason");
187		let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
188		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<_>>();
189		let mut has_errors = false;
190		for game in games {
191			if let Err(e) = LiveFeedRequest::builder().id(game).build_and_get().await {
192				dbg!(e);
193				has_errors = true;
194			}
195		}
196		assert!(!has_errors, "Has errors.");
197	}
198
199	#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
200    #[tokio::test]
201    async fn regular_season_live_feed() {
202        let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
203        let regular_season = season.regular_season;
204        let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
205        let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
206        let mut has_errors = false;
207        for game in games {
208            if let Err(e) = LiveFeedRequest::builder().id(game.game_id).build_and_get().await {
209                dbg!(e);
210                has_errors = true;
211            }
212        }
213        assert!(!has_errors, "Has errors.");
214    }
215}