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.
- package/internal/cli/matches.go +96 -0
- package/internal/cli/matches_test.go +71 -0
- package/internal/cli/search.go +156 -0
- package/internal/cricinfo/analysis.go +177 -47
- package/internal/cricinfo/client.go +23 -2
- package/internal/cricinfo/coverage_ledger_test.go +2 -22
- package/internal/cricinfo/entity_index.go +27 -0
- package/internal/cricinfo/matches.go +1036 -22
- package/internal/cricinfo/matches_phase7_test.go +11 -4
- package/internal/cricinfo/normalize_entities.go +67 -35
- package/internal/cricinfo/players.go +236 -2
- package/internal/cricinfo/render_contract.go +139 -37
- package/internal/cricinfo/renderer.go +422 -13
- package/internal/cricinfo/resolver.go +92 -15
- package/internal/cricinfo/teams.go +109 -6
- package/internal/cricinfo/testdata/coverage/cricinfo-field-path-catalog.txt +2536 -0
- package/internal/cricinfo/testdata/coverage/cricinfo-working-templates.tsv +56 -0
- package/package.json +1 -1
|
@@ -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
|
|
105
|
-
t.Fatalf("expected sparse situation to
|
|
104
|
+
if situationResult.Status == ResultStatusEmpty {
|
|
105
|
+
t.Fatalf("expected sparse situation to synthesize live fallback data")
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
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:
|
|
689
|
-
ID:
|
|
690
|
-
LeagueID:
|
|
691
|
-
EventID:
|
|
692
|
-
CompetitionID:
|
|
693
|
-
MatchID:
|
|
694
|
-
TeamID:
|
|
695
|
-
Period:
|
|
696
|
-
PeriodText:
|
|
697
|
-
OverNumber:
|
|
698
|
-
BallNumber:
|
|
699
|
-
ScoreValue:
|
|
700
|
-
ShortText:
|
|
701
|
-
Text:
|
|
702
|
-
HomeScore:
|
|
703
|
-
AwayScore:
|
|
704
|
-
BatsmanRef:
|
|
705
|
-
BowlerRef:
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
|
551
|
-
result = NewPartialResult(EntityPlayerStats, *playerStats,
|
|
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
|
|
696
|
-
ID
|
|
697
|
-
LeagueID
|
|
698
|
-
EventID
|
|
699
|
-
CompetitionID
|
|
700
|
-
MatchID
|
|
701
|
-
TeamID
|
|
702
|
-
Period
|
|
703
|
-
PeriodText
|
|
704
|
-
OverNumber
|
|
705
|
-
BallNumber
|
|
706
|
-
ScoreValue
|
|
707
|
-
ShortText
|
|
708
|
-
Text
|
|
709
|
-
HomeScore
|
|
710
|
-
AwayScore
|
|
711
|
-
BatsmanRef
|
|
712
|
-
BowlerRef
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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 {
|