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	/// Type is undocumented.
49	pub game_events: Vec<String>,
50	pub logical_events: Vec<LogicalEventId>,
51
52    #[serde(rename = "timeStamp")]
53	pub timestamp: SimplifiedTimestamp,
54}
55
56/// General information about the game
57#[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/// More specific information about the "game", child of [`LiveFeedData`]
92#[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	/// No clue what this means
104	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/// Live data about the game -- i.e. stuff that changes as the game goes on.
121/// 
122/// Includes a lot of sub-requests within it, such as the [`super::PlayByPlay`] and [`super::Linescore`].
123#[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/// Returns a [`LiveFeedResponse`]
134#[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}