cricinfo-cli-go 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,11 +101,18 @@ func TestMatchServicePhase7ScorecardAndSituation(t *testing.T) {
101
101
  if situationResult.Kind != EntityMatchSituation {
102
102
  t.Fatalf("expected situation kind %q, got %q", EntityMatchSituation, situationResult.Kind)
103
103
  }
104
- if situationResult.Status != ResultStatusEmpty {
105
- t.Fatalf("expected sparse situation to return empty status, got %q", situationResult.Status)
104
+ if situationResult.Status == ResultStatusEmpty {
105
+ t.Fatalf("expected sparse situation to synthesize live fallback data")
106
106
  }
107
- if strings.TrimSpace(situationResult.Message) == "" {
108
- t.Fatalf("expected sparse situation message to be populated")
107
+ situation, ok := situationResult.Data.(*MatchSituation)
108
+ if !ok {
109
+ t.Fatalf("expected situation data type *MatchSituation, got %T", situationResult.Data)
110
+ }
111
+ if situation.Live == nil {
112
+ t.Fatalf("expected synthesized live situation view")
113
+ }
114
+ if len(situation.Live.RecentBalls) == 0 {
115
+ t.Fatalf("expected recent balls in synthesized live situation view")
109
116
  }
110
117
  }
111
118
 
@@ -223,6 +223,7 @@ func NormalizeTeamRosterEntries(data []byte, team Team, scope TeamScope, matchID
223
223
  PlayerRef: athleteRef,
224
224
  DisplayName: displayName,
225
225
  TeamID: team.ID,
226
+ TeamName: nonEmpty(team.ShortName, team.Name, team.ID),
226
227
  TeamRef: team.Ref,
227
228
  MatchID: strings.TrimSpace(matchID),
228
229
  Scope: scope,
@@ -260,6 +261,7 @@ func NormalizeTeamAthletePage(data []byte, team Team) ([]TeamRosterEntry, error)
260
261
  PlayerID: refIDs(playerRef)["athleteId"],
261
262
  PlayerRef: playerRef,
262
263
  TeamID: team.ID,
264
+ TeamName: nonEmpty(team.ShortName, team.Name, team.ID),
263
265
  TeamRef: team.Ref,
264
266
  Scope: TeamScopeGlobal,
265
267
  })
@@ -670,12 +672,18 @@ func NormalizeDeliveryEvent(data []byte) (*DeliveryEvent, error) {
670
672
  dismissal := mapField(payload, "dismissal")
671
673
  batsman := mapField(payload, "batsman")
672
674
  bowler := mapField(payload, "bowler")
675
+ otherBatsman := mapField(payload, "otherBatsman")
676
+ otherBowler := mapField(payload, "otherBowler")
673
677
  fielder := mapField(dismissal, "fielder")
674
678
  batsmanRef := nonEmpty(nestedRef(payload, "batsman", "athlete"), refFromField(payload, "batsman"))
675
679
  bowlerRef := nonEmpty(nestedRef(payload, "bowler", "athlete"), refFromField(payload, "bowler"))
680
+ otherBatsmanRef := nonEmpty(nestedRef(payload, "otherBatsman", "athlete"), refFromField(payload, "otherBatsman"))
681
+ otherBowlerRef := nonEmpty(nestedRef(payload, "otherBowler", "athlete"), refFromField(payload, "otherBowler"))
676
682
  fielderRef := nonEmpty(nestedRef(payload, "dismissal", "fielder", "athlete"), nestedRef(payload, "dismissal", "fielder"))
677
683
  batsmanID := nonEmpty(stringField(batsman, "playerId"), stringField(batsman, "id"), refIDs(batsmanRef)["athleteId"])
678
684
  bowlerID := nonEmpty(stringField(bowler, "playerId"), stringField(bowler, "id"), refIDs(bowlerRef)["athleteId"])
685
+ otherBatsmanID := nonEmpty(stringField(otherBatsman, "playerId"), stringField(otherBatsman, "id"), refIDs(otherBatsmanRef)["athleteId"])
686
+ otherBowlerID := nonEmpty(stringField(otherBowler, "playerId"), stringField(otherBowler, "id"), refIDs(otherBowlerRef)["athleteId"])
679
687
  fielderID := nonEmpty(stringField(fielder, "playerId"), stringField(fielder, "id"), refIDs(fielderRef)["athleteId"])
680
688
  teamRef := refFromField(payload, "team")
681
689
  teamIDs := refIDs(teamRef)
@@ -685,41 +693,65 @@ func NormalizeDeliveryEvent(data []byte) (*DeliveryEvent, error) {
685
693
  bbbTimestamp := int64Field(payload, "bbbTimestamp")
686
694
 
687
695
  event := &DeliveryEvent{
688
- Ref: ref,
689
- ID: nonEmpty(stringField(payload, "id"), ids["detailId"]),
690
- LeagueID: ids["leagueId"],
691
- EventID: ids["eventId"],
692
- CompetitionID: ids["competitionId"],
693
- MatchID: ids["competitionId"],
694
- TeamID: nonEmpty(teamIDs["teamId"], teamIDs["competitorId"]),
695
- Period: intField(payload, "period"),
696
- PeriodText: stringField(payload, "periodText"),
697
- OverNumber: intField(over, "number"),
698
- BallNumber: intField(over, "ball"),
699
- ScoreValue: intField(payload, "scoreValue"),
700
- ShortText: stringField(payload, "shortText"),
701
- Text: stringField(payload, "text"),
702
- HomeScore: stringField(payload, "homeScore"),
703
- AwayScore: stringField(payload, "awayScore"),
704
- BatsmanRef: batsmanRef,
705
- BowlerRef: bowlerRef,
706
- BatsmanPlayerID: batsmanID,
707
- BowlerPlayerID: bowlerID,
708
- FielderPlayerID: fielderID,
709
- AthletePlayerIDs: athletePlayerIDs,
710
- PlayType: playType,
711
- Dismissal: dismissal,
712
- DismissalType: stringField(dismissal, "type"),
713
- DismissalName: nonEmpty(stringField(dismissal, "name"), stringField(dismissal, "type")),
714
- DismissalCard: stringField(dismissal, "dismissalCard"),
715
- DismissalText: stringField(dismissal, "text"),
716
- SpeedKPH: floatField(payload, "speedKPH"),
717
- XCoordinate: xCoordinate,
718
- YCoordinate: yCoordinate,
719
- BBBTimestamp: bbbTimestamp,
720
- CoordinateX: xCoordinate,
721
- CoordinateY: yCoordinate,
722
- Timestamp: bbbTimestamp,
696
+ Ref: ref,
697
+ ID: nonEmpty(stringField(payload, "id"), ids["detailId"]),
698
+ LeagueID: ids["leagueId"],
699
+ EventID: ids["eventId"],
700
+ CompetitionID: ids["competitionId"],
701
+ MatchID: ids["competitionId"],
702
+ TeamID: nonEmpty(teamIDs["teamId"], teamIDs["competitorId"]),
703
+ Period: intField(payload, "period"),
704
+ PeriodText: stringField(payload, "periodText"),
705
+ OverNumber: intField(over, "number"),
706
+ BallNumber: intField(over, "ball"),
707
+ ScoreValue: intField(payload, "scoreValue"),
708
+ ShortText: stringField(payload, "shortText"),
709
+ Text: stringField(payload, "text"),
710
+ HomeScore: stringField(payload, "homeScore"),
711
+ AwayScore: stringField(payload, "awayScore"),
712
+ BatsmanRef: batsmanRef,
713
+ BowlerRef: bowlerRef,
714
+ OtherBatsmanRef: otherBatsmanRef,
715
+ OtherBowlerRef: otherBowlerRef,
716
+ BatsmanPlayerID: batsmanID,
717
+ BowlerPlayerID: bowlerID,
718
+ OtherBatsmanID: otherBatsmanID,
719
+ OtherBowlerID: otherBowlerID,
720
+ FielderPlayerID: fielderID,
721
+ AthletePlayerIDs: athletePlayerIDs,
722
+ BatsmanRuns: intField(batsman, "runs"),
723
+ BatsmanTotalRuns: intField(batsman, "totalRuns"),
724
+ BatsmanBalls: intField(batsman, "faced"),
725
+ BatsmanFours: intField(batsman, "fours"),
726
+ BatsmanSixes: intField(batsman, "sixes"),
727
+ OtherBatterRuns: intField(otherBatsman, "totalRuns"),
728
+ OtherBatterBalls: intField(otherBatsman, "faced"),
729
+ OtherBatterFours: intField(otherBatsman, "fours"),
730
+ OtherBatterSixes: intField(otherBatsman, "sixes"),
731
+ BowlerOvers: floatField(bowler, "overs"),
732
+ BowlerBalls: intField(bowler, "balls"),
733
+ BowlerMaidens: intField(bowler, "maidens"),
734
+ BowlerConceded: intField(bowler, "conceded"),
735
+ BowlerWickets: intField(bowler, "wickets"),
736
+ OtherBowlerOvers: floatField(otherBowler, "overs"),
737
+ OtherBowlerBalls: intField(otherBowler, "balls"),
738
+ OtherBowlerMaidens: intField(otherBowler, "maidens"),
739
+ OtherBowlerConceded: intField(otherBowler, "conceded"),
740
+ OtherBowlerWickets: intField(otherBowler, "wickets"),
741
+ Sequence: intField(payload, "sequence"),
742
+ PlayType: playType,
743
+ Dismissal: dismissal,
744
+ DismissalType: stringField(dismissal, "type"),
745
+ DismissalName: nonEmpty(stringField(dismissal, "name"), stringField(dismissal, "type")),
746
+ DismissalCard: stringField(dismissal, "dismissalCard"),
747
+ DismissalText: stringField(dismissal, "text"),
748
+ SpeedKPH: floatField(payload, "speedKPH"),
749
+ XCoordinate: xCoordinate,
750
+ YCoordinate: yCoordinate,
751
+ BBBTimestamp: bbbTimestamp,
752
+ CoordinateX: xCoordinate,
753
+ CoordinateY: yCoordinate,
754
+ Timestamp: bbbTimestamp,
723
755
  Extensions: extensionsFromMap(payload,
724
756
  "$ref", "id", "period", "periodText", "over", "scoreValue", "shortText", "text", "homeScore", "awayScore",
725
757
  "batsman", "bowler", "playType", "dismissal", "speedKPH", "xCoordinate", "yCoordinate", "bbbTimestamp",
@@ -546,9 +546,36 @@ func (s *PlayerService) statistics(ctx context.Context, query string, opts Playe
546
546
  playerStats.PlayerRef = strings.TrimSpace(lookup.player.Ref)
547
547
  }
548
548
 
549
+ warnings := append([]string{}, lookup.warnings...)
550
+ if playerStatsLooksPlaceholder(playerStats, resolved) {
551
+ fallbackResolved, fallbackStats, fallbackWarnings, ok := s.fallbackPlayerStatsFromMatchContext(ctx, lookup, opts)
552
+ warnings = append(warnings, fallbackWarnings...)
553
+ if ok {
554
+ warnings = append(warnings, "global athlete statistics were placeholder; using match-context statistics")
555
+ compact := compactWarnings(warnings)
556
+ result := NewDataResult(EntityPlayerStats, *fallbackStats)
557
+ if len(compact) > 0 {
558
+ result = NewPartialResult(EntityPlayerStats, *fallbackStats, compact...)
559
+ }
560
+ if fallbackResolved != nil {
561
+ result.RequestedRef = fallbackResolved.RequestedRef
562
+ result.CanonicalRef = fallbackResolved.CanonicalRef
563
+ }
564
+ return result, nil
565
+ }
566
+ warnings = append(warnings, fmt.Sprintf("global athlete statistics for %q appear placeholder and no match-context fallback was found", nonEmpty(lookup.player.DisplayName, lookup.entity.Name, query)))
567
+ result := NewDataResult(EntityPlayerStats, *playerStats)
568
+ if compact := compactWarnings(warnings); len(compact) > 0 {
569
+ result = NewPartialResult(EntityPlayerStats, *playerStats, compact...)
570
+ }
571
+ result.RequestedRef = resolved.RequestedRef
572
+ result.CanonicalRef = resolved.CanonicalRef
573
+ return result, nil
574
+ }
575
+
549
576
  result := NewDataResult(EntityPlayerStats, *playerStats)
550
- if len(lookup.warnings) > 0 {
551
- result = NewPartialResult(EntityPlayerStats, *playerStats, lookup.warnings...)
577
+ if compact := compactWarnings(warnings); len(compact) > 0 {
578
+ result = NewPartialResult(EntityPlayerStats, *playerStats, compact...)
552
579
  }
553
580
  result.RequestedRef = resolved.RequestedRef
554
581
  result.CanonicalRef = resolved.CanonicalRef
@@ -603,6 +630,213 @@ func (s *PlayerService) resolvePlayerLookup(ctx context.Context, query string, o
603
630
  }, nil
604
631
  }
605
632
 
633
+ func playerStatsLooksPlaceholder(stats *PlayerStatistics, resolved *ResolvedDocument) bool {
634
+ if stats == nil {
635
+ return true
636
+ }
637
+ if hasMeaningfulStatValues(stats.Categories) {
638
+ return false
639
+ }
640
+
641
+ playerRefID := strings.TrimSpace(refIDs(stats.PlayerRef)["athleteId"])
642
+ if playerRefID == "" {
643
+ return true
644
+ }
645
+ if strings.Contains(strings.TrimSpace(stats.Ref), "/athletes//statistics") {
646
+ return true
647
+ }
648
+ if resolved != nil && strings.Contains(strings.TrimSpace(resolved.CanonicalRef), "/athletes//statistics") {
649
+ return true
650
+ }
651
+ return false
652
+ }
653
+
654
+ func hasMeaningfulStatValues(categories []StatCategory) bool {
655
+ for _, category := range categories {
656
+ for _, stat := range category.Stats {
657
+ display := strings.TrimSpace(stat.DisplayValue)
658
+ if display != "" && display != "-" && display != "0" && display != "0.0" && display != "0.00" {
659
+ return true
660
+ }
661
+ if statAsFloat(stat) > 0 {
662
+ return true
663
+ }
664
+ }
665
+ }
666
+ return false
667
+ }
668
+
669
+ func (s *PlayerService) fallbackPlayerStatsFromMatchContext(
670
+ ctx context.Context,
671
+ lookup *playerLookup,
672
+ opts PlayerLookupOptions,
673
+ ) (*ResolvedDocument, *PlayerStatistics, []string, bool) {
674
+ if lookup == nil || s == nil || s.resolver == nil {
675
+ return nil, nil, nil, false
676
+ }
677
+
678
+ warnings := make([]string, 0)
679
+ matchHelper := &MatchService{client: s.client, resolver: s.resolver}
680
+ candidateIDs := compactWarnings([]string{
681
+ strings.TrimSpace(lookup.player.ID),
682
+ strings.TrimSpace(lookup.entity.ID),
683
+ strings.TrimSpace(refIDs(lookup.player.Ref)["athleteId"]),
684
+ })
685
+ candidateNames := compactWarnings([]string{
686
+ strings.TrimSpace(lookup.player.DisplayName),
687
+ strings.TrimSpace(lookup.player.FullName),
688
+ strings.TrimSpace(lookup.player.Name),
689
+ strings.TrimSpace(lookup.player.BattingName),
690
+ strings.TrimSpace(lookup.player.FieldingName),
691
+ strings.TrimSpace(lookup.entity.Name),
692
+ strings.TrimSpace(lookup.entity.ShortName),
693
+ })
694
+ playerQuery := nonEmpty(
695
+ strings.TrimSpace(lookup.player.ID),
696
+ strings.TrimSpace(lookup.player.DisplayName),
697
+ strings.TrimSpace(lookup.player.FullName),
698
+ strings.TrimSpace(lookup.player.BattingName),
699
+ strings.TrimSpace(lookup.player.FieldingName),
700
+ strings.TrimSpace(lookup.entity.Name),
701
+ )
702
+ seenMatch := map[string]struct{}{}
703
+ candidateMatches := make([]Match, 0, 40)
704
+ appendCandidateMatch := func(match Match) {
705
+ key := nonEmpty(strings.TrimSpace(match.ID), strings.TrimSpace(match.Ref))
706
+ if key == "" {
707
+ return
708
+ }
709
+ if _, ok := seenMatch[key]; ok {
710
+ return
711
+ }
712
+ seenMatch[key] = struct{}{}
713
+ candidateMatches = append(candidateMatches, match)
714
+ }
715
+
716
+ appendMatchesFromListResult := func(result NormalizedResult, source string) {
717
+ if result.Status == ResultStatusError {
718
+ if strings.TrimSpace(result.Message) != "" {
719
+ warnings = append(warnings, fmt.Sprintf("%s match-context source failed: %s", source, result.Message))
720
+ }
721
+ return
722
+ }
723
+ warnings = append(warnings, result.Warnings...)
724
+ for _, item := range result.Items {
725
+ switch typed := item.(type) {
726
+ case Match:
727
+ appendCandidateMatch(typed)
728
+ case *Match:
729
+ if typed != nil {
730
+ appendCandidateMatch(*typed)
731
+ }
732
+ }
733
+ }
734
+ }
735
+
736
+ liveResult, liveErr := matchHelper.Live(ctx, MatchListOptions{
737
+ Limit: 24,
738
+ LeagueID: strings.TrimSpace(opts.LeagueID),
739
+ })
740
+ if liveErr != nil {
741
+ warnings = append(warnings, fmt.Sprintf("match-context live source failed: %v", liveErr))
742
+ } else {
743
+ appendMatchesFromListResult(liveResult, "live")
744
+ }
745
+
746
+ listResult, listErr := matchHelper.List(ctx, MatchListOptions{
747
+ Limit: 36,
748
+ LeagueID: strings.TrimSpace(opts.LeagueID),
749
+ })
750
+ if listErr != nil {
751
+ warnings = append(warnings, fmt.Sprintf("match-context list source failed: %v", listErr))
752
+ } else {
753
+ appendMatchesFromListResult(listResult, "list")
754
+ }
755
+
756
+ if len(candidateMatches) == 0 {
757
+ matchSearch, err := s.resolver.Search(ctx, EntityMatch, "", ResolveOptions{
758
+ Limit: 24,
759
+ LeagueID: strings.TrimSpace(opts.LeagueID),
760
+ })
761
+ if err != nil {
762
+ warnings = append(warnings, fmt.Sprintf("match-context fallback unavailable: %v", err))
763
+ return nil, nil, compactWarnings(warnings), false
764
+ }
765
+ warnings = append(warnings, matchSearch.Warnings...)
766
+
767
+ for _, entity := range matchSearch.Entities {
768
+ ref := buildMatchRef(entity)
769
+ if ref == "" {
770
+ continue
771
+ }
772
+ matchResolved, matchErr := matchHelper.resolveRefChainResilient(ctx, ref)
773
+ if matchErr != nil {
774
+ warnings = append(warnings, fmt.Sprintf("match %s: %v", ref, matchErr))
775
+ continue
776
+ }
777
+ match, normalizeErr := NormalizeMatch(matchResolved.Body)
778
+ if normalizeErr != nil {
779
+ warnings = append(warnings, fmt.Sprintf("match %s: %v", matchResolved.CanonicalRef, normalizeErr))
780
+ continue
781
+ }
782
+ appendCandidateMatch(*match)
783
+ }
784
+ }
785
+
786
+ if len(candidateMatches) == 0 {
787
+ warnings = append(warnings, "match-context fallback found no recent matches")
788
+ return nil, nil, compactWarnings(warnings), false
789
+ }
790
+
791
+ for _, match := range candidateMatches {
792
+ team, roster, routeWarnings, found := s.findPlayerRosterEntry(ctx, match, playerQuery, candidateIDs, candidateNames)
793
+ warnings = append(warnings, routeWarnings...)
794
+ if !found {
795
+ continue
796
+ }
797
+
798
+ team = s.enrichTeamIdentityFromIndex(team)
799
+ roster = s.enrichRosterEntryFromIndex(roster)
800
+ statsRef := rosterPlayerStatisticsRef(match, team, roster)
801
+ if statsRef == "" {
802
+ warnings = append(warnings, fmt.Sprintf("match %s has no roster statistics route for player %s", match.ID, nonEmpty(roster.PlayerID, lookup.player.ID)))
803
+ continue
804
+ }
805
+
806
+ statsResolved, categories, statsErr := s.fetchStatCategories(ctx, statsRef)
807
+ if statsErr != nil {
808
+ warnings = append(warnings, fmt.Sprintf("player statistics %s: %v", statsRef, statsErr))
809
+ continue
810
+ }
811
+ if !hasMeaningfulStatValues(categories) {
812
+ warnings = append(warnings, fmt.Sprintf("player statistics %s returned no meaningful values", statsResolved.CanonicalRef))
813
+ continue
814
+ }
815
+
816
+ playerID := nonEmpty(strings.TrimSpace(lookup.player.ID), strings.TrimSpace(lookup.entity.ID), strings.TrimSpace(roster.PlayerID))
817
+ playerRef := nonEmpty(strings.TrimSpace(lookup.player.Ref), strings.TrimSpace(roster.PlayerRef))
818
+ stats := &PlayerStatistics{
819
+ Ref: statsResolved.CanonicalRef,
820
+ PlayerID: playerID,
821
+ PlayerRef: playerRef,
822
+ SplitID: "match-context",
823
+ Name: "Match Context",
824
+ Abbreviation: "LIVE",
825
+ Categories: categories,
826
+ Extensions: map[string]any{
827
+ "source": "match-context-fallback",
828
+ "matchId": strings.TrimSpace(match.ID),
829
+ "teamId": strings.TrimSpace(team.ID),
830
+ "teamName": teamDisplayLabel(team),
831
+ "statisticsRef": strings.TrimSpace(statsResolved.CanonicalRef),
832
+ },
833
+ }
834
+ return statsResolved, stats, compactWarnings(warnings), true
835
+ }
836
+
837
+ return nil, nil, compactWarnings(warnings), false
838
+ }
839
+
606
840
  func (s *PlayerService) enrichPlayerProfile(ctx context.Context, player *Player) {
607
841
  if s == nil || s.resolver == nil || player == nil {
608
842
  return
@@ -16,6 +16,7 @@ const (
16
16
  EntityMatch EntityKind = "match"
17
17
  EntityMatchScorecard EntityKind = "match_scorecard"
18
18
  EntityMatchSituation EntityKind = "match_situation"
19
+ EntityMatchDuel EntityKind = "match_duel"
19
20
  EntityMatchPhases EntityKind = "match_phases"
20
21
  EntityCompetition EntityKind = "competition"
21
22
  EntityCompOfficial EntityKind = "competition_official"
@@ -226,9 +227,78 @@ type MatchSituation struct {
226
227
  MatchID string `json:"matchId,omitempty"`
227
228
  OddsRef string `json:"oddsRef,omitempty"`
228
229
  Data map[string]any `json:"data,omitempty"`
230
+ Live *MatchLiveView `json:"live,omitempty"`
229
231
  Extensions map[string]any `json:"extensions,omitempty"`
230
232
  }
231
233
 
234
+ // MatchLiveView is a synthesized fan-first live snapshot when upstream situation payload is sparse.
235
+ type MatchLiveView struct {
236
+ Fixture string `json:"fixture,omitempty"`
237
+ Status string `json:"status,omitempty"`
238
+ Score string `json:"score,omitempty"`
239
+ Overs string `json:"overs,omitempty"`
240
+ CurrentOver int `json:"currentOver,omitempty"`
241
+ BallInOver int `json:"ballInOver,omitempty"`
242
+ BattingTeam string `json:"battingTeam,omitempty"`
243
+ BowlingTeam string `json:"bowlingTeam,omitempty"`
244
+ Batters []LiveBatterView `json:"batters,omitempty"`
245
+ Bowlers []LiveBowlerView `json:"bowlers,omitempty"`
246
+ RecentBalls []DeliveryEvent `json:"recentBalls,omitempty"`
247
+ CurrentBalls []DeliveryEvent `json:"currentOverBalls,omitempty"`
248
+ LastDetailID string `json:"lastDetailId,omitempty"`
249
+ LastUpdateMS int64 `json:"lastUpdateMs,omitempty"`
250
+ SnapshotAt string `json:"snapshotAt,omitempty"`
251
+ SourceRoute string `json:"sourceRoute,omitempty"`
252
+ Stale bool `json:"stale"`
253
+ StaleReason string `json:"staleReason,omitempty"`
254
+ }
255
+
256
+ // LiveBatterView captures in-progress batter figures.
257
+ type LiveBatterView struct {
258
+ PlayerID string `json:"playerId,omitempty"`
259
+ PlayerName string `json:"playerName,omitempty"`
260
+ Runs int `json:"runs,omitempty"`
261
+ Balls int `json:"balls,omitempty"`
262
+ Fours int `json:"fours,omitempty"`
263
+ Sixes int `json:"sixes,omitempty"`
264
+ StrikeRate float64 `json:"strikeRate,omitempty"`
265
+ OnStrike bool `json:"onStrike"`
266
+ }
267
+
268
+ // LiveBowlerView captures in-progress bowler figures.
269
+ type LiveBowlerView struct {
270
+ PlayerID string `json:"playerId,omitempty"`
271
+ PlayerName string `json:"playerName,omitempty"`
272
+ Overs float64 `json:"overs,omitempty"`
273
+ Balls int `json:"balls,omitempty"`
274
+ Maidens int `json:"maidens,omitempty"`
275
+ Conceded int `json:"conceded,omitempty"`
276
+ Wickets int `json:"wickets,omitempty"`
277
+ Economy float64 `json:"economy,omitempty"`
278
+ }
279
+
280
+ // MatchDuel summarizes a batter-vs-bowler matchup in one match.
281
+ type MatchDuel struct {
282
+ MatchID string `json:"matchId,omitempty"`
283
+ Fixture string `json:"fixture,omitempty"`
284
+ Score string `json:"score,omitempty"`
285
+ BatterID string `json:"batterId,omitempty"`
286
+ BatterName string `json:"batterName,omitempty"`
287
+ BowlerID string `json:"bowlerId,omitempty"`
288
+ BowlerName string `json:"bowlerName,omitempty"`
289
+ Balls int `json:"balls,omitempty"`
290
+ Runs int `json:"runs,omitempty"`
291
+ Dots int `json:"dots,omitempty"`
292
+ Fours int `json:"fours,omitempty"`
293
+ Sixes int `json:"sixes,omitempty"`
294
+ Wickets int `json:"wickets,omitempty"`
295
+ StrikeRate float64 `json:"strikeRate,omitempty"`
296
+ RecentBalls []DeliveryEvent `json:"recentBalls,omitempty"`
297
+ LastUpdateMS int64 `json:"lastUpdateMs,omitempty"`
298
+ SnapshotAt string `json:"snapshotAt,omitempty"`
299
+ SourceRoute string `json:"sourceRoute,omitempty"`
300
+ }
301
+
232
302
  // Competition is the normalized competition metadata root view.
233
303
  type Competition struct {
234
304
  Ref string `json:"ref,omitempty"`
@@ -488,6 +558,7 @@ type TeamRosterEntry struct {
488
558
  PlayerRef string `json:"playerRef,omitempty"`
489
559
  DisplayName string `json:"displayName,omitempty"`
490
560
  TeamID string `json:"teamId,omitempty"`
561
+ TeamName string `json:"teamName,omitempty"`
491
562
  TeamRef string `json:"teamRef,omitempty"`
492
563
  MatchID string `json:"matchId,omitempty"`
493
564
  Scope TeamScope `json:"scope,omitempty"`
@@ -692,43 +763,67 @@ type InningsWicket struct {
692
763
 
693
764
  // DeliveryEvent is the normalized ball-level event shape.
694
765
  type DeliveryEvent struct {
695
- Ref string `json:"ref,omitempty"`
696
- ID string `json:"id,omitempty"`
697
- LeagueID string `json:"leagueId,omitempty"`
698
- EventID string `json:"eventId,omitempty"`
699
- CompetitionID string `json:"competitionId,omitempty"`
700
- MatchID string `json:"matchId,omitempty"`
701
- TeamID string `json:"teamId,omitempty"`
702
- Period int `json:"period,omitempty"`
703
- PeriodText string `json:"periodText,omitempty"`
704
- OverNumber int `json:"overNumber,omitempty"`
705
- BallNumber int `json:"ballNumber,omitempty"`
706
- ScoreValue int `json:"scoreValue,omitempty"`
707
- ShortText string `json:"shortText,omitempty"`
708
- Text string `json:"text,omitempty"`
709
- HomeScore string `json:"homeScore,omitempty"`
710
- AwayScore string `json:"awayScore,omitempty"`
711
- BatsmanRef string `json:"batsmanRef,omitempty"`
712
- BowlerRef string `json:"bowlerRef,omitempty"`
713
- BatsmanPlayerID string `json:"batsmanPlayerId,omitempty"`
714
- BowlerPlayerID string `json:"bowlerPlayerId,omitempty"`
715
- FielderPlayerID string `json:"fielderPlayerId,omitempty"`
716
- AthletePlayerIDs []string `json:"athletePlayerIds,omitempty"`
717
- Involvement []string `json:"involvement,omitempty"`
718
- PlayType map[string]any `json:"playType,omitempty"`
719
- Dismissal map[string]any `json:"dismissal,omitempty"`
720
- DismissalType string `json:"dismissalType,omitempty"`
721
- DismissalName string `json:"dismissalName,omitempty"`
722
- DismissalCard string `json:"dismissalCard,omitempty"`
723
- DismissalText string `json:"dismissalText,omitempty"`
724
- SpeedKPH float64 `json:"speedKPH,omitempty"`
725
- XCoordinate *float64 `json:"xCoordinate"`
726
- YCoordinate *float64 `json:"yCoordinate"`
727
- BBBTimestamp int64 `json:"bbbTimestamp"`
728
- CoordinateX *float64 `json:"coordinateX,omitempty"`
729
- CoordinateY *float64 `json:"coordinateY,omitempty"`
730
- Timestamp int64 `json:"timestamp,omitempty"`
731
- Extensions map[string]any `json:"extensions,omitempty"`
766
+ Ref string `json:"ref,omitempty"`
767
+ ID string `json:"id,omitempty"`
768
+ LeagueID string `json:"leagueId,omitempty"`
769
+ EventID string `json:"eventId,omitempty"`
770
+ CompetitionID string `json:"competitionId,omitempty"`
771
+ MatchID string `json:"matchId,omitempty"`
772
+ TeamID string `json:"teamId,omitempty"`
773
+ Period int `json:"period,omitempty"`
774
+ PeriodText string `json:"periodText,omitempty"`
775
+ OverNumber int `json:"overNumber,omitempty"`
776
+ BallNumber int `json:"ballNumber,omitempty"`
777
+ ScoreValue int `json:"scoreValue,omitempty"`
778
+ ShortText string `json:"shortText,omitempty"`
779
+ Text string `json:"text,omitempty"`
780
+ HomeScore string `json:"homeScore,omitempty"`
781
+ AwayScore string `json:"awayScore,omitempty"`
782
+ BatsmanRef string `json:"batsmanRef,omitempty"`
783
+ BowlerRef string `json:"bowlerRef,omitempty"`
784
+ OtherBatsmanRef string `json:"otherBatsmanRef,omitempty"`
785
+ OtherBowlerRef string `json:"otherBowlerRef,omitempty"`
786
+ BatsmanPlayerID string `json:"batsmanPlayerId,omitempty"`
787
+ BowlerPlayerID string `json:"bowlerPlayerId,omitempty"`
788
+ OtherBatsmanID string `json:"otherBatsmanPlayerId,omitempty"`
789
+ OtherBowlerID string `json:"otherBowlerPlayerId,omitempty"`
790
+ FielderPlayerID string `json:"fielderPlayerId,omitempty"`
791
+ AthletePlayerIDs []string `json:"athletePlayerIds,omitempty"`
792
+ Involvement []string `json:"involvement,omitempty"`
793
+ BatsmanRuns int `json:"batsmanRuns,omitempty"`
794
+ BatsmanTotalRuns int `json:"batsmanTotalRuns,omitempty"`
795
+ BatsmanBalls int `json:"batsmanBalls,omitempty"`
796
+ BatsmanFours int `json:"batsmanFours,omitempty"`
797
+ BatsmanSixes int `json:"batsmanSixes,omitempty"`
798
+ OtherBatterRuns int `json:"otherBatterRuns,omitempty"`
799
+ OtherBatterBalls int `json:"otherBatterBalls,omitempty"`
800
+ OtherBatterFours int `json:"otherBatterFours,omitempty"`
801
+ OtherBatterSixes int `json:"otherBatterSixes,omitempty"`
802
+ BowlerOvers float64 `json:"bowlerOvers,omitempty"`
803
+ BowlerBalls int `json:"bowlerBalls,omitempty"`
804
+ BowlerMaidens int `json:"bowlerMaidens,omitempty"`
805
+ BowlerConceded int `json:"bowlerConceded,omitempty"`
806
+ BowlerWickets int `json:"bowlerWickets,omitempty"`
807
+ OtherBowlerOvers float64 `json:"otherBowlerOvers,omitempty"`
808
+ OtherBowlerBalls int `json:"otherBowlerBalls,omitempty"`
809
+ OtherBowlerMaidens int `json:"otherBowlerMaidens,omitempty"`
810
+ OtherBowlerConceded int `json:"otherBowlerConceded,omitempty"`
811
+ OtherBowlerWickets int `json:"otherBowlerWickets,omitempty"`
812
+ Sequence int `json:"sequence,omitempty"`
813
+ PlayType map[string]any `json:"playType,omitempty"`
814
+ Dismissal map[string]any `json:"dismissal,omitempty"`
815
+ DismissalType string `json:"dismissalType,omitempty"`
816
+ DismissalName string `json:"dismissalName,omitempty"`
817
+ DismissalCard string `json:"dismissalCard,omitempty"`
818
+ DismissalText string `json:"dismissalText,omitempty"`
819
+ SpeedKPH float64 `json:"speedKPH,omitempty"`
820
+ XCoordinate *float64 `json:"xCoordinate"`
821
+ YCoordinate *float64 `json:"yCoordinate"`
822
+ BBBTimestamp int64 `json:"bbbTimestamp"`
823
+ CoordinateX *float64 `json:"coordinateX,omitempty"`
824
+ CoordinateY *float64 `json:"coordinateY,omitempty"`
825
+ Timestamp int64 `json:"timestamp,omitempty"`
826
+ Extensions map[string]any `json:"extensions,omitempty"`
732
827
  }
733
828
 
734
829
  // StatCategory is the normalized grouped statistics shape.
@@ -962,6 +1057,8 @@ func kindPlural(kind EntityKind) string {
962
1057
  return "match scorecards"
963
1058
  case EntityMatchSituation:
964
1059
  return "match situations"
1060
+ case EntityMatchDuel:
1061
+ return "match duels"
965
1062
  case EntityMatchPhases:
966
1063
  return "match phase reports"
967
1064
  case EntityCompetition:
@@ -1039,11 +1136,16 @@ func kindPlural(kind EntityKind) string {
1039
1136
 
1040
1137
  func compactWarnings(warnings []string) []string {
1041
1138
  out := make([]string, 0, len(warnings))
1139
+ seen := map[string]struct{}{}
1042
1140
  for _, warning := range warnings {
1043
1141
  warning = strings.TrimSpace(warning)
1044
1142
  if warning == "" {
1045
1143
  continue
1046
1144
  }
1145
+ if _, ok := seen[warning]; ok {
1146
+ continue
1147
+ }
1148
+ seen[warning] = struct{}{}
1047
1149
  out = append(out, warning)
1048
1150
  }
1049
1151
  if len(out) == 0 {