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, 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: DateTime<Utc>,
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: DateTime<Utc>,
76 pub original_date: NaiveDate,
78 pub official_date: NaiveDate,
80 pub sky: DayNight,
82}
83
84impl From<__GameDateTimeStruct> for GameDateTime {
85 fn from(value: __GameDateTimeStruct) -> Self {
86 let date = value.datetime.date_naive();
87 let time = value.ampm.into_24_hour_time(value.time);
88 Self {
89 datetime: NaiveDateTime::new(date, time).and_utc(),
90 original_date: value.original_date,
91 official_date: value.official_date,
92 sky: value.sky,
93 }
94 }
95}
96
97#[derive(Debug, Deserialize, PartialEq, Clone)]
99#[serde(try_from = "__WeatherConditionsStruct")]
100pub struct WeatherConditions {
101 pub condition: String,
102 pub temp: uom::si::f64::ThermodynamicTemperature,
103 pub wind_speed: uom::si::f64::Velocity,
104 pub wind_direction: WindDirectionId,
105}
106
107#[serde_as]
108#[derive(Deserialize)]
109#[doc(hidden)]
110#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
111struct __WeatherConditionsStruct {
112 condition: String,
113 #[serde_as(as = "DisplayFromStr")]
114 temp: i32,
115 wind: String,
116}
117
118impl TryFrom<__WeatherConditionsStruct> for WeatherConditions {
119 type Error = &'static str;
120
121 fn try_from(value: __WeatherConditionsStruct) -> Result<Self, Self::Error> {
122 let (speed, direction) = value.wind.split_once(" mph, ").ok_or("invalid wind format")?;
123 let speed = speed.parse::<i32>().map_err(|_| "invalid wind speed")?;
124 Ok(Self {
125 condition: value.condition,
126 temp: uom::si::f64::ThermodynamicTemperature::new::<uom::si::thermodynamic_temperature::degree_fahrenheit>(value.temp as f64),
127 wind_speed: uom::si::f64::Velocity::new::<uom::si::velocity::mile_per_hour>(speed as f64),
128 wind_direction: WindDirectionId::new(direction),
129 })
130 }
131}
132
133#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
135#[serde(rename_all = "camelCase")]
136#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
137pub struct GameInfo {
138 pub attendance: Option<u32>,
139 #[serde(deserialize_with = "crate::deserialize_datetime")]
140 pub first_pitch: DateTime<Utc>,
141 #[serde(rename = "gameDurationMinutes")]
143 pub game_duration: Option<u32>,
144 #[serde(rename = "delayDurationMinutes")]
146 pub delay_duration: Option<u32>,
147}
148
149#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
151#[serde(rename_all = "camelCase")]
152#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
153pub struct TeamReviewData {
154 pub has_challenges: bool,
155 #[serde(flatten)]
156 pub teams: HomeAway<ResourceUsage>,
157}
158
159#[allow(clippy::struct_excessive_bools, reason = "")]
161#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
162#[serde(rename_all = "camelCase")]
163#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
164pub struct GameTags {
165 no_hitter: bool,
166 perfect_game: bool,
167
168 away_team_no_hitter: bool,
169 away_team_perfect_game: bool,
170
171 home_team_no_hitter: bool,
172 home_team_perfect_game: bool,
173}
174
175#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
177pub enum DoubleHeaderKind {
178 #[serde(rename = "N")]
179 Not,
181
182 #[serde(rename = "Y")]
183 FirstGame,
185
186 #[serde(rename = "S")]
187 SecondGame,
189}
190
191impl DoubleHeaderKind {
192 #[must_use]
193 pub const fn is_double_header(self) -> bool {
194 matches!(self, Self::FirstGame | Self::SecondGame)
195 }
196}
197
198#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Deref, DerefMut, From)]
199pub struct Inning(usize);
200
201impl Display for Inning {
202 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
203 crate::write_nth(self.0, f)
204 }
205}
206
207#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Not)]
209pub enum InningHalf {
210 #[serde(rename = "Top", alias = "top")]
211 Top,
212 #[serde(rename = "Bottom", alias = "bottom")]
213 Bottom,
214}
215
216impl InningHalf {
217 #[must_use]
219 pub const fn unicode_char_filled(self) -> char {
220 match self {
221 Self::Top => '▲',
222 Self::Bottom => '▼',
223 }
224 }
225
226 #[must_use]
228 pub const fn unicode_char_empty(self) -> char {
229 match self {
230 Self::Top => '△',
231 Self::Bottom => '▽',
232 }
233 }
234
235 #[must_use]
237 pub const fn three_char(self) -> &'static str {
238 match self {
239 Self::Top => "Top",
240 Self::Bottom => "Bot",
241 }
242 }
243
244 #[must_use]
246 pub const fn bats(self) -> TeamSide {
247 match self {
248 Self::Top => TeamSide::Away,
249 Self::Bottom => TeamSide::Home,
250 }
251 }
252
253 #[must_use]
255 pub const fn pitches(self) -> TeamSide {
256 match self {
257 Self::Top => TeamSide::Home,
258 Self::Bottom => TeamSide::Away,
259 }
260 }
261}
262
263#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
265#[display("{balls}-{strikes} ({outs} out)")]
266pub struct AtBatCount {
267 pub balls: u8,
268 pub strikes: u8,
269 pub outs: u8,
270}
271
272#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
274#[serde(from = "__RHEStruct")]
275#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
276pub struct RHE {
277 pub runs: usize,
278 pub hits: usize,
279 pub errors: usize,
280 pub left_on_base: usize,
281 pub was_inning_half_played: bool,
283}
284
285#[doc(hidden)]
286#[derive(Deserialize)]
287#[serde(rename_all = "camelCase")]
288struct __RHEStruct {
289 pub runs: Option<usize>,
290 pub hits: usize,
291 pub errors: usize,
292 pub left_on_base: usize,
293
294 #[doc(hidden)]
296 #[serde(rename = "isWinner", default)]
297 pub __is_winner: IgnoredAny,
298}
299
300impl From<__RHEStruct> for RHE {
301 fn from(__RHEStruct { runs, hits, errors, left_on_base, .. }: __RHEStruct) -> Self {
302 Self {
303 runs: runs.unwrap_or(0),
304 hits,
305 errors,
306 left_on_base,
307 was_inning_half_played: runs.is_some(),
308 }
309 }
310}
311
312#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
323#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
324pub struct LabelledValue {
325 pub label: String,
326 #[serde(default)]
327 pub value: String,
328}
329
330#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
331#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
332pub struct SectionedLabelledValues {
333 #[serde(rename = "title")]
334 pub section: String,
335 #[serde(rename = "fieldList")]
336 pub values: Vec<LabelledValue>,
337}
338
339#[allow(clippy::struct_excessive_bools, reason = "not what's happening here")]
341#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
342#[serde(rename_all = "camelCase")]
343#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
344pub struct PlayerGameStatusFlags {
345 pub is_current_batter: bool,
346 pub is_current_pitcher: bool,
347 pub is_on_bench: bool,
348 pub is_substitute: bool,
349}
350
351#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
352#[serde(rename_all = "camelCase")]
353#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
354pub struct Official {
355 pub official: NamedPerson,
356 pub official_type: OfficialType,
357}
358
359#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
360pub enum OfficialType {
361 #[serde(rename = "Home Plate")]
362 HomePlate,
363 #[serde(rename = "First Base")]
364 FirstBase,
365 #[serde(rename = "Second Base")]
366 SecondBase,
367 #[serde(rename = "Third Base")]
368 ThirdBase,
369 #[serde(rename = "Left Field")]
370 LeftField,
371 #[serde(rename = "Right Field")]
372 RightField,
373}
374
375#[derive(Debug, PartialEq, Eq, Copy, Clone)]
387pub struct BattingOrderIndex {
388 pub major: usize,
389 pub minor: usize,
390}
391
392impl<'de> Deserialize<'de> for BattingOrderIndex {
393 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
394 where
395 D: Deserializer<'de>
396 {
397 let v: usize = String::deserialize(deserializer)?.parse().map_err(D::Error::custom)?;
398 Ok(Self {
399 major: v / 100,
400 minor: v % 100,
401 })
402 }
403}
404
405impl Display for BattingOrderIndex {
406 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
407 crate::write_nth(self.major, f)?;
408 if self.minor > 0 {
409 write!(f, " ({})", self.minor + 1)?;
410 }
411 Ok(())
412 }
413}
414
415#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
417#[serde(rename_all = "camelCase")]
418#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
419pub struct Decisions {
420 pub winner: Option<NamedPerson>,
421 pub loser: Option<NamedPerson>,
422 pub save: Option<NamedPerson>,
423}
424
425#[derive(Debug, Deserialize, PartialEq, Clone)]
429#[serde(rename_all = "camelCase")]
430#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
431pub struct GameStatLeaders {
432 #[doc(hidden)]
433 #[serde(rename = "hitDistance", default)]
434 pub __distance: IgnoredAny,
435 #[doc(hidden)]
436 #[serde(rename = "hitSpeed", default)]
437 pub __exit_velocity: IgnoredAny,
438 #[doc(hidden)]
439 #[serde(rename = "pitchSpeed", default)]
440 pub __velocity: IgnoredAny,
441}
442
443#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Display)]
444pub enum Base {
445 #[display("1B")]
446 First,
447 #[display("2B")]
448 Second,
449 #[display("3B")]
450 Third,
451 #[display("HP")]
452 Home,
453}
454
455impl<'de> Deserialize<'de> for Base {
456 #[allow(clippy::too_many_lines, reason = "Visitor impl takes up the bulk, is properly scoped")]
457 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
458 where
459 D: Deserializer<'de>
460 {
461 struct BaseVisitor;
462
463 impl Visitor<'_> for BaseVisitor {
464 type Value = Base;
465
466 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
467 write!(f, "a string or integer representing the base")
468 }
469
470 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
471 where
472 E: Error,
473 {
474 Ok(match v {
475 "1B" | "1" => Base::First,
476 "2B" | "2" => Base::Second,
477 "3B" | "3" => Base::Third,
478 "score" | "HP" | "4B" | "4" => Base::Home,
479 _ => return Err(E::unknown_variant(v, &["1B", "1", "2B" , "2", "3B", "3", "score", "HP", "4B", "4"]))
480 })
481 }
482
483 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
484 where
485 E: Error,
486 {
487 Ok(match v {
488 1 => Base::First,
489 2 => Base::Second,
490 3 => Base::Third,
491 4 => Base::Home,
492 _ => return Err(E::unknown_variant("[a number]", &["1", "2", "3", "4"]))
493 })
494 }
495 }
496
497 deserializer.deserialize_any(BaseVisitor)
498 }
499}
500
501#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
502pub enum ContactHardness {
503 #[serde(rename = "soft")]
504 Soft,
505 #[serde(rename = "medium")]
506 Medium,
507 #[serde(rename = "hard")]
508 Hard,
509}
510
511pub(crate) fn deserialize_players_cache<'de, T: DeserializeOwned, D: Deserializer<'de>>(deserializer: D) -> Result<FxHashMap<PersonId, T>, D::Error> {
512 struct PlayersCacheVisitor<T2: DeserializeOwned>(PhantomData<T2>);
513
514 impl<'de2, T2: DeserializeOwned> serde::de::Visitor<'de2> for PlayersCacheVisitor<T2> {
515 type Value = FxHashMap<PersonId, T2>;
516
517 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
518 formatter.write_str("a map")
519 }
520
521 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
522 where
523 A: MapAccess<'de2>,
524 {
525 let mut values = FxHashMap::default();
526
527 while let Some((key, value)) = map.next_entry()? {
528 let key: String = key;
529 let key = PersonId::new(key.strip_prefix("ID").ok_or_else(|| A::Error::custom("invalid id format"))?.parse::<u32>().map_err(A::Error::custom)?);
530 values.insert(key, value);
531 }
532
533 Ok(values)
534 }
535 }
536
537 deserializer.deserialize_map(PlayersCacheVisitor::<T>(PhantomData))
538}
539
540#[derive(Debug)]
572pub struct PlayStream {
573 game_id: GameId,
574
575 current_play_idx: usize,
576 in_progress_current_play: bool,
577 current_play_review_idx: usize,
578 in_progress_current_play_review: bool,
579
580 current_play_event_idx: usize,
581 current_play_event_review_idx: usize,
582 in_progress_current_play_event_review: bool,
583}
584
585impl PlayStream {
586 #[must_use]
587 pub fn new(game_id: impl Into<GameId>) -> Self {
588 Self {
589 game_id: game_id.into(),
590
591 current_play_idx: 0,
592 in_progress_current_play: false,
593 current_play_review_idx: 0,
594 in_progress_current_play_review: false,
595
596 current_play_event_idx: 0,
597 current_play_event_review_idx: 0,
598 in_progress_current_play_event_review: false,
599 }
600 }
601}
602
603#[derive(Debug, PartialEq, Clone)]
605pub enum PlayStreamEvent<'a> {
606 Start,
608
609 StartPlay(&'a Play),
610 PlayReviewStart(&'a ReviewData, &'a Play),
611 PlayReviewEnd(&'a ReviewData, &'a Play),
612 EndPlay(&'a Play),
613
614 PlayEvent(&'a PlayEvent, &'a Play),
615 PlayEventReviewStart(&'a ReviewData, &'a PlayEvent, &'a Play),
616 PlayEventReviewEnd(&'a ReviewData, &'a PlayEvent, &'a Play),
617
618 GameEnd(&'a Decisions, &'a GameStatLeaders),
619}
620
621impl PlayStream {
622 pub async fn run<F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, request::Error>>(self, f: F) -> Result<(), request::Error> {
627 self.run_with_custom_error::<request::Error, F>(f).await
628 }
629
630 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> {
632 macro_rules! flow_try {
633 ($($t:tt)*) => {
634 match ($($t)*).await? {
635 ControlFlow::Continue(()) => {},
636 ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
637 }
638 };
639 }
640
641 if !self.in_progress_current_play {
642 flow_try!(f(PlayStreamEvent::StartPlay(current_play), meta, data, linescore, boxscore));
643 }
644 let mut play_events = current_play.play_events.iter().skip(self.current_play_event_idx);
645 if let Some(current_play_event) = play_events.next() {
646 flow_try!(f(PlayStreamEvent::PlayEvent(current_play_event, current_play), meta, data, linescore, boxscore));
647 let mut reviews = current_play_event.reviews.iter().skip(self.current_play_event_review_idx);
648 if let Some(current_review) = reviews.next() {
649 if !self.in_progress_current_play_event_review {
650 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
651 }
652 if !current_review.is_in_progress {
653 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
654 }
655 }
656 for review in reviews {
657 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, current_play_event, current_play), meta, data, linescore, boxscore));
658 if !review.is_in_progress {
659 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, current_play_event, current_play), meta, data, linescore, boxscore));
660 }
661 }
662 }
663 for play_event in play_events {
664 flow_try!(f(PlayStreamEvent::PlayEvent(play_event, current_play), meta, data, linescore, boxscore));
665 for review in &play_event.reviews {
666 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, current_play), meta, data, linescore, boxscore));
667 if !review.is_in_progress {
668 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, current_play), meta, data, linescore, boxscore));
669 }
670 }
671 }
672 let mut reviews = current_play.reviews.iter().skip(self.current_play_review_idx);
673 if let Some(current_review) = reviews.next() {
674 if !self.in_progress_current_play_review {
675 flow_try!(f(PlayStreamEvent::PlayReviewStart(current_review, current_play), meta, data, linescore, boxscore));
676 }
677 if !current_review.is_in_progress {
678 flow_try!(f(PlayStreamEvent::PlayReviewEnd(current_review, current_play), meta, data, linescore, boxscore));
679 }
680 }
681
682 for review in reviews {
683 flow_try!(f(PlayStreamEvent::PlayReviewStart(review, current_play), meta, data, linescore, boxscore));
684 if !review.is_in_progress {
685 flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, current_play), meta, data, linescore, boxscore));
686 }
687 }
688 if current_play.about.is_complete {
689 flow_try!(f(PlayStreamEvent::EndPlay(current_play), meta, data, linescore, boxscore));
690 }
691
692 Ok(ControlFlow::Continue(()))
693 }
694
695 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> {
697 macro_rules! flow_try {
698 ($($t:tt)*) => {
699 match ($($t)*).await? {
700 ControlFlow::Continue(()) => {},
701 ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
702 }
703 };
704 }
705
706 for play in plays {
707 flow_try!(f(PlayStreamEvent::StartPlay(play), meta, data, linescore, boxscore));
708 for play_event in &play.play_events {
709 flow_try!(f(PlayStreamEvent::PlayEvent(play_event, play), meta, data, linescore, boxscore));
710 for review in &play_event.reviews {
711 flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, play), meta, data, linescore, boxscore));
712 if !review.is_in_progress {
713 flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, play), meta, data, linescore, boxscore));
714 }
715 }
716 }
717 for review in &play.reviews {
718 flow_try!(f(PlayStreamEvent::PlayReviewStart(review, play), meta, data, linescore, boxscore));
719 if !review.is_in_progress {
720 flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, play), meta, data, linescore, boxscore));
721 }
722 }
723 if play.about.is_complete {
724 flow_try!(f(PlayStreamEvent::EndPlay(play), meta, data, linescore, boxscore));
725 }
726 }
727
728 Ok(ControlFlow::Continue(()))
729 }
730
731 fn update_indices(&mut self, plays: &[Play]) {
732 let latest_play = plays.last();
733
734 self.in_progress_current_play = latest_play.is_some_and(|play| !play.about.is_complete);
735 self.current_play_idx = if self.in_progress_current_play { plays.len() - 1 } else { plays.len() };
736
737 let current_play = plays.get(self.current_play_idx);
738 let current_play_event = current_play.and_then(|play| play.play_events.last());
739 let current_play_review = current_play.and_then(|play| play.reviews.last());
740 let current_play_event_review = current_play_event.and_then(|play_event| play_event.reviews.last());
741
742 self.in_progress_current_play_review = current_play_review.is_some_and(|review| review.is_in_progress);
743 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() });
744
745 self.current_play_event_idx = current_play.map_or(0, |play| play.play_events.len());
746
747 self.in_progress_current_play_event_review = current_play_event_review.is_some_and(|review| review.is_in_progress);
748 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() });
749 }
750
751 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> {
756 let feed = LiveFeedRequest::builder().id(self.game_id).build_and_get().await?;
757 Self::with_presupplied_feed(feed, f).await
758 }
759
760 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> {
765 macro_rules! flow_try {
766 ($($t:tt)*) => {
767 match ($($t)*).await? {
768 ControlFlow::Continue(()) => {},
769 ControlFlow::Break(()) => return Ok(()),
770 }
771 };
772 }
773
774 let mut this = Self::new(feed.id);
775 flow_try!(f(PlayStreamEvent::Start, &feed.meta, &feed.data, &feed.live.linescore, &feed.live.boxscore));
776
777 loop {
778 let since_last_request = Instant::now();
779
780 let LiveFeedResponse { meta, data, live, .. } = &feed;
781 let LiveFeedLiveData { linescore, boxscore, decisions, leaders, plays } = live;
782 let mut plays = plays.iter().skip(this.current_play_idx);
783
784 if let Some(current_play) = plays.next() {
785 flow_try!(this.run_current_play(&mut f, current_play, meta, data, linescore, boxscore));
786 }
787
788 flow_try!(this.run_next_plays(&mut f, plays, meta, data, linescore, boxscore));
789
790 if data.status.abstract_game_code.is_finished() && let Some(decisions) = decisions {
791 let _ = f(PlayStreamEvent::GameEnd(decisions, leaders), meta, data, linescore, boxscore).await?;
792 return Ok(())
793 }
794
795 this.update_indices(&live.plays);
796
797 let total_sleep_time = Duration::from_secs(meta.recommended_poll_rate as _);
798 drop(feed);
799 tokio::time::sleep(total_sleep_time.saturating_sub(since_last_request.elapsed())).await;
800 feed = LiveFeedRequest::builder().id(this.game_id).build_and_get().await?;
801 }
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use std::ops::ControlFlow;
808 use crate::{cache::RequestableEntrypoint, game::{PlayEvent, PlayStream, PlayStreamEvent}};
809
810 #[tokio::test]
811 async fn test_play_stream() {
812 PlayStream::new(822_834).run(async |event, _meta, _data, _linescore, _boxscore| {
813 match event {
814 PlayStreamEvent::Start => println!("GameStart"),
815 PlayStreamEvent::StartPlay(play) => println!("PlayStart; {} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
816 PlayStreamEvent::PlayEvent(play_event, _play) => {
817 print!("PlayEvent; ");
818 match play_event {
819 PlayEvent::Action { details, .. } => println!("{}", details.description),
820 PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
821 PlayEvent::Stepoff { .. } => println!("Stepoff"),
822 PlayEvent::NoPitch { .. } => println!("No Pitch"),
823 PlayEvent::Pickoff { .. } => println!("Pickoff"),
824 }
825 },
826 PlayStreamEvent::PlayEventReviewStart(review, _, _) => println!("PlayEventReviewStart; {}", review.review_type),
827 PlayStreamEvent::PlayEventReviewEnd(review, _, _) => println!("PlayEventReviewEnd; {}", review.review_type),
828 PlayStreamEvent::PlayReviewStart(review, _) => println!("PlayReviewStart; {}", review.review_type),
829 PlayStreamEvent::PlayReviewEnd(review, _) => println!("PlayReviewEnd; {}", review.review_type),
830 PlayStreamEvent::EndPlay(play) => println!("PlayEnd; {}", play.result.completed_play_details.as_ref().expect("Completed play").description),
831 PlayStreamEvent::GameEnd(_, _) => println!("GameEnd"),
832 }
833 Ok(ControlFlow::Continue(()))
834 }).await.unwrap();
835 }
836}