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.
@@ -6,6 +6,7 @@ import (
6
6
  "io"
7
7
  "regexp"
8
8
  "sort"
9
+ "strconv"
9
10
  "strings"
10
11
  )
11
12
 
@@ -201,6 +202,15 @@ func renderText(w io.Writer, result NormalizedResult, opts RenderOptions) error
201
202
  lines = append(lines, formatInningsTimelines(itemMap)...)
202
203
  return writeTextLines(w, lines)
203
204
  }
205
+ if result.Kind == EntityMatchSituation {
206
+ itemMap, err := toMap(result.Data, opts.AllFields)
207
+ if err != nil {
208
+ return err
209
+ }
210
+ lines = append(lines, "Match Situation")
211
+ lines = append(lines, formatMatchSituation(itemMap)...)
212
+ return writeTextLines(w, lines)
213
+ }
204
214
  if result.Kind == EntityPlayerStats {
205
215
  itemMap, err := toMap(result.Data, opts.AllFields)
206
216
  if err != nil {
@@ -264,6 +274,15 @@ func renderText(w io.Writer, result NormalizedResult, opts RenderOptions) error
264
274
  lines = append(lines, formatMatchPhases(itemMap)...)
265
275
  return writeTextLines(w, lines)
266
276
  }
277
+ if result.Kind == EntityMatchDuel {
278
+ itemMap, err := toMap(result.Data, opts.AllFields)
279
+ if err != nil {
280
+ return err
281
+ }
282
+ lines = append(lines, "Match Duel")
283
+ lines = append(lines, formatMatchDuel(itemMap)...)
284
+ return writeTextLines(w, lines)
285
+ }
267
286
 
268
287
  itemMap, err := toMap(result.Data, opts.AllFields)
269
288
  if err != nil {
@@ -283,8 +302,35 @@ func renderText(w io.Writer, result NormalizedResult, opts RenderOptions) error
283
302
  return writeTextLines(w, lines)
284
303
  }
285
304
 
305
+ if result.Kind == EntityTeamStatistics || result.Kind == EntityTeamRecords {
306
+ title := "Team Statistics"
307
+ if result.Kind == EntityTeamRecords {
308
+ title = "Team Records"
309
+ }
310
+ lines = append(lines, fmt.Sprintf("%s Categories (%d)", title, len(result.Items)))
311
+ lines = append(lines, formatStatCategoryList(result.Items)...)
312
+ return writeTextLines(w, lines)
313
+ }
314
+
315
+ if result.Kind == EntityStandingsGroup {
316
+ lines = append(lines, formatStandingsGroupList(result.Items)...)
317
+ return writeTextLines(w, lines)
318
+ }
319
+
286
320
  if result.Kind == EntityDeliveryEvent || result.Kind == EntityPlayerDelivery {
287
321
  lines = append(lines, fmt.Sprintf("%s (%d)", titleize(kindPlural(result.Kind)), len(result.Items)))
322
+ if result.Kind == EntityDeliveryEvent {
323
+ source := ""
324
+ switch {
325
+ case strings.Contains(result.RequestedRef, "/details"):
326
+ source = "details"
327
+ case strings.Contains(result.RequestedRef, "/plays"):
328
+ source = "plays"
329
+ }
330
+ if source != "" {
331
+ lines = append(lines, "Source: "+source)
332
+ }
333
+ }
288
334
  for i, item := range result.Items {
289
335
  summary := summarizeDeliveryListItem(item, result.Kind)
290
336
  if strings.TrimSpace(summary) == "" {
@@ -339,19 +385,26 @@ func summarizeDeliveryListItem(item any, kind EntityKind) string {
339
385
  if short == "" {
340
386
  short = joinParts("over "+intString(typed.OverNumber), "ball "+intString(typed.BallNumber))
341
387
  }
388
+ lead := firstNonEmpty(overBallString(typed.OverNumber, typed.BallNumber), "")
389
+ score := firstNonEmpty(scoreLabel(typed.HomeScore), scoreLabel(typed.AwayScore))
342
390
  if kind == EntityPlayerDelivery {
343
- return joinParts(short, strings.Join(typed.Involvement, ","), overBallString(typed.OverNumber, typed.BallNumber))
391
+ return joinParts(lead, short, score, strings.Join(typed.Involvement, ","))
344
392
  }
345
- return short
393
+ return joinParts(lead, short, score)
346
394
  case map[string]any:
347
395
  short := firstNonEmpty(valueString(typed, "shortText"), valueString(typed, "text"))
348
396
  if short == "" {
349
397
  short = joinParts("over "+valueString(typed, "overNumber"), "ball "+valueString(typed, "ballNumber"))
350
398
  }
399
+ if strings.TrimSpace(short) == "/" || strings.TrimSpace(short) == "-" {
400
+ return ""
401
+ }
402
+ lead := overBallLabel(typed)
403
+ score := firstNonEmpty(scoreLabel(valueString(typed, "homeScore")), scoreLabel(valueString(typed, "awayScore")))
351
404
  if kind == EntityPlayerDelivery {
352
- return joinParts(short, involvementLabel(typed), overBallLabel(typed))
405
+ return joinParts(lead, short, score, involvementLabel(typed))
353
406
  }
354
- return short
407
+ return joinParts(lead, short, score)
355
408
  default:
356
409
  return ""
357
410
  }
@@ -361,7 +414,7 @@ func overBallString(over, ball int) string {
361
414
  if over <= 0 || ball <= 0 {
362
415
  return ""
363
416
  }
364
- return fmt.Sprintf("over %d.%d", over, ball)
417
+ return fmt.Sprintf("%d.%d", over, ball)
365
418
  }
366
419
 
367
420
  func intString(value int) string {
@@ -379,6 +432,17 @@ func defaultNumeric(raw string) string {
379
432
  return raw
380
433
  }
381
434
 
435
+ func scoreLabel(raw string) string {
436
+ raw = strings.TrimSpace(raw)
437
+ if raw == "" {
438
+ return ""
439
+ }
440
+ if strings.Contains(raw, "/") {
441
+ return raw
442
+ }
443
+ return ""
444
+ }
445
+
382
446
  func sanitizeValue(value any, allFields bool) (any, error) {
383
447
  blob, err := json.Marshal(value)
384
448
  if err != nil {
@@ -423,6 +487,25 @@ func toMap(value any, allFields bool) (map[string]any, error) {
423
487
  return mapped, nil
424
488
  }
425
489
 
490
+ func mapFromAny(value any) map[string]any {
491
+ if value == nil {
492
+ return nil
493
+ }
494
+ if mapped, ok := value.(map[string]any); ok {
495
+ return mapped
496
+ }
497
+
498
+ blob, err := json.Marshal(value)
499
+ if err != nil {
500
+ return nil
501
+ }
502
+ var mapped map[string]any
503
+ if err := json.Unmarshal(blob, &mapped); err != nil {
504
+ return nil
505
+ }
506
+ return mapped
507
+ }
508
+
426
509
  func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) string {
427
510
  switch kind {
428
511
  case EntityMatch:
@@ -464,6 +547,15 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
464
547
  return joinParts("situation", data)
465
548
  }
466
549
  return joinParts("situation", valueString(entity, "competitionId"))
550
+ case EntityMatchDuel:
551
+ duelLabel := strings.TrimSpace(fmt.Sprintf("%s vs %s",
552
+ firstNonEmpty(valueString(entity, "batterName"), valueString(entity, "batterId")),
553
+ firstNonEmpty(valueString(entity, "bowlerName"), valueString(entity, "bowlerId")),
554
+ ))
555
+ return joinParts(
556
+ duelLabel,
557
+ fmt.Sprintf("%s off %s", valueString(entity, "runs"), valueString(entity, "balls")),
558
+ )
467
559
  case EntityMatchPhases:
468
560
  return joinParts(
469
561
  "match "+firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")),
@@ -520,7 +612,15 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
520
612
  name := firstNonEmpty(valueString(entity, "name"), valueString(entity, "shortName"), valueString(entity, "id"))
521
613
  return joinParts(name, bracket(valueString(entity, "homeAway")))
522
614
  case EntityTeamRoster:
523
- name := firstNonEmpty(valueString(entity, "displayName"), valueString(entity, "playerId"), valueString(entity, "athleteId"))
615
+ name := strings.TrimSpace(valueString(entity, "displayName"))
616
+ if name == "" {
617
+ playerID := firstNonEmpty(valueString(entity, "playerId"), valueString(entity, "athleteId"))
618
+ if playerID != "" {
619
+ name = "Unknown player (" + playerID + ")"
620
+ } else {
621
+ name = "Unknown player"
622
+ }
623
+ }
524
624
  badges := []string{}
525
625
  if valueString(entity, "captain") == "true" {
526
626
  badges = append(badges, "captain")
@@ -528,7 +628,7 @@ func summarizeEntity(entity map[string]any, kind EntityKind, verbose bool) strin
528
628
  if valueString(entity, "active") == "true" {
529
629
  badges = append(badges, "active")
530
630
  }
531
- return joinParts(name, strings.Join(badges, ", "))
631
+ return joinParts(name, firstNonEmpty(valueString(entity, "teamName"), valueString(entity, "teamId")), strings.Join(badges, ", "))
532
632
  case EntityTeamScore:
533
633
  return joinParts(valueString(entity, "displayValue"), valueString(entity, "value"), bracket(valueString(entity, "source")))
534
634
  case EntityTeamLeaders:
@@ -915,7 +1015,7 @@ func formatPlayerProfile(entity map[string]any) []string {
915
1015
  }
916
1016
 
917
1017
  func formatPlayerMatchView(entity map[string]any) []string {
918
- lines := make([]string, 0, 16)
1018
+ lines := make([]string, 0, 48)
919
1019
  if player := valueString(entity, "playerName"); player != "" {
920
1020
  lines = append(lines, "Player: "+player)
921
1021
  }
@@ -931,13 +1031,21 @@ func formatPlayerMatchView(entity map[string]any) []string {
931
1031
  }
932
1032
  }
933
1033
  if batting := sliceValue(entity, "batting"); len(batting) > 0 {
934
- lines = append(lines, fmt.Sprintf("Batting Categories: %d", len(batting)))
1034
+ lines = append(lines, "Batting")
1035
+ lines = append(lines, formatStatCategoryList(batting)...)
935
1036
  }
936
1037
  if bowling := sliceValue(entity, "bowling"); len(bowling) > 0 {
937
- lines = append(lines, fmt.Sprintf("Bowling Categories: %d", len(bowling)))
1038
+ lines = append(lines, "Bowling")
1039
+ lines = append(lines, formatStatCategoryList(bowling)...)
938
1040
  }
939
1041
  if fielding := sliceValue(entity, "fielding"); len(fielding) > 0 {
940
- lines = append(lines, fmt.Sprintf("Fielding Categories: %d", len(fielding)))
1042
+ lines = append(lines, "Fielding")
1043
+ lines = append(lines, formatStatCategoryList(fielding)...)
1044
+ }
1045
+ if len(sliceValue(entity, "batting")) == 0 &&
1046
+ len(sliceValue(entity, "bowling")) == 0 &&
1047
+ len(sliceValue(entity, "fielding")) == 0 {
1048
+ lines = append(lines, "No match statistics categories available.")
941
1049
  }
942
1050
  return lines
943
1051
  }
@@ -992,6 +1100,164 @@ func formatMatchView(entity map[string]any) []string {
992
1100
  return lines
993
1101
  }
994
1102
 
1103
+ func formatMatchSituation(entity map[string]any) []string {
1104
+ lines := make([]string, 0, 64)
1105
+ if matchID := firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")); matchID != "" {
1106
+ lines = append(lines, "Match: "+matchID)
1107
+ }
1108
+
1109
+ live, _ := entity["live"].(map[string]any)
1110
+ if live == nil {
1111
+ if oddsRef := valueString(entity, "oddsRef"); oddsRef != "" {
1112
+ lines = append(lines, "Odds Ref: "+oddsRef)
1113
+ }
1114
+ if data, ok := entity["data"].(map[string]any); ok && len(data) > 0 {
1115
+ lines = append(lines, "Data: "+printableValue(data))
1116
+ }
1117
+ if len(lines) <= 1 {
1118
+ lines = append(lines, "No situation data available for this match.")
1119
+ }
1120
+ return lines
1121
+ }
1122
+
1123
+ if fixture := valueString(live, "fixture"); fixture != "" {
1124
+ lines = append(lines, "Fixture: "+fixture)
1125
+ }
1126
+ if status := valueString(live, "status"); status != "" {
1127
+ lines = append(lines, "Status: "+status)
1128
+ }
1129
+ scoreLine := joinParts(valueString(live, "score"), valueString(live, "overs"))
1130
+ if scoreLine != "" {
1131
+ lines = append(lines, "Score: "+scoreLine)
1132
+ }
1133
+ teamsLine := joinParts(
1134
+ "Batting "+valueString(live, "battingTeam"),
1135
+ "Bowling "+valueString(live, "bowlingTeam"),
1136
+ )
1137
+ if teamsLine != "" {
1138
+ lines = append(lines, teamsLine)
1139
+ }
1140
+ if snapshotAt := valueString(live, "snapshotAt"); snapshotAt != "" {
1141
+ lines = append(lines, "Snapshot: "+snapshotAt)
1142
+ }
1143
+ if stale := valueString(live, "stale"); stale == "true" {
1144
+ lines = append(lines, "Stale: true")
1145
+ if reason := valueString(live, "staleReason"); reason != "" {
1146
+ lines = append(lines, "Stale Reason: "+reason)
1147
+ }
1148
+ }
1149
+
1150
+ batters := sliceValue(live, "batters")
1151
+ if len(batters) > 0 {
1152
+ lines = append(lines, "Batters")
1153
+ for i, raw := range batters {
1154
+ batter, ok := raw.(map[string]any)
1155
+ if !ok {
1156
+ continue
1157
+ }
1158
+ score := fmt.Sprintf("%s(%s)", defaultNumeric(valueString(batter, "runs")), defaultNumeric(valueString(batter, "balls")))
1159
+ boundaries := joinParts("4s "+defaultNumeric(valueString(batter, "fours")), "6s "+defaultNumeric(valueString(batter, "sixes")))
1160
+ row := joinParts(
1161
+ firstNonEmpty(valueString(batter, "playerName"), valueString(batter, "playerId")),
1162
+ score,
1163
+ "SR "+defaultNumeric(valueString(batter, "strikeRate")),
1164
+ boundaries,
1165
+ )
1166
+ if strings.EqualFold(valueString(batter, "onStrike"), "true") {
1167
+ row = joinParts(row, "*")
1168
+ }
1169
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, row))
1170
+ }
1171
+ }
1172
+
1173
+ bowlers := sliceValue(live, "bowlers")
1174
+ if len(bowlers) > 0 {
1175
+ lines = append(lines, "Bowlers")
1176
+ for i, raw := range bowlers {
1177
+ bowler, ok := raw.(map[string]any)
1178
+ if !ok {
1179
+ continue
1180
+ }
1181
+ figures := fmt.Sprintf("%s-%s-%s-%s",
1182
+ oversLabelFromFields(valueString(bowler, "overs"), valueString(bowler, "balls")),
1183
+ defaultNumeric(valueString(bowler, "maidens")),
1184
+ defaultNumeric(valueString(bowler, "conceded")),
1185
+ defaultNumeric(valueString(bowler, "wickets")),
1186
+ )
1187
+ row := joinParts(
1188
+ firstNonEmpty(valueString(bowler, "playerName"), valueString(bowler, "playerId")),
1189
+ figures,
1190
+ "Econ "+defaultNumeric(valueString(bowler, "economy")),
1191
+ )
1192
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, row))
1193
+ }
1194
+ }
1195
+
1196
+ balls := sliceValue(live, "recentBalls")
1197
+ if len(balls) == 0 {
1198
+ balls = sliceValue(live, "currentOverBalls")
1199
+ }
1200
+ if len(balls) > 0 {
1201
+ lines = append(lines, "Recent Balls")
1202
+ for i, raw := range balls {
1203
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, summarizeDeliveryListItem(raw, EntityDeliveryEvent)))
1204
+ }
1205
+ }
1206
+ return lines
1207
+ }
1208
+
1209
+ func formatMatchDuel(entity map[string]any) []string {
1210
+ lines := make([]string, 0, 48)
1211
+ if matchID := valueString(entity, "matchId"); matchID != "" {
1212
+ lines = append(lines, "Match: "+matchID)
1213
+ }
1214
+ if fixture := valueString(entity, "fixture"); fixture != "" {
1215
+ lines = append(lines, "Fixture: "+fixture)
1216
+ }
1217
+ if score := valueString(entity, "score"); score != "" {
1218
+ lines = append(lines, "Score: "+score)
1219
+ }
1220
+ lines = append(lines, "Duel: "+firstNonEmpty(valueString(entity, "batterName"), valueString(entity, "batterId"))+" vs "+firstNonEmpty(valueString(entity, "bowlerName"), valueString(entity, "bowlerId")))
1221
+ summary := joinParts(
1222
+ fmt.Sprintf("%s off %s", defaultNumeric(valueString(entity, "runs")), defaultNumeric(valueString(entity, "balls"))),
1223
+ "SR "+defaultNumeric(valueString(entity, "strikeRate")),
1224
+ "dots "+defaultNumeric(valueString(entity, "dots")),
1225
+ "4s "+defaultNumeric(valueString(entity, "fours")),
1226
+ "6s "+defaultNumeric(valueString(entity, "sixes")),
1227
+ "wkts "+defaultNumeric(valueString(entity, "wickets")),
1228
+ )
1229
+ if strings.TrimSpace(summary) != "" {
1230
+ lines = append(lines, "Summary: "+summary)
1231
+ }
1232
+ if snapshot := valueString(entity, "snapshotAt"); snapshot != "" {
1233
+ lines = append(lines, "Snapshot: "+snapshot)
1234
+ }
1235
+ balls := sliceValue(entity, "recentBalls")
1236
+ if len(balls) > 0 {
1237
+ lines = append(lines, "Recent Duel Balls")
1238
+ for i, raw := range balls {
1239
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, summarizeDeliveryListItem(raw, EntityDeliveryEvent)))
1240
+ }
1241
+ }
1242
+ return lines
1243
+ }
1244
+
1245
+ func oversLabelFromFields(overs string, balls string) string {
1246
+ overs = strings.TrimSpace(overs)
1247
+ if overs != "" && overs != "0" {
1248
+ return overs
1249
+ }
1250
+ balls = strings.TrimSpace(balls)
1251
+ if balls == "" || balls == "0" {
1252
+ return "0.0"
1253
+ }
1254
+ b, err := strconv.Atoi(balls)
1255
+ if err != nil || b < 0 {
1256
+ return "0.0"
1257
+ }
1258
+ return fmt.Sprintf("%d.%d", b/6, b%6)
1259
+ }
1260
+
995
1261
  func formatMatchPhases(entity map[string]any) []string {
996
1262
  lines := make([]string, 0, 64)
997
1263
  if matchID := firstNonEmpty(valueString(entity, "matchId"), valueString(entity, "competitionId")); matchID != "" {
@@ -1266,6 +1532,149 @@ func formatTeamLeaders(entity map[string]any) []string {
1266
1532
  return lines
1267
1533
  }
1268
1534
 
1535
+ func formatStatCategoryList(items []any) []string {
1536
+ lines := make([]string, 0, len(items)*4)
1537
+ for i, rawCategory := range items {
1538
+ category := mapFromAny(rawCategory)
1539
+ if category == nil {
1540
+ continue
1541
+ }
1542
+
1543
+ categoryName := firstNonEmpty(valueString(category, "displayName"), valueString(category, "name"), "Category")
1544
+ lines = append(lines, fmt.Sprintf(" %d. %s", i+1, categoryName))
1545
+
1546
+ stats := sliceValue(category, "stats")
1547
+ if len(stats) == 0 {
1548
+ continue
1549
+ }
1550
+
1551
+ limit := len(stats)
1552
+ if limit > 16 {
1553
+ limit = 16
1554
+ }
1555
+ for j := 0; j < limit; j++ {
1556
+ statMap, ok := stats[j].(map[string]any)
1557
+ if !ok {
1558
+ continue
1559
+ }
1560
+ label := firstNonEmpty(valueString(statMap, "displayName"), valueString(statMap, "name"), valueString(statMap, "abbreviation"))
1561
+ value := firstNonEmpty(valueString(statMap, "displayValue"), valueString(statMap, "value"))
1562
+ row := joinParts(label, value, bracket(valueString(statMap, "abbreviation")))
1563
+ if row == "" {
1564
+ continue
1565
+ }
1566
+ lines = append(lines, fmt.Sprintf(" - %s", row))
1567
+ }
1568
+ if len(stats) > limit {
1569
+ lines = append(lines, fmt.Sprintf(" - ... %d more", len(stats)-limit))
1570
+ }
1571
+ }
1572
+ return lines
1573
+ }
1574
+
1575
+ func formatStandingsGroupList(items []any) []string {
1576
+ lines := make([]string, 0, len(items)*8+1)
1577
+ lines = append(lines, fmt.Sprintf("Standings Groups (%d)", len(items)))
1578
+
1579
+ for i, rawGroup := range items {
1580
+ group := mapFromAny(rawGroup)
1581
+ if group == nil {
1582
+ continue
1583
+ }
1584
+
1585
+ groupID := firstNonEmpty(valueString(group, "groupId"), valueString(group, "id"), fmt.Sprintf("%d", i+1))
1586
+ seasonID := valueString(group, "seasonId")
1587
+ header := "Group " + groupID
1588
+ if seasonID != "" {
1589
+ header = joinParts(header, "Season "+seasonID)
1590
+ }
1591
+ lines = append(lines, header)
1592
+
1593
+ entries := sliceValue(group, "entries")
1594
+ if len(entries) == 0 {
1595
+ lines = append(lines, " No standings entries available.")
1596
+ continue
1597
+ }
1598
+
1599
+ for entryIndex, rawEntry := range entries {
1600
+ team := mapFromAny(rawEntry)
1601
+ if team == nil {
1602
+ continue
1603
+ }
1604
+
1605
+ rank := standingsStatValueFromTeam(team, "rank", "position")
1606
+ if rank == "" {
1607
+ rank = fmt.Sprintf("%d", entryIndex+1)
1608
+ }
1609
+ teamName := firstNonEmpty(valueString(team, "shortName"), valueString(team, "name"), valueString(team, "id"))
1610
+ played := standingsStatValueFromTeam(team, "matchesplayed", "played", "matches")
1611
+ won := standingsStatValueFromTeam(team, "wins", "won")
1612
+ lost := standingsStatValueFromTeam(team, "losses", "lost")
1613
+ points := standingsStatValueFromTeam(team, "matchpoints", "points", "pts")
1614
+ nrr := standingsStatValueFromTeam(team, "netrunrate", "nrr", "runrate")
1615
+
1616
+ row := joinParts(
1617
+ fmt.Sprintf("#%s %s", rank, teamName),
1618
+ nonEmptyLabel("P", played),
1619
+ nonEmptyLabel("W", won),
1620
+ nonEmptyLabel("L", lost),
1621
+ nonEmptyLabel("Pts", points),
1622
+ nonEmptyLabel("NRR", nrr),
1623
+ )
1624
+ if strings.TrimSpace(row) == "" {
1625
+ row = joinParts(fmt.Sprintf("#%s %s", rank, teamName), valueString(team, "scoreSummary"))
1626
+ }
1627
+ lines = append(lines, " "+row)
1628
+ }
1629
+ }
1630
+ return lines
1631
+ }
1632
+
1633
+ func standingsStatValueFromTeam(team map[string]any, names ...string) string {
1634
+ if len(names) == 0 || team == nil {
1635
+ return ""
1636
+ }
1637
+
1638
+ targets := map[string]struct{}{}
1639
+ for _, name := range names {
1640
+ key := normalizeStatName(name)
1641
+ if key != "" {
1642
+ targets[key] = struct{}{}
1643
+ }
1644
+ }
1645
+
1646
+ extensions, ok := team["extensions"].(map[string]any)
1647
+ if !ok || extensions == nil {
1648
+ return ""
1649
+ }
1650
+ records, ok := extensions["records"].([]any)
1651
+ if !ok || len(records) == 0 {
1652
+ return ""
1653
+ }
1654
+
1655
+ for _, rawRecord := range records {
1656
+ record := mapFromAny(rawRecord)
1657
+ if record == nil {
1658
+ continue
1659
+ }
1660
+ for _, rawStat := range sliceValue(record, "stats") {
1661
+ stat := mapFromAny(rawStat)
1662
+ if stat == nil {
1663
+ continue
1664
+ }
1665
+ nameKey := normalizeStatName(firstNonEmpty(valueString(stat, "name"), valueString(stat, "type")))
1666
+ if _, ok := targets[nameKey]; !ok {
1667
+ continue
1668
+ }
1669
+ value := firstNonEmpty(valueString(stat, "displayValue"), valueString(stat, "value"))
1670
+ if value != "" {
1671
+ return value
1672
+ }
1673
+ }
1674
+ }
1675
+ return ""
1676
+ }
1677
+
1269
1678
  func formatAnalysisView(entity map[string]any) []string {
1270
1679
  lines := make([]string, 0, 64)
1271
1680
  if command := valueString(entity, "command"); command != "" {
@@ -1535,9 +1944,9 @@ func overBallLabel(entity map[string]any) string {
1535
1944
  return ""
1536
1945
  }
1537
1946
  if ball == "" {
1538
- return "over " + over
1947
+ return over
1539
1948
  }
1540
- return "over " + over + "." + ball
1949
+ return over + "." + ball
1541
1950
  }
1542
1951
 
1543
1952
  func involvementLabel(entity map[string]any) string {