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