Skip to main content

mlb_api/requests/game/
mod.rs

1//! The thing you're most likely here for.
2//!
3//! This module itself acts like [`crate::types`] but for misc game-specific types as there are many.
4
5#![allow(unused_imports, reason = "usage of children modules")]
6
7use std::fmt::{Display, Formatter};
8use std::marker::PhantomData;
9use std::ops::{ControlFlow, Sub};
10use std::time::{Duration, Instant};
11use bon::Builder;
12use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Utc};
13use derive_more::{Deref, DerefMut, Display, From, Not};
14use fxhash::FxHashMap;
15use serde::{Deserialize, Deserializer};
16use serde::de::{DeserializeOwned, Error, IgnoredAny, MapAccess, Visitor};
17use serde_with::{serde_as, DisplayFromStr};
18use crate::person::{Ballplayer, JerseyNumber, NamedPerson, PersonId};
19use crate::meta::{DayNight, NamedPosition};
20use crate::request::RequestURLBuilderExt;
21use crate::team::TeamId;
22use crate::team::roster::RosterStatus;
23use crate::{DayHalf, HomeAway, ResourceUsage};
24use crate::meta::WindDirectionId;
25use crate::request;
26
27mod boxscore; // done
28mod changes;
29mod content;
30mod context_metrics;
31mod diff;
32mod linescore; // done
33mod pace; // done
34mod plays; // done
35mod timestamps; // done
36mod uniforms;
37mod win_probability;
38mod live_feed; // done
39
40pub use boxscore::*;
41pub use changes::*;
42pub use content::*;
43pub use context_metrics::*;
44pub use diff::*;
45pub use linescore::*;
46pub use pace::*;
47pub use plays::*;
48pub use timestamps::*;
49pub use uniforms::*;
50pub use win_probability::*;
51pub use live_feed::*;
52
53id!(#[doc = "A [`u32`] representing a baseball game. [Sport](crate::sport)-independent"] GameId { gamePk: u32 });
54
55#[derive(Deserialize)]
56#[serde(rename_all = "camelCase")]
57#[doc(hidden)]
58#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
59struct __GameDateTimeStruct {
60	#[serde(rename = "dateTime", deserialize_with = "crate::deserialize_datetime")]
61	datetime: NaiveDateTime,
62	original_date: NaiveDate,
63	official_date: NaiveDate,
64	#[serde(rename = "dayNight")]
65	sky: DayNight,
66	time: NaiveTime,
67	ampm: DayHalf,
68}
69
70/// Date & Time of the game. Note that the time is typically rounded to the hour and the :07, :05 on the hour is for the first pitch, which is a different timestamp.
71#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
72#[serde(from = "__GameDateTimeStruct")]
73pub struct GameDateTime {
74	pub datetime: NaiveDateTime,
75	pub original_date: NaiveDate,
76	pub official_date: NaiveDate,
77	pub sky: DayNight,
78}
79
80impl From<__GameDateTimeStruct> for GameDateTime {
81	fn from(value: __GameDateTimeStruct) -> Self {
82		let date = value.datetime.date();
83		let time = value.ampm.into_24_hour_time(value.time);
84		Self {
85			datetime: NaiveDateTime::new(date, time),
86			original_date: value.original_date,
87			official_date: value.official_date,
88			sky: value.sky,
89		}
90	}
91}
92
93/// General weather conditions, temperature, wind, etc.
94#[derive(Debug, Deserialize, PartialEq, Clone)]
95#[serde(try_from = "__WeatherConditionsStruct")]
96pub struct WeatherConditions {
97	pub condition: String,
98	pub temp: uom::si::f64::ThermodynamicTemperature,
99	pub wind_speed: uom::si::f64::Velocity,
100	pub wind_direction: WindDirectionId,
101}
102
103#[serde_as]
104#[derive(Deserialize)]
105#[doc(hidden)]
106#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
107struct __WeatherConditionsStruct {
108	condition: String,
109	#[serde_as(as = "DisplayFromStr")]
110	temp: i32,
111	wind: String,
112}
113
114impl TryFrom<__WeatherConditionsStruct> for WeatherConditions {
115	type Error = &'static str;
116
117	fn try_from(value: __WeatherConditionsStruct) -> Result<Self, Self::Error> {
118		let (speed, direction) = value.wind.split_once(" mph, ").ok_or("invalid wind format")?;
119		let speed = speed.parse::<i32>().map_err(|_| "invalid wind speed")?;
120		Ok(Self {
121			condition: value.condition,
122			temp: uom::si::f64::ThermodynamicTemperature::new::<uom::si::thermodynamic_temperature::degree_fahrenheit>(value.temp as f64),
123			wind_speed: uom::si::f64::Velocity::new::<uom::si::velocity::mile_per_hour>(speed as f64),
124			wind_direction: WindDirectionId::new(direction),
125		})
126	}
127}
128
129/// Misc
130#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
131#[serde(rename_all = "camelCase")]
132#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
133pub struct GameInfo {
134	pub attendance: Option<u32>,
135	#[serde(deserialize_with = "crate::deserialize_datetime")]
136	pub first_pitch: NaiveDateTime,
137	/// Measured in minutes,
138	#[serde(rename = "gameDurationMinutes")]
139	pub game_duration: Option<u32>,
140	/// Durationg of the game delay; measured in minutes.
141	#[serde(rename = "delayDurationMinutes")]
142	pub delay_duration: Option<u32>,
143}
144
145/// Review usage for each team and if the game supports challenges.
146#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
147#[serde(rename_all = "camelCase")]
148#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
149pub struct TeamReviewData {
150	pub has_challenges: bool,
151	#[serde(flatten)]
152	pub teams: HomeAway<ResourceUsage>,
153}
154
155/// Tags about a game, such as a perfect game in progress, no-hitter, etc.
156#[allow(clippy::struct_excessive_bools, reason = "")]
157#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
158#[serde(rename_all = "camelCase")]
159#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
160pub struct GameTags {
161	no_hitter: bool,
162	perfect_game: bool,
163
164	away_team_no_hitter: bool,
165	away_team_perfect_game: bool,
166
167	home_team_no_hitter: bool,
168	home_team_perfect_game: bool,
169}
170
171/// Double-header information.
172#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
173pub enum DoubleHeaderKind {
174	#[serde(rename = "N")]
175	/// Not a doubleheader
176	Not,
177
178	#[serde(rename = "Y")]
179	/// First game in a double-header
180	FirstGame,
181
182	#[serde(rename = "S")]
183	/// Second game in a double-header.
184	SecondGame,
185}
186
187impl DoubleHeaderKind {
188	#[must_use]
189	pub const fn is_double_header(self) -> bool {
190		matches!(self, Self::FirstGame | Self::SecondGame)
191	}
192}
193
194#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Deref, DerefMut, From)]
195pub struct Inning(usize);
196
197impl Display for Inning {
198	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
199		crate::write_nth(self.0, f)
200	}
201}
202
203/// Half of the inning.
204#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Not)]
205pub enum InningHalf {
206	#[serde(rename = "Top", alias = "top")]
207	Top,
208	#[serde(rename = "Bottom", alias = "bottom")]
209	Bottom,
210}
211
212impl InningHalf {
213	/// A unicode character representing an up or down arrow.
214	#[must_use]
215	pub const fn unicode_char_filled(self) -> char {
216		match self {
217			Self::Top => '▲',
218			Self::Bottom => '▼',
219		}
220	}
221	
222	/// A hollow character representing the inning half
223	#[must_use]
224	pub const fn unicode_char_empty(self) -> char {
225		match self {
226			Self::Top => '△',
227			Self::Bottom => '▽',
228		}
229	}
230
231	#[must_use]
232	pub const fn three_char(self) -> &'static str {
233		match self {
234			Self::Top => "Top",
235			Self::Bottom => "Bot",
236		}
237	}
238}
239
240/// The balls and strikes in a given at bat. Along with the number of outs (this technically can change during the AB due to pickoffs etc)
241#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
242#[display("{balls}-{strikes} ({outs} out)")]
243pub struct AtBatCount {
244	pub balls: u8,
245	pub strikes: u8,
246	pub outs: u8,
247}
248
249/// The classic "R | H | E" and LOB in a scoreboard.
250#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
251#[serde(from = "__RHEStruct")]
252#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
253pub struct RHE {
254	pub runs: usize,
255	pub hits: usize,
256	pub errors: usize,
257	pub left_on_base: usize,
258	/// Ex: Home team wins and doesn't need to play Bot 9.
259	pub was_inning_half_played: bool,
260}
261
262#[doc(hidden)]
263#[derive(Deserialize)]
264#[serde(rename_all = "camelCase")]
265struct __RHEStruct {
266	pub runs: Option<usize>,
267    pub hits: usize,
268    pub errors: usize,
269    pub left_on_base: usize,
270
271    // only sometimes present, regardless of whether a game is won
272    #[doc(hidden)]
273    #[serde(rename = "isWinner", default)]
274    pub __is_winner: IgnoredAny,
275}
276
277impl From<__RHEStruct> for RHE {
278	fn from(__RHEStruct { runs, hits, errors, left_on_base, .. }: __RHEStruct) -> Self {
279		Self {
280			runs: runs.unwrap_or(0),
281			hits,
282			errors,
283			left_on_base,
284			was_inning_half_played: runs.is_some(),
285		}
286	}
287}
288
289/// Unparsed miscellaneous data.
290///
291/// Some of these values might be handwritten per game so parsing them would prove rather difficult.
292/// 
293/// ## Examples
294/// | Name          | Value     |
295/// |---------------|-----------|
296/// | First pitch   | 8:10 PM.  |
297/// | Weather       | 68 degrees, Roof Closed |
298/// | Att           | 44,713.   |
299#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
300#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
301pub struct LabelledValue {
302	pub label: String,
303	#[serde(default)]
304	pub value: String,
305}
306
307#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
308#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
309pub struct SectionedLabelledValues {
310	#[serde(rename = "title")]
311	pub section: String,
312	#[serde(rename = "fieldList")]
313	pub values: Vec<LabelledValue>,
314}
315
316/// Various flags about the player in the current game
317#[allow(clippy::struct_excessive_bools, reason = "not what's happening here")]
318#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
319#[serde(rename_all = "camelCase")]
320#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
321pub struct PlayerGameStatusFlags {
322	pub is_current_batter: bool,
323	pub is_current_pitcher: bool,
324	pub is_on_bench: bool,
325	pub is_substitute: bool,
326}
327
328#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
329#[serde(rename_all = "camelCase")]
330#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
331pub struct Official {
332	pub official: NamedPerson,
333	pub official_type: OfficialType,
334}
335
336#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
337pub enum OfficialType {
338	#[serde(rename = "Home Plate")]
339	HomePlate,
340	#[serde(rename = "First Base")]
341	FirstBase,
342	#[serde(rename = "Second Base")]
343	SecondBase,
344	#[serde(rename = "Third Base")]
345	ThirdBase,
346	#[serde(rename = "Left Field")]
347	LeftField,
348	#[serde(rename = "Right Field")]
349	RightField,
350}
351
352/// A position in the batting order, 1st, 2nd, 3rd, 4th, etc.
353///
354/// Note that this number is split in two, the general batting order position is the `major` while if there is a lineup movement then the player would have an increased `minor` since they replace an existing batting order position.
355///
356/// Example:
357/// Alice bats 1st (major = 1, minor = 0)
358/// Bob pinch hits and bats 1st for Alice (major = 1, minor = 1)
359/// Alice somehow hits again (major = 1, minor = 0)
360/// Charlie pinch runs and takes over from then on (major = 1, minor = 2)
361///
362/// Note: These minors are [`Display`]ed incremented one more than is done internally, so (major = 1, minor = 1) displays as `1st (2)`.
363#[derive(Debug, PartialEq, Eq, Copy, Clone)]
364pub struct BattingOrderIndex {
365	pub major: usize,
366	pub minor: usize,
367}
368
369impl<'de> Deserialize<'de> for BattingOrderIndex {
370	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
371	where
372	    D: Deserializer<'de>
373	{
374		let v: usize = String::deserialize(deserializer)?.parse().map_err(D::Error::custom)?;
375		Ok(Self {
376			major: v / 100,
377			minor: v % 100,
378		})
379	}
380}
381
382impl Display for BattingOrderIndex {
383	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
384		crate::write_nth(self.major, f)?;
385		if self.minor > 0 {
386			write!(f, " ({})", self.minor + 1)?;
387		}
388		Ok(())
389	}
390}
391
392/// Decisions of winner & loser (and potentially the save)
393#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
394#[serde(rename_all = "camelCase")]
395#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
396pub struct Decisions {
397	pub winner: Option<NamedPerson>,
398	pub loser: Option<NamedPerson>,
399	pub save: Option<NamedPerson>,
400}
401
402/// Game records in stats like exit velocity, hit distance, etc.
403///
404/// Currently unable to actually get data for these though
405#[derive(Debug, Deserialize, PartialEq, Clone)]
406#[serde(rename_all = "camelCase")]
407#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
408pub struct GameStatLeaders {
409	#[doc(hidden)]
410	#[serde(rename = "hitDistance", default)]
411	pub __distance: IgnoredAny,
412	#[doc(hidden)]
413	#[serde(rename = "hitSpeed", default)]
414	pub __exit_velocity: IgnoredAny,
415	#[doc(hidden)]
416	#[serde(rename = "pitchSpeed", default)]
417	pub __velocity: IgnoredAny,
418}
419
420#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Display)]
421pub enum Base {
422	#[display("1B")]
423	First,
424	#[display("2B")]
425	Second,
426	#[display("3B")]
427	Third,
428	#[display("HP")]
429	Home,
430}
431
432impl<'de> Deserialize<'de> for Base {
433	#[allow(clippy::too_many_lines, reason = "Visitor impl takes up the bulk, is properly scoped")]
434	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
435	where
436		D: Deserializer<'de>
437	{
438		struct BaseVisitor;
439
440		impl Visitor<'_> for BaseVisitor {
441			type Value = Base;
442
443			fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
444				write!(f, "a string or integer representing the base")
445			}
446
447			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
448			where
449				E: Error,
450			{
451				Ok(match v {
452					"1B" | "1" => Base::First,
453					"2B" | "2" => Base::Second,
454					"3B" | "3" => Base::Third,
455					"score" | "HP" | "4B" | "4" => Base::Home,
456					_ => return Err(E::unknown_variant(v, &["1B", "1", "2B" , "2", "3B", "3", "score", "HP", "4B", "4"]))
457				})
458			}
459
460			fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
461			where
462				E: Error,
463			{
464				Ok(match v {
465					1 => Base::First,
466					2 => Base::Second,
467					3 => Base::Third,
468					4 => Base::Home,
469					_ => return Err(E::unknown_variant("[a number]", &["1", "2", "3", "4"]))
470				})
471			}
472		}
473
474		deserializer.deserialize_any(BaseVisitor)
475	}
476}
477
478#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
479pub enum ContactHardness {
480	#[serde(rename = "soft")]
481	Soft,
482	#[serde(rename = "medium")]
483	Medium,
484	#[serde(rename = "hard")]
485	Hard,
486}
487
488pub(crate) fn deserialize_players_cache<'de, T: DeserializeOwned, D: Deserializer<'de>>(deserializer: D) -> Result<FxHashMap<PersonId, T>, D::Error> {
489	struct PlayersCacheVisitor<T2: DeserializeOwned>(PhantomData<T2>);
490
491	impl<'de2, T2: DeserializeOwned> serde::de::Visitor<'de2> for PlayersCacheVisitor<T2> {
492		type Value = FxHashMap<PersonId, T2>;
493
494		fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
495			formatter.write_str("a map")
496		}
497
498		fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
499		where
500			A: MapAccess<'de2>,
501		{
502			let mut values = FxHashMap::default();
503
504			while let Some((key, value)) = map.next_entry()? {
505				let key: String = key;
506				let key = PersonId::new(key.strip_prefix("ID").ok_or_else(|| A::Error::custom("invalid id format"))?.parse::<u32>().map_err(A::Error::custom)?);
507				values.insert(key, value);
508			}
509
510			Ok(values)
511		}
512	}
513
514	deserializer.deserialize_map(PlayersCacheVisitor::<T>(PhantomData))
515}
516
517/// Meant for active & live games, gives a streamable version of the plays in a game.
518///
519/// The [`PlayStream`] is meant to be for consistently polling the MLB API for live play-by-play updates.
520/// The list of events can be seen on [`PlayStreamEvent`]
521/// 
522/// ## Examples
523/// ```no_run
524/// PlayStream::new(/* game id */).run(|event: PlayStreamEvent, meta: &LiveFeedMetadata, data: &LiveFeedData| {
525///     match event {
526///         PlayStreamEvent::GameStart => println!("Game Start"),
527///         PlayStreamEvent::StartPlay(play) => println!("{} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
528///         PlayStreamEvent::PlayEvent(play_event) => {
529///             match play_event {
530///                 PlayEvent::Action { details, .. } => println!("{}", details.description),
531///                 PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
532///                 PlayEvent::Stepoff { .. } => println!("Stepoff"),
533///                 PlayEvent::NoPitch { .. } => println!("No Pitch"),
534///                 PlayEvent::Pickoff { .. } => println!("Pickoff"),
535///             }
536///         },
537///         PlayStreamEvent::PlayEventReviewStart(review) => println!("PlayEventReviewStart; {}", review.review_type),
538///         PlayStreamEvent::PlayEventReviewEnd(review) => println!("PlayEventReviewEnd; {}", review.review_type),
539///         PlayStreamEvent::PlayReviewStart(review) => println!("PlayReviewStart; {}", review.review_type),
540///         PlayStreamEvent::PlayReviewEnd(review) => println!("PlayReviewEnd; {}", review.review_type),
541///         PlayStreamEvent::EndPlay(play) => println!("{}", play.result.completed_play_details.as_ref().expect("Completed play").description),
542///         PlayStreamEvent::GameEnd(_, _, _, _) => println!("GameEnd"),
543///     }
544/// }).await?;
545/// ```
546#[derive(Debug)]
547pub struct PlayStream {
548	game_id: GameId,
549
550	current_play_idx: usize,
551	in_progress_current_play: bool,
552	current_play_review_idx: usize,
553	in_progress_current_play_review: bool,
554	
555	current_play_event_idx: usize,
556	current_play_event_review_idx: usize,
557	in_progress_current_play_event_review: bool,
558}
559
560impl PlayStream {
561	#[must_use]
562	pub fn new(game_id: impl Into<GameId>) -> Self {
563		Self {
564			game_id: game_id.into(),
565			
566			current_play_idx: 0,
567			in_progress_current_play: false,
568			current_play_review_idx: 0,
569			in_progress_current_play_review: false,
570			
571			current_play_event_idx: 0,
572			current_play_event_review_idx: 0,
573			in_progress_current_play_event_review: false,
574		}
575	}
576}
577
578/// An event in a game, such as the game starting, ending, a [`Play`] (At-Bat) starting, or a [`PlayEvent`] occuring, or a challenge on a play or play event.
579#[derive(Debug, PartialEq, Clone)]
580pub enum PlayStreamEvent<'a> {
581	/// Sent at the beginning of a game
582	GameStart,
583	
584	StartPlay(&'a Play),
585	PlayReviewStart(&'a ReviewData, &'a Play),
586	PlayReviewEnd(&'a ReviewData, &'a Play),
587	EndPlay(&'a Play),
588	
589	PlayEvent(&'a PlayEvent, &'a Play),
590	PlayEventReviewStart(&'a ReviewData, &'a PlayEvent, &'a Play),
591	PlayEventReviewEnd(&'a ReviewData, &'a PlayEvent, &'a Play),
592	
593	GameEnd(&'a Decisions, &'a Linescore, &'a Boxscore, &'a GameStatLeaders),
594}
595
596impl PlayStream {
597	/// Runs through plays until the game is over.
598	///
599	/// # Errors
600	/// See [`request::Error`]
601	pub async fn run<F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData) -> Result<ControlFlow<()>, request::Error>>(self, f: F) -> Result<(), request::Error> {
602		self.run_with_custom_error::<request::Error, F>(f).await
603	}
604
605	/// Evaluation for the current play
606	async fn run_current_play<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData) -> Result<ControlFlow<()>, E>>(&self, mut f: F, current_play: &Play, meta: &LiveFeedMetadata, data: &LiveFeedData) -> Result<ControlFlow<()>, E> {
607		macro_rules! flow_try {
608			($($t:tt)*) => {
609				match ($($t)*).await? {
610					ControlFlow::Continue(()) => {},
611					ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
612				}
613			};
614		}
615		
616		if !self.in_progress_current_play {
617			flow_try!(f(PlayStreamEvent::StartPlay(current_play), meta, data));
618		}
619		let mut play_events = current_play.play_events.iter().skip(self.current_play_event_idx);
620		if let Some(current_play_event) = play_events.next() {
621			flow_try!(f(PlayStreamEvent::PlayEvent(current_play_event, current_play), meta, data));
622			let mut reviews = current_play_event.reviews.iter().skip(self.current_play_event_review_idx);
623			if let Some(current_review) = reviews.next() {
624				if !self.in_progress_current_play_event_review {
625					flow_try!(f(PlayStreamEvent::PlayEventReviewStart(current_review, current_play_event, current_play), meta, data));
626				}
627				if !current_review.is_in_progress {
628					flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(current_review, current_play_event, current_play), meta, data));
629				}
630			}
631			for review in reviews {
632				flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, current_play_event, current_play), meta, data));
633				if !review.is_in_progress {
634					flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, current_play_event, current_play), meta, data));
635				}
636			}
637		}
638		for play_event in play_events {
639			flow_try!(f(PlayStreamEvent::PlayEvent(play_event, current_play), meta, data));
640			for review in &play_event.reviews {
641				flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, current_play), meta, data));
642				if !review.is_in_progress {
643					flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, current_play), meta, data));
644				}
645			}
646		}
647		let mut reviews = current_play.reviews.iter().skip(self.current_play_review_idx);
648		if let Some(current_review) = reviews.next() {
649			if !self.in_progress_current_play_review {
650				flow_try!(f(PlayStreamEvent::PlayReviewStart(current_review, current_play), meta, data));
651			}
652			if !current_review.is_in_progress {
653				flow_try!(f(PlayStreamEvent::PlayReviewEnd(current_review, current_play), meta, data));
654			}
655		}
656		
657		for review in reviews {
658			flow_try!(f(PlayStreamEvent::PlayReviewStart(review, current_play), meta, data));
659			if !review.is_in_progress {
660				flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, current_play), meta, data));
661			}
662		}
663		if current_play.about.is_complete {
664			flow_try!(f(PlayStreamEvent::EndPlay(current_play), meta, data));
665		}
666		
667		Ok(ControlFlow::Continue(()))
668	}
669
670	/// Evaluation for remaining plays
671	async fn run_next_plays<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData) -> Result<ControlFlow<()>, E>>(&self, mut f: F, plays: impl Iterator<Item=&Play>, meta: &LiveFeedMetadata, data: &LiveFeedData) -> Result<ControlFlow<()>, E> {
672		macro_rules! flow_try {
673			($($t:tt)*) => {
674				match ($($t)*).await? {
675					ControlFlow::Continue(()) => {},
676					ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
677				}
678			};
679		}
680		
681		for play in plays {
682			flow_try!(f(PlayStreamEvent::StartPlay(play), meta, data));
683			for play_event in &play.play_events {
684				flow_try!(f(PlayStreamEvent::PlayEvent(play_event, play), meta, data));
685				for review in &play_event.reviews {
686					flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, play), meta, data));
687					if !review.is_in_progress {
688						flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, play), meta, data));
689					}
690				}
691			}
692			for review in &play.reviews {
693				flow_try!(f(PlayStreamEvent::PlayReviewStart(review, play), meta, data));
694				if !review.is_in_progress {
695					flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, play), meta, data));
696				}
697			}
698			if play.about.is_complete {
699				flow_try!(f(PlayStreamEvent::EndPlay(play), meta, data));
700			}
701		}
702
703		Ok(ControlFlow::Continue(()))
704	}
705
706	fn update_indices(&mut self, plays: &[Play]) {
707		let latest_play = plays.last();
708
709		self.in_progress_current_play = latest_play.is_some_and(|play| !play.about.is_complete);
710		self.current_play_idx = if self.in_progress_current_play { plays.len() - 1 } else { plays.len() };
711
712		let current_play = plays.get(self.current_play_idx);
713		let current_play_event = current_play.and_then(|play| play.play_events.last());
714		let current_play_review = current_play.and_then(|play| play.reviews.last());
715		let current_play_event_review = current_play_event.and_then(|play_event| play_event.reviews.last());
716		
717		self.in_progress_current_play_review = current_play_review.is_some_and(|review| review.is_in_progress);
718		self.current_play_review_idx = current_play.map_or(0, |play| if self.in_progress_current_play_review { play.reviews.len() - 1 } else { play.reviews.len() });
719
720		self.current_play_event_idx = current_play.map_or(0, |play| play.play_events.len());
721
722		self.in_progress_current_play_event_review = current_play_event_review.is_some_and(|review| review.is_in_progress);
723		self.current_play_event_review_idx = current_play_event.map_or(0, |play_event| if self.in_progress_current_play_event_review { play_event.reviews.len() - 1 } else { play_event.reviews.len() });
724	}
725
726	/// Variant of the ``run`` function that allows for custom error types.
727	///
728	/// # Errors
729	/// See [`request::Error`]
730	pub async fn run_with_custom_error<E: From<request::Error>, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData) -> Result<ControlFlow<()>, E>>(mut self, mut f: F) -> Result<(), E> {
731		macro_rules! flow_try {
732			($($t:tt)*) => {
733				match ($($t)*).await? {
734					ControlFlow::Continue(()) => {},
735					ControlFlow::Break(()) => return Ok(()),
736				}
737			};
738		}
739		
740		let mut feed = LiveFeedRequest::builder().id(self.game_id).build_and_get().await?;
741		flow_try!(f(PlayStreamEvent::GameStart, &feed.meta, &feed.data));
742		
743		loop {
744		    let since_last_request = Instant::now();
745		    
746			let LiveFeedResponse { meta, data, live, .. } = &feed;
747			let LiveFeedLiveData { linescore, boxscore, decisions, leaders, plays } = live;
748			let mut plays = plays.iter().skip(self.current_play_idx);
749
750			if let Some(current_play) = plays.next() {
751				flow_try!(self.run_current_play(&mut f, current_play, meta, data));
752			}
753			
754			flow_try!(self.run_next_plays(&mut f, plays, meta, data));
755			
756			if data.status.abstract_game_code.is_finished() && let Some(decisions) = decisions {
757				let _ = f(PlayStreamEvent::GameEnd(decisions, linescore, boxscore, leaders), meta, data).await?;
758				return Ok(())
759			}
760
761			self.update_indices(&live.plays);
762
763			let total_sleep_time = Duration::from_secs(meta.recommended_poll_rate as _);
764			drop(feed);
765			tokio::time::sleep(total_sleep_time.saturating_sub(since_last_request.elapsed())).await;
766		    feed = LiveFeedRequest::builder().id(self.game_id).build_and_get().await?;
767		}
768	}
769}
770	
771#[cfg(test)]
772mod tests {
773    use std::ops::ControlFlow;
774    use crate::{cache::RequestableEntrypoint, game::{PlayEvent, PlayStream, PlayStreamEvent}};
775
776	#[tokio::test]
777	async fn test_play_stream() {
778		PlayStream::new(822_834).run(async |event, _meta, _data| {
779			match event {
780				PlayStreamEvent::GameStart => println!("GameStart"),
781				PlayStreamEvent::StartPlay(play) => println!("PlayStart; {} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
782				PlayStreamEvent::PlayEvent(play_event, _play) => {
783					print!("PlayEvent; ");
784					match play_event {
785						PlayEvent::Action { details, .. } => println!("{}", details.description),
786						PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
787						PlayEvent::Stepoff { .. } => println!("Stepoff"),
788						PlayEvent::NoPitch { .. } => println!("No Pitch"),
789						PlayEvent::Pickoff { .. } => println!("Pickoff"),
790					}
791				},
792				PlayStreamEvent::PlayEventReviewStart(review, _, _) => println!("PlayEventReviewStart; {}", review.review_type),
793				PlayStreamEvent::PlayEventReviewEnd(review, _, _) => println!("PlayEventReviewEnd; {}", review.review_type),
794				PlayStreamEvent::PlayReviewStart(review, _) => println!("PlayReviewStart; {}", review.review_type),
795				PlayStreamEvent::PlayReviewEnd(review, _) => println!("PlayReviewEnd; {}", review.review_type),
796				PlayStreamEvent::EndPlay(play) => println!("PlayEnd; {}", play.result.completed_play_details.as_ref().expect("Completed play").description),
797				PlayStreamEvent::GameEnd(_, _, _, _) => println!("GameEnd"),
798			}
799			Ok(ControlFlow::Continue(()))
800		}).await.unwrap();
801	}
802}