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