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, 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, 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: HomeAway<NamedPerson>,
78	pub official_scorer: NamedPerson,
79	pub primary_datacaster: NamedPerson,
80	pub mound_visits: HomeAway<ResourceUsage>,
81
82    #[doc(hidden)]
83    #[serde(rename = "alerts", default)]
84	pub __alerts: IgnoredAny,
85
86	#[doc(hidden)] // todo
87	#[serde(rename = "absChallenges", default)]
88	pub __abs_challenges: 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	/// Will state `P` for [`GameType::Playoffs`] games rather than what playoff series it is, amongst other things
102	pub gameday_type: GameType,
103	#[serde(deserialize_with = "crate::from_yes_no")]
104	pub tiebreaker: bool,
105	/// No clue what this means
106	pub game_number: u32,
107	pub season: SeasonId,
108	#[serde(rename = "seasonDisplay")]
109	pub displayed_season: SeasonId,
110
111	#[doc(hidden)]
112	#[serde(rename = "id", default)]
113	pub __id: IgnoredAny,
114	#[doc(hidden)]
115	#[serde(rename = "calendarEventID", default)]
116	pub __calender_event_id: IgnoredAny,
117}
118
119/// Live data about the game -- i.e. stuff that changes as the game goes on.
120/// 
121/// Includes a lot of sub-requests within it, such as the [`super::PlayByPlay`] and [`super::Linescore`].
122#[derive(Debug, Deserialize, PartialEq, Clone)]
123#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
124pub struct LiveFeedLiveData {
125	pub linescore: Linescore,
126	pub boxscore: Boxscore,
127	pub decisions: Option<Decisions>,
128	pub leaders: GameStatLeaders,
129	pub plays: Plays,
130}
131
132/// Returns a [`LiveFeedResponse`]
133#[derive(Builder)]
134#[builder(derive(Into))]
135pub struct LiveFeedRequest {
136	#[builder(into)]
137	id: GameId,
138}
139
140impl<S: live_feed_request_builder::State + live_feed_request_builder::IsComplete> crate::request::RequestURLBuilderExt for LiveFeedRequestBuilder<S> {
141	type Built = LiveFeedRequest;
142}
143
144impl Display for LiveFeedRequest {
145	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146		write!(f, "http://statsapi.mlb.com/api/v1.1/game/{}/feed/live", self.id)
147	}
148}
149
150impl RequestURL for LiveFeedRequest {
151	type Response = LiveFeedResponse;
152}
153
154#[cfg(test)]
155mod tests {
156	use crate::TEST_YEAR;
157use crate::game::LiveFeedRequest;
158	use crate::meta::GameType;
159use crate::request::RequestURLBuilderExt;
160    use crate::schedule::ScheduleRequest;
161    use crate::season::{Season, SeasonsRequest};
162    use crate::sport::SportId;
163
164	#[tokio::test]
165	async fn ws_gm7_2025_live_feed() {
166		dbg!(LiveFeedRequest::builder().id(813_024).build().to_string());
167		let response = LiveFeedRequest::builder().id(813_024).build_and_get().await.unwrap();
168		dbg!(response);
169	}
170
171	#[tokio::test]
172	async fn postseason_live_feed() {
173		let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
174		let postseason = season.postseason.expect("Expected the MLB to have a postseason");
175		let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
176		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<_>>();
177		let mut has_errors = false;
178		for game in games {
179			if let Err(e) = LiveFeedRequest::builder().id(game).build_and_get().await {
180				dbg!(e);
181				has_errors = true;
182			}
183		}
184		assert!(!has_errors, "Has errors.");
185	}
186
187	#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
188    #[tokio::test]
189    async fn regular_season_live_feed() {
190        let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
191        let regular_season = season.regular_season;
192        let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
193        let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
194        let mut has_errors = false;
195        for game in games {
196            if let Err(e) = LiveFeedRequest::builder().id(game.game_id).build_and_get().await {
197                dbg!(e);
198                has_errors = true;
199            }
200        }
201        assert!(!has_errors, "Has errors.");
202    }
203}