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::{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, 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(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#[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#[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#[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 #[serde(rename = "gameDurationMinutes")]
139 pub game_duration: Option<u32>,
140 #[serde(rename = "delayDurationMinutes")]
142 pub delay_duration: Option<u32>,
143}
144
145#[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#[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#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
173pub enum DoubleHeaderKind {
174 #[serde(rename = "N")]
175 Not,
177
178 #[serde(rename = "Y")]
179 FirstGame,
181
182 #[serde(rename = "S")]
183 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#[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 #[must_use]
215 pub const fn unicode_char_filled(self) -> char {
216 match self {
217 Self::Top => '▲',
218 Self::Bottom => '▼',
219 }
220 }
221
222 #[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]
233 pub const fn three_char(self) -> &'static str {
234 match self {
235 Self::Top => "Top",
236 Self::Bottom => "Bot",
237 }
238 }
239
240 #[must_use]
242 pub const fn bats(self) -> TeamSide {
243 match self {
244 Self::Top => TeamSide::Away,
245 Self::Bottom => TeamSide::Home,
246 }
247 }
248
249 #[must_use]
251 pub const fn pitches(self) -> TeamSide {
252 match self {
253 Self::Top => TeamSide::Home,
254 Self::Bottom => TeamSide::Away,
255 }
256 }
257}
258
259#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
261#[display("{balls}-{strikes} ({outs} out)")]
262pub struct AtBatCount {
263 pub balls: u8,
264 pub strikes: u8,
265 pub outs: u8,
266}
267
268#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
270#[serde(from = "__RHEStruct")]
271#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
272pub struct RHE {
273 pub runs: usize,
274 pub hits: usize,
275 pub errors: usize,
276 pub left_on_base: usize,
277 pub was_inning_half_played: bool,
279}
280
281#[doc(hidden)]
282#[derive(Deserialize)]
283#[serde(rename_all = "camelCase")]
284struct __RHEStruct {
285 pub runs: Option<usize>,
286 pub hits: usize,
287 pub errors: usize,
288 pub left_on_base: usize,
289
290 #[doc(hidden)]
292 #[serde(rename = "isWinner", default)]
293 pub __is_winner: IgnoredAny,
294}
295
296impl From<__RHEStruct> for RHE {
297 fn from(__RHEStruct { runs, hits, errors, left_on_base, .. }: __RHEStruct) -> Self {
298 Self {
299 runs: runs.unwrap_or(0),
300 hits,
301 errors,
302 left_on_base,
303 was_inning_half_played: runs.is_some(),
304 }
305 }
306}
307
308#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
319#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
320pub struct LabelledValue {
321 pub label: String,
322 #[serde(default)]
323 pub value: String,
324}
325
326#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
327#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
328pub struct SectionedLabelledValues {
329 #[serde(rename = "title")]
330 pub section: String,
331 #[serde(rename = "fieldList")]
332 pub values: Vec<LabelledValue>,
333}
334
335#[allow(clippy::struct_excessive_bools, reason = "not what's happening here")]
337#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
338#[serde(rename_all = "camelCase")]
339#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
340pub struct PlayerGameStatusFlags {
341 pub is_current_batter: bool,
342 pub is_current_pitcher: bool,
343 pub is_on_bench: bool,
344 pub is_substitute: bool,
345}
346
347#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
348#[serde(rename_all = "camelCase")]
349#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
350pub struct Official {
351 pub official: NamedPerson,
352 pub official_type: OfficialType,
353}
354
355#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
356pub enum OfficialType {
357 #[serde(rename = "Home Plate")]
358 HomePlate,
359 #[serde(rename = "First Base")]
360 FirstBase,
361 #[serde(rename = "Second Base")]
362 SecondBase,
363 #[serde(rename = "Third Base")]
364 ThirdBase,
365 #[serde(rename = "Left Field")]
366 LeftField,
367 #[serde(rename = "Right Field")]
368 RightField,
369}
370
371#[derive(Debug, PartialEq, Eq, Copy, Clone)]
383pub struct BattingOrderIndex {
384 pub major: usize,
385 pub minor: usize,
386}
387
388impl<'de> Deserialize<'de> for BattingOrderIndex {
389 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
390 where
391 D: Deserializer<'de>
392 {
393 let v: usize = String::deserialize(deserializer)?.parse().map_err(D::Error::custom)?;
394 Ok(Self {
395 major: v / 100,
396 minor: v % 100,
397 })
398 }
399}
400
401impl Display for BattingOrderIndex {
402 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
403 crate::write_nth(self.major, f)?;
404 if self.minor > 0 {
405 write!(f, " ({})", self.minor + 1)?;
406 }
407 Ok(())
408 }
409}
410
411#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
413#[serde(rename_all = "camelCase")]
414#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
415pub struct Decisions {
416 pub winner: Option<NamedPerson>,
417 pub loser: Option<NamedPerson>,
418 pub save: Option<NamedPerson>,
419}
420
421#[derive(Debug, Deserialize, PartialEq, Clone)]
425#[serde(rename_all = "camelCase")]
426#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
427pub struct GameStatLeaders {
428 #[doc(hidden)]
429 #[serde(rename = "hitDistance", default)]
430 pub __distance: IgnoredAny,
431 #[doc(hidden)]
432 #[serde(rename = "hitSpeed", default)]
433 pub __exit_velocity: IgnoredAny,
434 #[doc(hidden)]
435 #[serde(rename = "pitchSpeed", default)]
436 pub __velocity: IgnoredAny,
437}
438
439#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Display)]
440pub enum Base {
441 #[display("1B")]
442 First,
443 #[display("2B")]
444 Second,
445 #[display("3B")]
446 Third,
447 #[display("HP")]
448 Home,
449}
450
451impl<'de> Deserialize<'de> for Base {
452 #[allow(clippy::too_many_lines, reason = "Visitor impl takes up the bulk, is properly scoped")]
453 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
454 where
455 D: Deserializer<'de>
456 {
457 struct BaseVisitor;
458
459 impl Visitor<'_> for BaseVisitor {
460 type Value = Base;
461
462 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
463 write!(f, "a string or integer representing the base")
464 }
465
466 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
467 where
468 E: Error,
469 {
470 Ok(match v {
471 "1B" | "1" => Base::First,
472 "2B" | "2" => Base::Second,
473 "3B" | "3" => Base::Third,
474 "score" | "HP" | "4B" | "4" => Base::Home,
475 _ => return Err(E::unknown_variant(v, &["1B", "1", "2B" , "2", "3B", "3", "score", "HP", "4B", "4"]))
476 })
477 }
478
479 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
480 where
481 E: Error,
482 {
483 Ok(match v {
484 1 => Base::First,
485 2 => Base::Second,
486 3 => Base::Third,
487 4 => Base::Home,
488 _ => return Err(E::unknown_variant("[a number]", &["1", "2", "3", "4"]))
489 })
490 }
491 }
492
493 deserializer.deserialize_any(BaseVisitor)
494 }
495}
496
497#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
498pub enum ContactHardness {
499 #[serde(rename = "soft")]
500 Soft,
501 #[serde(rename = "medium")]
502 Medium,
503 #[serde(rename = "hard")]
504 Hard,
505}
506
507pub(crate) fn deserialize_players_cache<'de, T: DeserializeOwned, D: Deserializer<'de>>(deserializer: D) -> Result<FxHashMap<PersonId, T>, D::Error> {
508 struct PlayersCacheVisitor<T2: DeserializeOwned>(PhantomData<T2>);
509
510 impl<'de2, T2: DeserializeOwned> serde::de::Visitor<'de2> for PlayersCacheVisitor<T2> {
511 type Value = FxHashMap<PersonId, T2>;
512
513 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
514 formatter.write_str("a map")
515 }
516
517 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
518 where
519 A: MapAccess<'de2>,
520 {
521 let mut values = FxHashMap::default();
522
523 while let Some((key, value)) = map.next_entry()? {
524 let key: String = key;
525 let key = PersonId::new(key.strip_prefix("ID").ok_or_else(|| A::Error::custom("invalid id format"))?.parse::<u32>().map_err(A::Error::custom)?);
526 values.insert(key, value);
527 }
528
529 Ok(values)
530 }
531 }
532
533 deserializer.deserialize_map(PlayersCacheVisitor::<T>(PhantomData))
534}
535
536#[derive(Debug)]
568pub struct PlayStream {
569 game_id: GameId,
570
571 current_play_idx: usize,
572 in_progress_current_play: bool,
573 current_play_review_idx: usize,
574 in_progress_current_play_review: bool,
575
576 current_play_event_idx: usize,
577 current_play_event_review_idx: usize,
578 in_progress_current_play_event_review: bool,
579}
580
581impl PlayStream {
582 #[must_use]
583 pub fn new(game_id: impl Into<GameId>) -> Self {
584 Self {
585 game_id: game_id.into(),
586
587 current_play_idx: 0,
588 in_progress_current_play: false,
589 current_play_review_idx: 0,
590 in_progress_current_play_review: false,
591
592 current_play_event_idx: 0,
593 current_play_event_review_idx: 0,
594 in_progress_current_play_event_review: false,
595 }
596 }
597}
598
599#[derive(Debug, PartialEq, Clone)]
601pub enum PlayStreamEvent<'a> {
602 Start,
604
605 StartPlay(&'a Play),
606 PlayReviewStart(&'a ReviewData, &'a Play),
607 PlayReviewEnd(&'a ReviewData, &'a Play),
608 EndPlay(&'a Play),
609
610 PlayEvent(&'a PlayEvent, &'a Play),
611 PlayEventReviewStart(&'a ReviewData, &'a PlayEvent, &'a Play),
612 PlayEventReviewEnd(&'a ReviewData, &'a PlayEvent, &'a Play),
613
614 GameEnd(&'a Decisions, &'a GameStatLeaders),
615}
616
617impl PlayStream {
618 pub async fn run<F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, request::Error>>(self, f: F) -> Result<(), request::Error> {
623 self.run_with_custom_error::<request::Error, F>(f).await
624 }
625
626 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> {
628 macro_rules! flow_try {
629 ($($t:tt)*) => {
630 match ($($t)*).await? {
631 ControlFlow::Continue(()) => {},
632 ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
633 }
634 };
635 }
636
637 if !self.in_progress_current_play {
638 flow_try!(f(PlayStreamEvent::StartPlay(current_play), meta, data, linescore, boxscore));
639 }
640 let mut play_events = current_play.play_events.iter().skip(self.current_play_event_idx);
641 if let Some(current_play_event) = play_events.next() {
642 flow_try!(f(PlayStreamEvent::PlayEvent(current_play_event, current_play), meta, data, linescore, boxscore));
643 let mut reviews = current_play_event.reviews.iter().skip(self.current_play_event_review_idx);
644 if let Some(current_review) = reviews.next() {
645 if !self.in_progress_current_play_event_review {
646 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
647 }
648 if !current_review.is_in_progress {
649 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
650 }
651 }
652 for review in reviews {
653 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, current_play_event, current_play), meta, data, linescore, boxscore));
654 if !review.is_in_progress {
655 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, current_play_event, current_play), meta, data, linescore, boxscore));
656 }
657 }
658 }
659 for play_event in play_events {
660 flow_try!(f(PlayStreamEvent::PlayEvent(play_event, current_play), meta, data, linescore, boxscore));
661 for review in &play_event.reviews {
662 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, current_play), meta, data, linescore, boxscore));
663 if !review.is_in_progress {
664 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, current_play), meta, data, linescore, boxscore));
665 }
666 }
667 }
668 let mut reviews = current_play.reviews.iter().skip(self.current_play_review_idx);
669 if let Some(current_review) = reviews.next() {
670 if !self.in_progress_current_play_review {
671 flow_try!(f(PlayStreamEvent::PlayReviewStart(current_review, current_play), meta, data, linescore, boxscore));
672 }
673 if !current_review.is_in_progress {
674 flow_try!(f(PlayStreamEvent::PlayReviewEnd(current_review, current_play), meta, data, linescore, boxscore));
675 }
676 }
677
678 for review in reviews {
679 flow_try!(f(PlayStreamEvent::PlayReviewStart(review, current_play), meta, data, linescore, boxscore));
680 if !review.is_in_progress {
681 flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, current_play), meta, data, linescore, boxscore));
682 }
683 }
684 if current_play.about.is_complete {
685 flow_try!(f(PlayStreamEvent::EndPlay(current_play), meta, data, linescore, boxscore));
686 }
687
688 Ok(ControlFlow::Continue(()))
689 }
690
691 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> {
693 macro_rules! flow_try {
694 ($($t:tt)*) => {
695 match ($($t)*).await? {
696 ControlFlow::Continue(()) => {},
697 ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
698 }
699 };
700 }
701
702 for play in plays {
703 flow_try!(f(PlayStreamEvent::StartPlay(play), meta, data, linescore, boxscore));
704 for play_event in &play.play_events {
705 flow_try!(f(PlayStreamEvent::PlayEvent(play_event, play), meta, data, linescore, boxscore));
706 for review in &play_event.reviews {
707 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, play), meta, data, linescore, boxscore));
708 if !review.is_in_progress {
709 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, play), meta, data, linescore, boxscore));
710 }
711 }
712 }
713 for review in &play.reviews {
714 flow_try!(f(PlayStreamEvent::PlayReviewStart(review, play), meta, data, linescore, boxscore));
715 if !review.is_in_progress {
716 flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, play), meta, data, linescore, boxscore));
717 }
718 }
719 if play.about.is_complete {
720 flow_try!(f(PlayStreamEvent::EndPlay(play), meta, data, linescore, boxscore));
721 }
722 }
723
724 Ok(ControlFlow::Continue(()))
725 }
726
727 fn update_indices(&mut self, plays: &[Play]) {
728 let latest_play = plays.last();
729
730 self.in_progress_current_play = latest_play.is_some_and(|play| !play.about.is_complete);
731 self.current_play_idx = if self.in_progress_current_play { plays.len() - 1 } else { plays.len() };
732
733 let current_play = plays.get(self.current_play_idx);
734 let current_play_event = current_play.and_then(|play| play.play_events.last());
735 let current_play_review = current_play.and_then(|play| play.reviews.last());
736 let current_play_event_review = current_play_event.and_then(|play_event| play_event.reviews.last());
737
738 self.in_progress_current_play_review = current_play_review.is_some_and(|review| review.is_in_progress);
739 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() });
740
741 self.current_play_event_idx = current_play.map_or(0, |play| play.play_events.len());
742
743 self.in_progress_current_play_event_review = current_play_event_review.is_some_and(|review| review.is_in_progress);
744 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() });
745 }
746
747 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> {
752 let feed = LiveFeedRequest::builder().id(self.game_id).build_and_get().await?;
753 Self::with_presupplied_feed(feed, f).await
754 }
755
756 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> {
761 macro_rules! flow_try {
762 ($($t:tt)*) => {
763 match ($($t)*).await? {
764 ControlFlow::Continue(()) => {},
765 ControlFlow::Break(()) => return Ok(()),
766 }
767 };
768 }
769
770 let mut this = Self::new(feed.id);
771 flow_try!(f(PlayStreamEvent::Start, &feed.meta, &feed.data, &feed.live.linescore, &feed.live.boxscore));
772
773 loop {
774 let since_last_request = Instant::now();
775
776 let LiveFeedResponse { meta, data, live, .. } = &feed;
777 let LiveFeedLiveData { linescore, boxscore, decisions, leaders, plays } = live;
778 let mut plays = plays.iter().skip(this.current_play_idx);
779
780 if let Some(current_play) = plays.next() {
781 flow_try!(this.run_current_play(&mut f, current_play, meta, data, linescore, boxscore));
782 }
783
784 flow_try!(this.run_next_plays(&mut f, plays, meta, data, linescore, boxscore));
785
786 if data.status.abstract_game_code.is_finished() && let Some(decisions) = decisions {
787 let _ = f(PlayStreamEvent::GameEnd(decisions, leaders), meta, data, linescore, boxscore).await?;
788 return Ok(())
789 }
790
791 this.update_indices(&live.plays);
792
793 let total_sleep_time = Duration::from_secs(meta.recommended_poll_rate as _);
794 drop(feed);
795 tokio::time::sleep(total_sleep_time.saturating_sub(since_last_request.elapsed())).await;
796 feed = LiveFeedRequest::builder().id(this.game_id).build_and_get().await?;
797 }
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use std::ops::ControlFlow;
804 use crate::{cache::RequestableEntrypoint, game::{PlayEvent, PlayStream, PlayStreamEvent}};
805
806 #[tokio::test]
807 async fn test_play_stream() {
808 PlayStream::new(822_834).run(async |event, _meta, _data, _linescore, _boxscore| {
809 match event {
810 PlayStreamEvent::Start => println!("GameStart"),
811 PlayStreamEvent::StartPlay(play) => println!("PlayStart; {} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
812 PlayStreamEvent::PlayEvent(play_event, _play) => {
813 print!("PlayEvent; ");
814 match play_event {
815 PlayEvent::Action { details, .. } => println!("{}", details.description),
816 PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
817 PlayEvent::Stepoff { .. } => println!("Stepoff"),
818 PlayEvent::NoPitch { .. } => println!("No Pitch"),
819 PlayEvent::Pickoff { .. } => println!("Pickoff"),
820 }
821 },
822 PlayStreamEvent::PlayEventReviewStart(review, _, _) => println!("PlayEventReviewStart; {}", review.review_type),
823 PlayStreamEvent::PlayEventReviewEnd(review, _, _) => println!("PlayEventReviewEnd; {}", review.review_type),
824 PlayStreamEvent::PlayReviewStart(review, _) => println!("PlayReviewStart; {}", review.review_type),
825 PlayStreamEvent::PlayReviewEnd(review, _) => println!("PlayReviewEnd; {}", review.review_type),
826 PlayStreamEvent::EndPlay(play) => println!("PlayEnd; {}", play.result.completed_play_details.as_ref().expect("Completed play").description),
827 PlayStreamEvent::GameEnd(_, _) => println!("GameEnd"),
828 }
829 Ok(ControlFlow::Continue(()))
830 }).await.unwrap();
831 }
832}