1#![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::{DateTime, Datelike, 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, ResultHoldingResourceUsage, TeamSide};
24use crate::meta::WindDirectionId;
25use crate::request;
26
27mod boxscore; mod changes;
29mod content;
30mod context_metrics;
31mod diff;
32mod linescore; mod pace; mod plays; mod timestamps; mod uniforms;
37mod win_probability;
38mod live_feed; pub 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(Debug, Deserialize, PartialEq, Clone)]
57#[serde(rename_all = "camelCase")]
58#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
59pub struct GameDateTime {
60 #[serde(rename = "dateTime", deserialize_with = "crate::deserialize_datetime")]
62 pub datetime: DateTime<Utc>,
63 pub original_date: NaiveDate,
65 pub official_date: NaiveDate,
67 #[serde(flatten, default)]
69 pub resumed: Option<GameResumedDateTime>,
70
71 #[serde(rename = "dayNight")]
73 pub sky: DayNight,
74 pub time: NaiveTime,
76 pub ampm: DayHalf,
78}
79
80#[derive(Debug, Deserialize, PartialEq, Clone)]
82#[serde(rename_all = "camelCase")]
83#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
84pub struct GameResumedDateTime {
85 #[serde(rename = "resumeDateTime", deserialize_with = "crate::deserialize_datetime")]
86 pub resumed_datetime: DateTime<Utc>,
87 #[serde(rename = "resumedFromDateTime", deserialize_with = "crate::deserialize_datetime")]
88 pub resumed_from_datetime: DateTime<Utc>,
89
90 #[serde(rename = "resumeDate", default)]
91 pub __resume_date: IgnoredAny,
92 #[serde(rename = "resumeFromDate", default)]
93 pub __resume_from_date: IgnoredAny,
94}
95
96#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
98#[serde(try_from = "__WeatherConditionsStruct")]
99pub struct WeatherConditions {
100 pub condition: Option<String>,
101 pub temp: Option<uom::si::f64::ThermodynamicTemperature>,
102 pub wind: Option<(uom::si::f64::Velocity, WindDirectionId)>,
103}
104
105#[derive(Deserialize)]
106#[doc(hidden)]
107#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
108struct __WeatherConditionsStruct {
109 condition: Option<String>,
110 temp: Option<String>,
111 wind: Option<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 Ok(Self {
119 condition: value.condition,
120 temp: value.temp.and_then(|temp| temp.parse::<i32>().ok()).map(|temp| uom::si::f64::ThermodynamicTemperature::new::<uom::si::thermodynamic_temperature::degree_fahrenheit>(temp as f64)),
121 wind: if let Some(wind) = value.wind {
122 let (speed, direction) = wind.split_once(" mph, ").ok_or("invalid wind format")?;
123 let speed = speed.parse::<i32>().map_err(|_| "invalid wind speed")?;
124 Some((uom::si::f64::Velocity::new::<uom::si::velocity::mile_per_hour>(speed as f64), WindDirectionId::new(direction)))
125 } else { None },
126 })
127 }
128}
129
130#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
132#[serde(rename_all = "camelCase")]
133#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
134pub struct GameInfo {
135 pub attendance: Option<u32>,
136 #[serde(deserialize_with = "crate::try_deserialize_datetime", default)]
137 pub first_pitch: Option<DateTime<Utc>>,
138 #[serde(rename = "gameDurationMinutes")]
140 pub game_duration: Option<u32>,
141 #[serde(rename = "delayDurationMinutes")]
143 pub delay_duration: Option<u32>,
144}
145
146#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
148#[serde(rename_all = "camelCase")]
149#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
150pub struct TeamReviewData {
151 pub has_challenges: bool,
152 #[serde(flatten)]
153 pub teams: HomeAway<ResourceUsage>,
154}
155
156#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
157#[serde(rename_all = "camelCase")]
158#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
159pub struct TeamChallengeData {
160 pub has_challenges: bool,
161 #[serde(flatten)]
162 pub teams: HomeAway<ResultHoldingResourceUsage>,
163}
164
165#[allow(clippy::struct_excessive_bools, reason = "no")]
167#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
168#[serde(rename_all = "camelCase")]
169#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
170pub struct GameTags {
171 pub no_hitter: bool,
172 pub perfect_game: bool,
173
174 pub away_team_no_hitter: bool,
175 pub away_team_perfect_game: bool,
176
177 pub home_team_no_hitter: bool,
178 pub home_team_perfect_game: bool,
179}
180
181#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
183pub enum DoubleHeaderKind {
184 #[serde(rename = "N")]
185 Not,
187
188 #[serde(rename = "Y")]
189 FirstGame,
191
192 #[serde(rename = "S")]
193 SecondGame,
195}
196
197impl DoubleHeaderKind {
198 #[must_use]
199 pub const fn is_double_header(self) -> bool {
200 matches!(self, Self::FirstGame | Self::SecondGame)
201 }
202}
203
204#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Deref, DerefMut, From)]
205pub struct Inning(usize);
206
207impl Inning {
208 #[must_use]
210 pub const fn starting() -> Self {
211 Self(1)
212 }
213}
214
215impl Display for Inning {
216 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
217 crate::write_nth(self.0, f)
218 }
219}
220
221#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Not)]
223pub enum InningHalf {
224 #[serde(rename = "Top", alias = "top")]
225 Top,
226 #[serde(rename = "Bottom", alias = "bottom")]
227 Bottom,
228}
229
230impl InningHalf {
231 #[must_use]
233 pub const fn starting() -> Self {
234 Self::Top
235 }
236
237 pub(crate) fn deserialize_from_is_top_inning<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
238 Ok(match bool::deserialize(deserializer)? {
239 true => Self::Top,
240 false => Self::Bottom,
241 })
242 }
243}
244
245impl InningHalf {
246 #[must_use]
248 pub const fn unicode_char_filled(self) -> char {
249 match self {
250 Self::Top => '▲',
251 Self::Bottom => '▼',
252 }
253 }
254
255 #[must_use]
257 pub const fn unicode_char_empty(self) -> char {
258 match self {
259 Self::Top => '△',
260 Self::Bottom => '▽',
261 }
262 }
263
264 #[must_use]
266 pub const fn three_char(self) -> &'static str {
267 match self {
268 Self::Top => "Top",
269 Self::Bottom => "Bot",
270 }
271 }
272
273 #[must_use]
275 pub const fn bats(self) -> TeamSide {
276 match self {
277 Self::Top => TeamSide::Away,
278 Self::Bottom => TeamSide::Home,
279 }
280 }
281
282 #[must_use]
284 pub const fn pitches(self) -> TeamSide {
285 match self {
286 Self::Top => TeamSide::Home,
287 Self::Bottom => TeamSide::Away,
288 }
289 }
290}
291
292#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, Default)]
294#[display("{balls}-{strikes} ({outs} out)")]
295pub struct AtBatCount {
296 #[serde(default)]
297 pub balls: u8,
298 #[serde(default)]
299 pub strikes: u8,
300 #[serde(default)]
301 pub outs: u8,
302}
303
304#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
305pub struct SituationCount {
306 pub balls: u8,
307 pub strikes: u8,
308 pub outs: u8,
309 pub inning: Inning,
310 #[serde(rename = "isTopInning", deserialize_with = "InningHalf::deserialize_from_is_top_inning")]
311 pub inning_half: InningHalf,
312 #[serde(rename = "runnerOn1b")]
313 pub runner_on_first: bool,
314 #[serde(rename = "runnerOn2b")]
315 pub runner_on_second: bool,
316 #[serde(rename = "runnerOn3b")]
317 pub runner_on_third: bool,
318}
319
320#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
322#[serde(from = "__RHEStruct")]
323#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
324pub struct RHE {
325 pub runs: usize,
326 pub hits: usize,
327 pub errors: usize,
328 pub left_on_base: usize,
329 pub was_inning_half_played: bool,
331}
332
333#[doc(hidden)]
334#[derive(Deserialize)]
335#[serde(rename_all = "camelCase")]
336struct __RHEStruct {
337 pub runs: Option<usize>,
338 #[serde(default)]
339 pub hits: usize,
340 #[serde(default)]
341 pub errors: usize,
342 #[serde(default)]
343 pub left_on_base: usize,
344
345 #[doc(hidden)]
347 #[serde(rename = "isWinner", default)]
348 pub __is_winner: IgnoredAny,
349}
350
351impl From<__RHEStruct> for RHE {
352 fn from(__RHEStruct { runs, hits, errors, left_on_base, .. }: __RHEStruct) -> Self {
353 Self {
354 runs: runs.unwrap_or(0),
355 hits,
356 errors,
357 left_on_base,
358 was_inning_half_played: runs.is_some(),
359 }
360 }
361}
362
363#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
374#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
375pub struct LabelledValue {
376 pub label: String,
377 #[serde(default)]
378 pub value: String,
379}
380
381#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
382#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
383pub struct SectionedLabelledValues {
384 #[serde(rename = "title")]
385 pub section: String,
386 #[serde(rename = "fieldList")]
387 pub values: Vec<LabelledValue>,
388}
389
390#[allow(clippy::struct_excessive_bools, reason = "not what's happening here")]
392#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
393#[serde(rename_all = "camelCase")]
394#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
395pub struct PlayerGameStatusFlags {
396 pub is_current_batter: bool,
397 pub is_current_pitcher: bool,
398 pub is_on_bench: bool,
399 pub is_substitute: bool,
400}
401
402#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
403#[serde(rename_all = "camelCase")]
404#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
405pub struct Official {
406 pub official: NamedPerson,
407 pub official_type: OfficialType,
408}
409
410#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
411pub enum OfficialType {
412 #[serde(rename = "Home Plate")]
413 HomePlate,
414 #[serde(rename = "First Base")]
415 FirstBase,
416 #[serde(rename = "Second Base")]
417 SecondBase,
418 #[serde(rename = "Third Base")]
419 ThirdBase,
420 #[serde(rename = "Left Field")]
421 LeftField,
422 #[serde(rename = "Right Field")]
423 RightField,
424}
425
426#[derive(Debug, PartialEq, Eq, Copy, Clone)]
438pub struct BattingOrderIndex {
439 pub major: usize,
440 pub minor: usize,
441}
442
443impl<'de> Deserialize<'de> for BattingOrderIndex {
444 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
445 where
446 D: Deserializer<'de>
447 {
448 let v: usize = String::deserialize(deserializer)?.parse().map_err(D::Error::custom)?;
449 Ok(Self {
450 major: v / 100,
451 minor: v % 100,
452 })
453 }
454}
455
456impl Display for BattingOrderIndex {
457 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
458 crate::write_nth(self.major, f)?;
459 if self.minor > 0 {
460 write!(f, " ({})", self.minor + 1)?;
461 }
462 Ok(())
463 }
464}
465
466#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
468#[serde(rename_all = "camelCase")]
469#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
470pub struct Decisions {
471 pub winner: Option<NamedPerson>,
472 pub loser: Option<NamedPerson>,
473 pub save: Option<NamedPerson>,
474}
475
476#[derive(Debug, Deserialize, PartialEq, Clone)]
480#[serde(rename_all = "camelCase")]
481#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
482pub struct GameStatLeaders {
483 #[doc(hidden)]
484 #[serde(rename = "hitDistance", default)]
485 pub __distance: IgnoredAny,
486 #[doc(hidden)]
487 #[serde(rename = "hitSpeed", default)]
488 pub __exit_velocity: IgnoredAny,
489 #[doc(hidden)]
490 #[serde(rename = "pitchSpeed", default)]
491 pub __velocity: IgnoredAny,
492}
493
494#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Display)]
495pub enum Base {
496 #[display("1B")]
497 First,
498 #[display("2B")]
499 Second,
500 #[display("3B")]
501 Third,
502 #[display("HP")]
503 Home,
504}
505
506impl<'de> Deserialize<'de> for Base {
507 #[allow(clippy::too_many_lines, reason = "Visitor impl takes up the bulk, is properly scoped")]
508 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
509 where
510 D: Deserializer<'de>
511 {
512 struct BaseVisitor;
513
514 impl Visitor<'_> for BaseVisitor {
515 type Value = Base;
516
517 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
518 write!(f, "a string or integer representing the base")
519 }
520
521 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
522 where
523 E: Error,
524 {
525 Ok(match v {
526 "1B" | "1" => Base::First,
527 "2B" | "2" => Base::Second,
528 "3B" | "3" => Base::Third,
529 "score" | "HP" | "4B" | "4" => Base::Home,
530 _ => return Err(E::unknown_variant(v, &["1B", "1", "2B" , "2", "3B", "3", "score", "HP", "4B", "4"]))
531 })
532 }
533
534 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
535 where
536 E: Error,
537 {
538 Ok(match v {
539 1 => Base::First,
540 2 => Base::Second,
541 3 => Base::Third,
542 4 => Base::Home,
543 _ => return Err(E::unknown_variant("[a number]", &["1", "2", "3", "4"]))
544 })
545 }
546 }
547
548 deserializer.deserialize_any(BaseVisitor)
549 }
550}
551
552#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
553pub enum ContactHardness {
554 #[serde(rename = "soft")]
555 Soft,
556 #[serde(rename = "medium")]
557 Medium,
558 #[serde(rename = "hard")]
559 Hard,
560}
561
562pub(crate) fn deserialize_players_cache<'de, T: DeserializeOwned, D: Deserializer<'de>>(deserializer: D) -> Result<FxHashMap<PersonId, T>, D::Error> {
563 struct PlayersCacheVisitor<T2: DeserializeOwned>(PhantomData<T2>);
564
565 impl<'de2, T2: DeserializeOwned> serde::de::Visitor<'de2> for PlayersCacheVisitor<T2> {
566 type Value = FxHashMap<PersonId, T2>;
567
568 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
569 formatter.write_str("a map")
570 }
571
572 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
573 where
574 A: MapAccess<'de2>,
575 {
576 let mut values = FxHashMap::default();
577
578 while let Some((key, value)) = map.next_entry()? {
579 let key: String = key;
580 let key = PersonId::new(key.strip_prefix("ID").ok_or_else(|| A::Error::custom("invalid id format"))?.parse::<u32>().map_err(A::Error::custom)?);
581 values.insert(key, value);
582 }
583
584 Ok(values)
585 }
586 }
587
588 deserializer.deserialize_map(PlayersCacheVisitor::<T>(PhantomData))
589}
590
591#[derive(Debug)]
623pub struct PlayStream {
624 game_id: GameId,
625
626 current_play_idx: usize,
627 in_progress_current_play: bool,
628 current_play_review_idx: usize,
629 in_progress_current_play_review: bool,
630
631 current_play_event_idx: usize,
632 current_play_event_review_idx: usize,
633 in_progress_current_play_event_review: bool,
634}
635
636impl PlayStream {
637 #[must_use]
638 pub fn new(game_id: impl Into<GameId>) -> Self {
639 Self {
640 game_id: game_id.into(),
641
642 current_play_idx: 0,
643 in_progress_current_play: false,
644 current_play_review_idx: 0,
645 in_progress_current_play_review: false,
646
647 current_play_event_idx: 0,
648 current_play_event_review_idx: 0,
649 in_progress_current_play_event_review: false,
650 }
651 }
652}
653
654#[derive(Debug, PartialEq, Clone)]
656pub enum PlayStreamEvent<'a> {
657 Start,
659
660 StartPlay(&'a Play),
661 PlayReviewStart(&'a ReviewData, &'a Play),
662 PlayReviewEnd(&'a ReviewData, &'a Play),
663 EndPlay(&'a Play),
664
665 PlayEvent(&'a PlayEvent, &'a Play),
666 PlayEventReviewStart(&'a ReviewData, &'a PlayEvent, &'a Play),
667 PlayEventReviewEnd(&'a ReviewData, &'a PlayEvent, &'a Play),
668
669 GameEnd(&'a Decisions, &'a GameStatLeaders),
670}
671
672impl PlayStream {
673 pub async fn run<F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, request::Error>>(self, f: F) -> Result<(), request::Error> {
678 self.run_with_custom_error::<request::Error, F>(f).await
679 }
680
681 async fn run_current_play<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(&self, mut f: F, current_play: &Play, meta: &LiveFeedMetadata, data: &LiveFeedData, linescore: &Linescore, boxscore: &Boxscore) -> Result<ControlFlow<()>, E> {
683 macro_rules! flow_try {
684 ($($t:tt)*) => {
685 match ($($t)*).await? {
686 ControlFlow::Continue(()) => {},
687 ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
688 }
689 };
690 }
691
692 if !self.in_progress_current_play {
693 flow_try!(f(PlayStreamEvent::StartPlay(current_play), meta, data, linescore, boxscore));
694 }
695 let mut play_events = current_play.play_events.iter().skip(self.current_play_event_idx);
696 if let Some(current_play_event) = play_events.next() {
697 flow_try!(f(PlayStreamEvent::PlayEvent(current_play_event, current_play), meta, data, linescore, boxscore));
698 let mut reviews = current_play_event.reviews.iter().skip(self.current_play_event_review_idx);
699 if let Some(current_review) = reviews.next() {
700 if !self.in_progress_current_play_event_review {
701 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
702 }
703 if !current_review.is_in_progress {
704 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
705 }
706 }
707 for review in reviews {
708 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, current_play_event, current_play), meta, data, linescore, boxscore));
709 if !review.is_in_progress {
710 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, current_play_event, current_play), meta, data, linescore, boxscore));
711 }
712 }
713 }
714 for play_event in play_events {
715 flow_try!(f(PlayStreamEvent::PlayEvent(play_event, current_play), meta, data, linescore, boxscore));
716 for review in &play_event.reviews {
717 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, current_play), meta, data, linescore, boxscore));
718 if !review.is_in_progress {
719 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, current_play), meta, data, linescore, boxscore));
720 }
721 }
722 }
723 let mut reviews = current_play.reviews.iter().skip(self.current_play_review_idx);
724 if let Some(current_review) = reviews.next() {
725 if !self.in_progress_current_play_review {
726 flow_try!(f(PlayStreamEvent::PlayReviewStart(current_review, current_play), meta, data, linescore, boxscore));
727 }
728 if !current_review.is_in_progress {
729 flow_try!(f(PlayStreamEvent::PlayReviewEnd(current_review, current_play), meta, data, linescore, boxscore));
730 }
731 }
732
733 for review in reviews {
734 flow_try!(f(PlayStreamEvent::PlayReviewStart(review, current_play), meta, data, linescore, boxscore));
735 if !review.is_in_progress {
736 flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, current_play), meta, data, linescore, boxscore));
737 }
738 }
739 if current_play.about.is_complete {
740 flow_try!(f(PlayStreamEvent::EndPlay(current_play), meta, data, linescore, boxscore));
741 }
742
743 Ok(ControlFlow::Continue(()))
744 }
745
746 async fn run_next_plays<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(&self, mut f: F, plays: impl Iterator<Item=&Play>, meta: &LiveFeedMetadata, data: &LiveFeedData, linescore: &Linescore, boxscore: &Boxscore) -> Result<ControlFlow<()>, E> {
748 macro_rules! flow_try {
749 ($($t:tt)*) => {
750 match ($($t)*).await? {
751 ControlFlow::Continue(()) => {},
752 ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
753 }
754 };
755 }
756
757 for play in plays {
758 flow_try!(f(PlayStreamEvent::StartPlay(play), meta, data, linescore, boxscore));
759 for play_event in &play.play_events {
760 flow_try!(f(PlayStreamEvent::PlayEvent(play_event, play), meta, data, linescore, boxscore));
761 for review in &play_event.reviews {
762 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, play), meta, data, linescore, boxscore));
763 if !review.is_in_progress {
764 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, play), meta, data, linescore, boxscore));
765 }
766 }
767 }
768 for review in &play.reviews {
769 flow_try!(f(PlayStreamEvent::PlayReviewStart(review, play), meta, data, linescore, boxscore));
770 if !review.is_in_progress {
771 flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, play), meta, data, linescore, boxscore));
772 }
773 }
774 if play.about.is_complete {
775 flow_try!(f(PlayStreamEvent::EndPlay(play), meta, data, linescore, boxscore));
776 }
777 }
778
779 Ok(ControlFlow::Continue(()))
780 }
781
782 fn update_indices(&mut self, plays: &[Play]) {
783 let latest_play = plays.last();
784
785 self.in_progress_current_play = latest_play.is_some_and(|play| !play.about.is_complete);
786 self.current_play_idx = if self.in_progress_current_play { plays.len() - 1 } else { plays.len() };
787
788 let current_play = plays.get(self.current_play_idx);
789 let current_play_event = current_play.and_then(|play| play.play_events.last());
790 let current_play_review = current_play.and_then(|play| play.reviews.last());
791 let current_play_event_review = current_play_event.and_then(|play_event| play_event.reviews.last());
792
793 self.in_progress_current_play_review = current_play_review.is_some_and(|review| review.is_in_progress);
794 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() });
795
796 self.current_play_event_idx = current_play.map_or(0, |play| play.play_events.len());
797
798 self.in_progress_current_play_event_review = current_play_event_review.is_some_and(|review| review.is_in_progress);
799 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() });
800 }
801
802 pub async fn run_with_custom_error<E: From<request::Error>, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(self, f: F) -> Result<(), E> {
807 let feed = LiveFeedRequest::builder().id(self.game_id).build_and_get().await?;
808 Self::with_presupplied_feed(feed, f).await
809 }
810
811 pub async fn with_presupplied_feed<E: From<request::Error>, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(mut feed: LiveFeedResponse, mut f: F) -> Result<(), E> {
816 macro_rules! flow_try {
817 ($($t:tt)*) => {
818 match ($($t)*).await? {
819 ControlFlow::Continue(()) => {},
820 ControlFlow::Break(()) => return Ok(()),
821 }
822 };
823 }
824
825 let mut this = Self::new(feed.id);
826 flow_try!(f(PlayStreamEvent::Start, &feed.meta, &feed.data, &feed.live.linescore, &feed.live.boxscore));
827
828 loop {
829 let since_last_request = Instant::now();
830
831 let LiveFeedResponse { meta, data, live, .. } = &feed;
832 let LiveFeedLiveData { linescore, boxscore, decisions, leaders, plays } = live;
833 let mut plays = plays.iter().skip(this.current_play_idx);
834
835 if let Some(current_play) = plays.next() {
836 flow_try!(this.run_current_play(&mut f, current_play, meta, data, linescore, boxscore));
837 }
838
839 flow_try!(this.run_next_plays(&mut f, plays, meta, data, linescore, boxscore));
840
841 if data.status.abstract_game_code.is_finished() && let Some(decisions) = decisions {
842 let _ = f(PlayStreamEvent::GameEnd(decisions, leaders), meta, data, linescore, boxscore).await?;
843 return Ok(())
844 }
845
846 this.update_indices(&live.plays);
847
848 let total_sleep_time = Duration::from_secs(meta.recommended_poll_rate as _);
849 drop(feed);
850 tokio::time::sleep(total_sleep_time.saturating_sub(since_last_request.elapsed())).await;
851 feed = LiveFeedRequest::builder().id(this.game_id).build_and_get().await?;
852 }
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use std::ops::ControlFlow;
859 use crate::{cache::RequestableEntrypoint, game::{PlayEvent, PlayStream, PlayStreamEvent}};
860
861 #[tokio::test]
862 async fn test_play_stream() {
863 Box::pin(PlayStream::new(822_834).run(async |event, _meta, _data, _linescore, _boxscore| {
864 match event {
865 PlayStreamEvent::Start => println!("GameStart"),
866 PlayStreamEvent::StartPlay(play) => println!("PlayStart; {} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
867 PlayStreamEvent::PlayEvent(play_event, _play) => {
868 print!("PlayEvent; ");
869 match play_event {
870 PlayEvent::Action { details, .. } => println!("{}", details.description),
871 PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
872 PlayEvent::Stepoff { .. } => println!("Stepoff"),
873 PlayEvent::NoPitch { .. } => println!("No Pitch"),
874 PlayEvent::Pickoff { .. } => println!("Pickoff"),
875 }
876 },
877 PlayStreamEvent::PlayEventReviewStart(review, _, _) => println!("PlayEventReviewStart; {}", review.review_type),
878 PlayStreamEvent::PlayEventReviewEnd(review, _, _) => println!("PlayEventReviewEnd; {}", review.review_type),
879 PlayStreamEvent::PlayReviewStart(review, _) => println!("PlayReviewStart; {}", review.review_type),
880 PlayStreamEvent::PlayReviewEnd(review, _) => println!("PlayReviewEnd; {}", review.review_type),
881 PlayStreamEvent::EndPlay(play) => println!("PlayEnd; {}", play.result.completed_play_details.as_ref().expect("Completed play").description),
882 PlayStreamEvent::GameEnd(_, _) => println!("GameEnd"),
883 }
884 Ok(ControlFlow::Continue(()))
885 })).await.unwrap();
886 }
887}