cricinfo-cli-go 0.1.1 → 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",
@@ -1033,6 +1065,16 @@ func NormalizePartnership(data []byte) (*Partnership, error) {
1033
1065
  ),
1034
1066
  }
1035
1067
 
1068
+ if partnership.WicketNumber == 0 {
1069
+ partnership.WicketNumber = parseInt(ids["partnershipId"])
1070
+ }
1071
+ if partnership.Runs == 0 && partnership.End.Runs > partnership.Start.Runs {
1072
+ partnership.Runs = partnership.End.Runs - partnership.Start.Runs
1073
+ }
1074
+ if partnership.Overs == 0 && partnership.End.Overs > partnership.Start.Overs {
1075
+ partnership.Overs = partnership.End.Overs - partnership.Start.Overs
1076
+ }
1077
+
1036
1078
  return partnership, nil
1037
1079
  }
1038
1080
 
@@ -1097,6 +1139,12 @@ func NormalizeFallOfWicket(data []byte) (*FallOfWicket, error) {
1097
1139
  if fow.WicketNumber == 0 {
1098
1140
  fow.WicketNumber = parseInt(ids["fowId"])
1099
1141
  }
1142
+ if fow.Runs == 0 && fow.RunsScored > 0 {
1143
+ fow.Runs = fow.RunsScored
1144
+ }
1145
+ if fow.RunsScored == 0 && fow.Runs > 0 {
1146
+ fow.RunsScored = fow.Runs
1147
+ }
1100
1148
 
1101
1149
  return fow, nil
1102
1150
  }
@@ -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