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
|
@@ -13,9 +13,16 @@ import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
const defaultMatchListLimit = 20
|
|
16
|
+
const matchListEventFetchConcurrency = 3
|
|
17
|
+
const matchListStatusFetchConcurrency = 3
|
|
18
|
+
const matchListEventFetchTimeout = 4500 * time.Millisecond
|
|
19
|
+
const matchListStatusFetchTimeout = 4 * time.Second
|
|
20
|
+
const matchLineupRosterFetchConcurrency = 2
|
|
21
|
+
const matchLineupRosterFetchTimeout = 5 * time.Second
|
|
16
22
|
const deliveryFetchConcurrency = 96
|
|
17
23
|
const detailSubresourceFetchConcurrency = 24
|
|
18
24
|
const detailItemFetchTimeout = 3 * time.Second
|
|
25
|
+
const liveViewRecentDeliveryFetchCount = 60
|
|
19
26
|
const matchTeamQueryScanRange = 6
|
|
20
27
|
const maxTeamQueryEventCandidates = 36
|
|
21
28
|
const teamQueryEventFetchTimeout = 1500 * time.Millisecond
|
|
@@ -45,6 +52,13 @@ type MatchInningsOptions struct {
|
|
|
45
52
|
Period int
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
// MatchDuelOptions controls batter-vs-bowler matchup lookup behavior.
|
|
56
|
+
type MatchDuelOptions struct {
|
|
57
|
+
LeagueID string
|
|
58
|
+
BatterQuery string
|
|
59
|
+
BowlerQuery string
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
// MatchService implements domain-level match discovery and lookup commands.
|
|
49
63
|
type MatchService struct {
|
|
50
64
|
client *Client
|
|
@@ -99,6 +113,124 @@ func (s *MatchService) Live(ctx context.Context, opts MatchListOptions) (Normali
|
|
|
99
113
|
return s.listFromEvents(ctx, opts, true)
|
|
100
114
|
}
|
|
101
115
|
|
|
116
|
+
// Lineups resolves one match and returns match-scoped roster entries for both teams.
|
|
117
|
+
func (s *MatchService) Lineups(ctx context.Context, query string, opts MatchLookupOptions) (NormalizedResult, error) {
|
|
118
|
+
lookup, passthrough := s.resolveMatchLookup(ctx, query, opts)
|
|
119
|
+
if passthrough != nil {
|
|
120
|
+
passthrough.Kind = EntityTeamRoster
|
|
121
|
+
return *passthrough, nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if len(lookup.match.Teams) == 0 {
|
|
125
|
+
return NormalizedResult{
|
|
126
|
+
Kind: EntityTeamRoster,
|
|
127
|
+
Status: ResultStatusEmpty,
|
|
128
|
+
Message: fmt.Sprintf("no teams found for match %q", lookup.match.ID),
|
|
129
|
+
}, nil
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type lineupLoadResult struct {
|
|
133
|
+
entries []TeamRosterEntry
|
|
134
|
+
warns []string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
results := make([]lineupLoadResult, len(lookup.match.Teams))
|
|
138
|
+
sem := make(chan struct{}, matchLineupRosterFetchConcurrency)
|
|
139
|
+
var wg sync.WaitGroup
|
|
140
|
+
teamCache := map[string]teamIdentity{}
|
|
141
|
+
|
|
142
|
+
teamService := &TeamService{
|
|
143
|
+
client: s.client,
|
|
144
|
+
resolver: s.resolver,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for i := range lookup.match.Teams {
|
|
148
|
+
team := lookup.match.Teams[i]
|
|
149
|
+
teamID := strings.TrimSpace(team.ID)
|
|
150
|
+
if teamID == "" {
|
|
151
|
+
teamID = strings.TrimSpace(refIDs(team.Ref)["teamId"])
|
|
152
|
+
}
|
|
153
|
+
if teamID == "" {
|
|
154
|
+
teamID = strings.TrimSpace(refIDs(team.Ref)["competitorId"])
|
|
155
|
+
}
|
|
156
|
+
if teamID == "" {
|
|
157
|
+
results[i].warns = []string{fmt.Sprintf("skip team with missing id/ref in match %q", lookup.match.ID)}
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
team.ID = teamID
|
|
161
|
+
if strings.TrimSpace(team.Name) == "" || strings.TrimSpace(team.ShortName) == "" {
|
|
162
|
+
identity, err := s.fetchTeamIdentity(ctx, &team, teamCache)
|
|
163
|
+
if err != nil {
|
|
164
|
+
results[i].warns = append(results[i].warns, fmt.Sprintf("team %s: %v", nonEmpty(team.Ref, team.ID), err))
|
|
165
|
+
} else {
|
|
166
|
+
team.Name = nonEmpty(team.Name, identity.name)
|
|
167
|
+
team.ShortName = nonEmpty(team.ShortName, identity.shortName)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
wg.Add(1)
|
|
172
|
+
go func(index int, team Team) {
|
|
173
|
+
defer wg.Done()
|
|
174
|
+
sem <- struct{}{}
|
|
175
|
+
defer func() { <-sem }()
|
|
176
|
+
|
|
177
|
+
rosterRef := nonEmpty(strings.TrimSpace(team.RosterRef), competitorSubresourceRef(*lookup.match, team.ID, "roster"))
|
|
178
|
+
if rosterRef == "" {
|
|
179
|
+
results[index].warns = []string{fmt.Sprintf("roster route unavailable for team %q", team.ID)}
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
reqCtx, cancel := context.WithTimeout(ctx, matchLineupRosterFetchTimeout)
|
|
184
|
+
resolved, err := s.client.ResolveRefChain(reqCtx, rosterRef)
|
|
185
|
+
cancel()
|
|
186
|
+
if err != nil {
|
|
187
|
+
results[index].warns = []string{fmt.Sprintf("roster %s: %v", rosterRef, err)}
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
entries, err := NormalizeTeamRosterEntries(resolved.Body, team, TeamScopeMatch, lookup.match.ID)
|
|
192
|
+
if err != nil {
|
|
193
|
+
results[index].warns = []string{fmt.Sprintf("roster %s: %v", resolved.CanonicalRef, err)}
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for i := range entries {
|
|
198
|
+
entries[i].TeamName = nonEmpty(entries[i].TeamName, team.ShortName, team.Name, team.ID)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
reqCtx, cancel = context.WithTimeout(ctx, matchLineupRosterFetchTimeout)
|
|
202
|
+
hydrateWarnings := teamService.enrichRosterEntries(reqCtx, entries)
|
|
203
|
+
cancel()
|
|
204
|
+
results[index] = lineupLoadResult{
|
|
205
|
+
entries: entries,
|
|
206
|
+
warns: hydrateWarnings,
|
|
207
|
+
}
|
|
208
|
+
}(i, team)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
wg.Wait()
|
|
212
|
+
|
|
213
|
+
warnings := append([]string{}, lookup.warnings...)
|
|
214
|
+
items := make([]any, 0)
|
|
215
|
+
for i := range results {
|
|
216
|
+
warnings = append(warnings, results[i].warns...)
|
|
217
|
+
for _, entry := range results[i].entries {
|
|
218
|
+
items = append(items, entry)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
result := NewListResult(EntityTeamRoster, items)
|
|
223
|
+
if len(warnings) > 0 {
|
|
224
|
+
result = NewPartialListResult(EntityTeamRoster, items, compactWarnings(warnings)...)
|
|
225
|
+
}
|
|
226
|
+
result.RequestedRef = lookup.resolved.RequestedRef
|
|
227
|
+
result.CanonicalRef = lookup.resolved.CanonicalRef
|
|
228
|
+
if len(items) == 0 && strings.TrimSpace(result.Message) == "" {
|
|
229
|
+
result.Message = "no lineup entries found for this match"
|
|
230
|
+
}
|
|
231
|
+
return result, nil
|
|
232
|
+
}
|
|
233
|
+
|
|
102
234
|
// Show resolves and returns one match with normalized summary fields.
|
|
103
235
|
func (s *MatchService) Show(ctx context.Context, query string, opts MatchLookupOptions) (NormalizedResult, error) {
|
|
104
236
|
return s.lookupMatch(ctx, query, opts, false)
|
|
@@ -116,6 +248,10 @@ func (s *MatchService) Scorecard(ctx context.Context, query string, opts MatchLo
|
|
|
116
248
|
passthrough.Kind = EntityMatchScorecard
|
|
117
249
|
return *passthrough, nil
|
|
118
250
|
}
|
|
251
|
+
statusCache := map[string]matchStatusSnapshot{}
|
|
252
|
+
teamCache := map[string]teamIdentity{}
|
|
253
|
+
scoreCache := map[string]string{}
|
|
254
|
+
hydrationWarnings := s.hydrateMatch(ctx, lookup.match, statusCache, teamCache, scoreCache)
|
|
119
255
|
|
|
120
256
|
scorecardRef := matchSubresourceRef(*lookup.match, "matchcards", "matchcards")
|
|
121
257
|
if scorecardRef == "" {
|
|
@@ -126,8 +262,26 @@ func (s *MatchService) Scorecard(ctx context.Context, query string, opts MatchLo
|
|
|
126
262
|
}, nil
|
|
127
263
|
}
|
|
128
264
|
|
|
129
|
-
resolved, err := s.
|
|
265
|
+
resolved, err := s.resolveRefChainResilient(ctx, scorecardRef)
|
|
130
266
|
if err != nil {
|
|
267
|
+
if live, liveWarnings := s.buildLiveView(ctx, *lookup.match); live != nil {
|
|
268
|
+
scorecard := &MatchScorecard{
|
|
269
|
+
Ref: scorecardRef,
|
|
270
|
+
LeagueID: lookup.match.LeagueID,
|
|
271
|
+
EventID: lookup.match.EventID,
|
|
272
|
+
CompetitionID: lookup.match.CompetitionID,
|
|
273
|
+
MatchID: lookup.match.ID,
|
|
274
|
+
}
|
|
275
|
+
augmentScorecardFromLive(scorecard, live)
|
|
276
|
+
warnings := append([]string{}, lookup.warnings...)
|
|
277
|
+
warnings = append(warnings, hydrationWarnings...)
|
|
278
|
+
warnings = append(warnings, liveWarnings...)
|
|
279
|
+
warnings = append(warnings, fmt.Sprintf("scorecard fallback used after %v", err))
|
|
280
|
+
result := NewPartialResult(EntityMatchScorecard, scorecard, warnings...)
|
|
281
|
+
result.RequestedRef = scorecardRef
|
|
282
|
+
result.CanonicalRef = scorecardRef
|
|
283
|
+
return result, nil
|
|
284
|
+
}
|
|
131
285
|
return NewTransportErrorResult(EntityMatchScorecard, scorecardRef, err), nil
|
|
132
286
|
}
|
|
133
287
|
|
|
@@ -135,8 +289,17 @@ func (s *MatchService) Scorecard(ctx context.Context, query string, opts MatchLo
|
|
|
135
289
|
if err != nil {
|
|
136
290
|
return NormalizedResult{}, fmt.Errorf("normalize matchcards %q: %w", resolved.CanonicalRef, err)
|
|
137
291
|
}
|
|
292
|
+
enrichmentWarnings := []string{}
|
|
293
|
+
if len(scorecard.BattingCards) == 0 || len(scorecard.BowlingCards) == 0 {
|
|
294
|
+
if live, warns := s.buildLiveView(ctx, *lookup.match); live != nil {
|
|
295
|
+
enrichmentWarnings = append(enrichmentWarnings, warns...)
|
|
296
|
+
augmentScorecardFromLive(scorecard, live)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
138
299
|
|
|
139
300
|
warnings := append([]string{}, lookup.warnings...)
|
|
301
|
+
warnings = append(warnings, hydrationWarnings...)
|
|
302
|
+
warnings = append(warnings, enrichmentWarnings...)
|
|
140
303
|
result := NewDataResult(EntityMatchScorecard, scorecard)
|
|
141
304
|
if len(warnings) > 0 {
|
|
142
305
|
result = NewPartialResult(EntityMatchScorecard, scorecard, warnings...)
|
|
@@ -163,7 +326,23 @@ func (s *MatchService) Details(ctx context.Context, query string, opts MatchLook
|
|
|
163
326
|
}, nil
|
|
164
327
|
}
|
|
165
328
|
|
|
166
|
-
|
|
329
|
+
events, warnings, err := s.deliveryEventsForMatchRefs(ctx, detailsRef, matchSubresourceRef(*lookup.match, "plays", "plays"))
|
|
330
|
+
if err != nil {
|
|
331
|
+
return NewTransportErrorResult(EntityDeliveryEvent, detailsRef, err), nil
|
|
332
|
+
}
|
|
333
|
+
warnings = append(lookup.warnings, warnings...)
|
|
334
|
+
|
|
335
|
+
items := make([]any, 0, len(events))
|
|
336
|
+
for _, delivery := range events {
|
|
337
|
+
items = append(items, delivery)
|
|
338
|
+
}
|
|
339
|
+
result := NewListResult(EntityDeliveryEvent, items)
|
|
340
|
+
if len(warnings) > 0 {
|
|
341
|
+
result = NewPartialListResult(EntityDeliveryEvent, items, warnings...)
|
|
342
|
+
}
|
|
343
|
+
result.RequestedRef = detailsRef
|
|
344
|
+
result.CanonicalRef = detailsRef
|
|
345
|
+
return result, nil
|
|
167
346
|
}
|
|
168
347
|
|
|
169
348
|
// Plays resolves and returns normalized delivery events from the plays route.
|
|
@@ -183,7 +362,23 @@ func (s *MatchService) Plays(ctx context.Context, query string, opts MatchLookup
|
|
|
183
362
|
}, nil
|
|
184
363
|
}
|
|
185
364
|
|
|
186
|
-
|
|
365
|
+
events, warnings, err := s.deliveryEventsForMatchRefs(ctx, playsRef, nonEmpty(strings.TrimSpace(lookup.match.DetailsRef), matchSubresourceRef(*lookup.match, "details", "details")))
|
|
366
|
+
if err != nil {
|
|
367
|
+
return NewTransportErrorResult(EntityDeliveryEvent, playsRef, err), nil
|
|
368
|
+
}
|
|
369
|
+
warnings = append(lookup.warnings, warnings...)
|
|
370
|
+
|
|
371
|
+
items := make([]any, 0, len(events))
|
|
372
|
+
for _, delivery := range events {
|
|
373
|
+
items = append(items, delivery)
|
|
374
|
+
}
|
|
375
|
+
result := NewListResult(EntityDeliveryEvent, items)
|
|
376
|
+
if len(warnings) > 0 {
|
|
377
|
+
result = NewPartialListResult(EntityDeliveryEvent, items, warnings...)
|
|
378
|
+
}
|
|
379
|
+
result.RequestedRef = playsRef
|
|
380
|
+
result.CanonicalRef = playsRef
|
|
381
|
+
return result, nil
|
|
187
382
|
}
|
|
188
383
|
|
|
189
384
|
// Situation resolves and returns normalized match situation data.
|
|
@@ -194,6 +389,11 @@ func (s *MatchService) Situation(ctx context.Context, query string, opts MatchLo
|
|
|
194
389
|
return *passthrough, nil
|
|
195
390
|
}
|
|
196
391
|
|
|
392
|
+
statusCache := map[string]matchStatusSnapshot{}
|
|
393
|
+
teamCache := map[string]teamIdentity{}
|
|
394
|
+
scoreCache := map[string]string{}
|
|
395
|
+
hydrationWarnings := s.hydrateMatch(ctx, lookup.match, statusCache, teamCache, scoreCache)
|
|
396
|
+
|
|
197
397
|
situationRef := matchSubresourceRef(*lookup.match, "situation", "situation")
|
|
198
398
|
if situationRef == "" {
|
|
199
399
|
return NormalizedResult{
|
|
@@ -214,6 +414,19 @@ func (s *MatchService) Situation(ctx context.Context, query string, opts MatchLo
|
|
|
214
414
|
}
|
|
215
415
|
|
|
216
416
|
if isSparseSituation(situation) {
|
|
417
|
+
if live, warnings := s.buildLiveView(ctx, *lookup.match); live != nil {
|
|
418
|
+
situation.Live = live
|
|
419
|
+
result := NewDataResult(EntityMatchSituation, situation)
|
|
420
|
+
combinedWarnings := append([]string{}, lookup.warnings...)
|
|
421
|
+
combinedWarnings = append(combinedWarnings, hydrationWarnings...)
|
|
422
|
+
combinedWarnings = append(combinedWarnings, warnings...)
|
|
423
|
+
if len(combinedWarnings) > 0 {
|
|
424
|
+
result = NewPartialResult(EntityMatchSituation, situation, combinedWarnings...)
|
|
425
|
+
}
|
|
426
|
+
result.RequestedRef = resolved.RequestedRef
|
|
427
|
+
result.CanonicalRef = resolved.CanonicalRef
|
|
428
|
+
return result, nil
|
|
429
|
+
}
|
|
217
430
|
result := NormalizedResult{
|
|
218
431
|
Kind: EntityMatchSituation,
|
|
219
432
|
Status: ResultStatusEmpty,
|
|
@@ -225,14 +438,211 @@ func (s *MatchService) Situation(ctx context.Context, query string, opts MatchLo
|
|
|
225
438
|
}
|
|
226
439
|
|
|
227
440
|
result := NewDataResult(EntityMatchSituation, situation)
|
|
228
|
-
|
|
229
|
-
|
|
441
|
+
combinedWarnings := append([]string{}, lookup.warnings...)
|
|
442
|
+
combinedWarnings = append(combinedWarnings, hydrationWarnings...)
|
|
443
|
+
if len(combinedWarnings) > 0 {
|
|
444
|
+
result = NewPartialResult(EntityMatchSituation, situation, combinedWarnings...)
|
|
230
445
|
}
|
|
231
446
|
result.RequestedRef = resolved.RequestedRef
|
|
232
447
|
result.CanonicalRef = resolved.CanonicalRef
|
|
233
448
|
return result, nil
|
|
234
449
|
}
|
|
235
450
|
|
|
451
|
+
// LiveView resolves and returns a fan-first live view synthesized from delivery details.
|
|
452
|
+
func (s *MatchService) LiveView(ctx context.Context, query string, opts MatchLookupOptions) (NormalizedResult, error) {
|
|
453
|
+
lookup, passthrough := s.resolveMatchLookup(ctx, query, opts)
|
|
454
|
+
if passthrough != nil {
|
|
455
|
+
passthrough.Kind = EntityMatchSituation
|
|
456
|
+
return *passthrough, nil
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
statusCache := map[string]matchStatusSnapshot{}
|
|
460
|
+
teamCache := map[string]teamIdentity{}
|
|
461
|
+
scoreCache := map[string]string{}
|
|
462
|
+
hydrationWarnings := s.hydrateMatch(ctx, lookup.match, statusCache, teamCache, scoreCache)
|
|
463
|
+
|
|
464
|
+
live, liveWarnings := s.buildLiveView(ctx, *lookup.match)
|
|
465
|
+
if live == nil {
|
|
466
|
+
fallback, fallbackErr := s.Situation(ctx, query, opts)
|
|
467
|
+
if fallbackErr == nil && fallback.Status != ResultStatusError {
|
|
468
|
+
combinedWarnings := append([]string{}, fallback.Warnings...)
|
|
469
|
+
combinedWarnings = append(combinedWarnings, lookup.warnings...)
|
|
470
|
+
combinedWarnings = append(combinedWarnings, hydrationWarnings...)
|
|
471
|
+
combinedWarnings = append(combinedWarnings, liveWarnings...)
|
|
472
|
+
combinedWarnings = append(combinedWarnings, "live-view fallback: showing situation data")
|
|
473
|
+
combinedWarnings = compactWarnings(combinedWarnings)
|
|
474
|
+
|
|
475
|
+
if fallback.Data != nil && len(combinedWarnings) > 0 {
|
|
476
|
+
partial := NewPartialResult(EntityMatchSituation, fallback.Data, combinedWarnings...)
|
|
477
|
+
partial.RequestedRef = nonEmpty(fallback.RequestedRef, lookup.resolved.RequestedRef)
|
|
478
|
+
partial.CanonicalRef = nonEmpty(fallback.CanonicalRef, lookup.resolved.CanonicalRef)
|
|
479
|
+
return partial, nil
|
|
480
|
+
}
|
|
481
|
+
return fallback, nil
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
result := NormalizedResult{
|
|
485
|
+
Kind: EntityMatchSituation,
|
|
486
|
+
Status: ResultStatusEmpty,
|
|
487
|
+
Message: fmt.Sprintf("no live view data available for match %q", lookup.match.ID),
|
|
488
|
+
}
|
|
489
|
+
warnings := compactWarnings(append(append(append([]string{}, lookup.warnings...), hydrationWarnings...), liveWarnings...))
|
|
490
|
+
if len(warnings) > 0 {
|
|
491
|
+
result.Status = ResultStatusPartial
|
|
492
|
+
result.Message = "live view unavailable"
|
|
493
|
+
result.Warnings = warnings
|
|
494
|
+
}
|
|
495
|
+
return result, nil
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
situation := &MatchSituation{
|
|
499
|
+
Ref: matchSubresourceRef(*lookup.match, "situation", "situation"),
|
|
500
|
+
LeagueID: lookup.match.LeagueID,
|
|
501
|
+
EventID: lookup.match.EventID,
|
|
502
|
+
CompetitionID: lookup.match.CompetitionID,
|
|
503
|
+
MatchID: lookup.match.ID,
|
|
504
|
+
Live: live,
|
|
505
|
+
}
|
|
506
|
+
warnings := append([]string{}, lookup.warnings...)
|
|
507
|
+
warnings = append(warnings, hydrationWarnings...)
|
|
508
|
+
warnings = append(warnings, liveWarnings...)
|
|
509
|
+
|
|
510
|
+
result := NewDataResult(EntityMatchSituation, situation)
|
|
511
|
+
if len(warnings) > 0 {
|
|
512
|
+
result = NewPartialResult(EntityMatchSituation, situation, warnings...)
|
|
513
|
+
}
|
|
514
|
+
result.RequestedRef = lookup.resolved.RequestedRef
|
|
515
|
+
result.CanonicalRef = lookup.resolved.CanonicalRef
|
|
516
|
+
return result, nil
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Duel resolves and returns a batter-vs-bowler matchup summary for one match.
|
|
520
|
+
func (s *MatchService) Duel(ctx context.Context, query string, opts MatchDuelOptions) (NormalizedResult, error) {
|
|
521
|
+
lookup, passthrough := s.resolveMatchLookup(ctx, query, MatchLookupOptions{LeagueID: opts.LeagueID})
|
|
522
|
+
if passthrough != nil {
|
|
523
|
+
passthrough.Kind = EntityMatchDuel
|
|
524
|
+
return *passthrough, nil
|
|
525
|
+
}
|
|
526
|
+
if strings.TrimSpace(opts.BatterQuery) == "" || strings.TrimSpace(opts.BowlerQuery) == "" {
|
|
527
|
+
return NormalizedResult{
|
|
528
|
+
Kind: EntityMatchDuel,
|
|
529
|
+
Status: ResultStatusEmpty,
|
|
530
|
+
Message: "--batter and --bowler are required",
|
|
531
|
+
}, nil
|
|
532
|
+
}
|
|
533
|
+
statusCache := map[string]matchStatusSnapshot{}
|
|
534
|
+
teamCache := map[string]teamIdentity{}
|
|
535
|
+
scoreCache := map[string]string{}
|
|
536
|
+
hydrationWarnings := s.hydrateMatch(ctx, lookup.match, statusCache, teamCache, scoreCache)
|
|
537
|
+
|
|
538
|
+
detailsRef := nonEmpty(strings.TrimSpace(lookup.match.DetailsRef), matchSubresourceRef(*lookup.match, "details", "details"))
|
|
539
|
+
playsRef := matchSubresourceRef(*lookup.match, "plays", "plays")
|
|
540
|
+
primaryRef := nonEmpty(detailsRef, playsRef)
|
|
541
|
+
deliveries, warnings, err := s.deliveryEventsForMatchRefs(ctx, primaryRef, playsRef)
|
|
542
|
+
if err != nil {
|
|
543
|
+
return NewTransportErrorResult(EntityMatchDuel, primaryRef, err), nil
|
|
544
|
+
}
|
|
545
|
+
if len(deliveries) == 0 {
|
|
546
|
+
return NormalizedResult{
|
|
547
|
+
Kind: EntityMatchDuel,
|
|
548
|
+
Status: ResultStatusEmpty,
|
|
549
|
+
Message: "no delivery data available for duel analysis",
|
|
550
|
+
}, nil
|
|
551
|
+
}
|
|
552
|
+
matchLatest := deliveries[len(deliveries)-1]
|
|
553
|
+
|
|
554
|
+
batterID, batterName := resolveDuelIdentity(deliveries, strings.TrimSpace(opts.BatterQuery), true)
|
|
555
|
+
bowlerID, bowlerName := resolveDuelIdentity(deliveries, strings.TrimSpace(opts.BowlerQuery), false)
|
|
556
|
+
if batterID == "" && normalizeAlias(batterName) == "" {
|
|
557
|
+
return NormalizedResult{Kind: EntityMatchDuel, Status: ResultStatusEmpty, Message: fmt.Sprintf("batter %q not found in this match stream", opts.BatterQuery)}, nil
|
|
558
|
+
}
|
|
559
|
+
if bowlerID == "" && normalizeAlias(bowlerName) == "" {
|
|
560
|
+
return NormalizedResult{Kind: EntityMatchDuel, Status: ResultStatusEmpty, Message: fmt.Sprintf("bowler %q not found in this match stream", opts.BowlerQuery)}, nil
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
runs := 0
|
|
564
|
+
dots := 0
|
|
565
|
+
fours := 0
|
|
566
|
+
sixes := 0
|
|
567
|
+
wickets := 0
|
|
568
|
+
balls := 0
|
|
569
|
+
recent := make([]DeliveryEvent, 0, 8)
|
|
570
|
+
lastUpdate := int64(0)
|
|
571
|
+
for _, delivery := range deliveries {
|
|
572
|
+
if !deliveryMatchesDuel(delivery, batterID, batterName, bowlerID, bowlerName) {
|
|
573
|
+
continue
|
|
574
|
+
}
|
|
575
|
+
balls++
|
|
576
|
+
if delivery.ScoreValue > 0 {
|
|
577
|
+
runs += delivery.ScoreValue
|
|
578
|
+
} else {
|
|
579
|
+
dots++
|
|
580
|
+
}
|
|
581
|
+
short := strings.ToUpper(strings.TrimSpace(delivery.ShortText))
|
|
582
|
+
if strings.Contains(short, "FOUR") {
|
|
583
|
+
fours++
|
|
584
|
+
}
|
|
585
|
+
if strings.Contains(short, "SIX") {
|
|
586
|
+
sixes++
|
|
587
|
+
}
|
|
588
|
+
if truthyField(delivery.Dismissal, "dismissal") {
|
|
589
|
+
wickets++
|
|
590
|
+
}
|
|
591
|
+
recent = append(recent, delivery)
|
|
592
|
+
if delivery.BBBTimestamp > lastUpdate {
|
|
593
|
+
lastUpdate = delivery.BBBTimestamp
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if balls == 0 {
|
|
597
|
+
return NormalizedResult{
|
|
598
|
+
Kind: EntityMatchDuel,
|
|
599
|
+
Status: ResultStatusEmpty,
|
|
600
|
+
Message: fmt.Sprintf("no deliveries found for %s vs %s", nonEmpty(batterName, opts.BatterQuery), nonEmpty(bowlerName, opts.BowlerQuery)),
|
|
601
|
+
}, nil
|
|
602
|
+
}
|
|
603
|
+
if len(recent) > 8 {
|
|
604
|
+
recent = recent[len(recent)-8:]
|
|
605
|
+
}
|
|
606
|
+
liveScore := firstNonEmpty(matchScoreLabel(matchLatest.HomeScore), matchScoreLabel(matchLatest.AwayScore), lookup.match.ScoreSummary)
|
|
607
|
+
liveOver := overBallString(matchLatest.OverNumber, matchLatest.BallNumber)
|
|
608
|
+
duelScore := liveScore
|
|
609
|
+
if liveScore != "" && liveOver != "" {
|
|
610
|
+
duelScore = fmt.Sprintf("%s (%s ov)", liveScore, liveOver)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
duel := MatchDuel{
|
|
614
|
+
MatchID: lookup.match.ID,
|
|
615
|
+
Fixture: nonEmpty(lookup.match.ShortDescription, lookup.match.Description),
|
|
616
|
+
Score: duelScore,
|
|
617
|
+
BatterID: batterID,
|
|
618
|
+
BatterName: nonEmpty(batterName, opts.BatterQuery),
|
|
619
|
+
BowlerID: bowlerID,
|
|
620
|
+
BowlerName: nonEmpty(bowlerName, opts.BowlerQuery),
|
|
621
|
+
Balls: balls,
|
|
622
|
+
Runs: runs,
|
|
623
|
+
Dots: dots,
|
|
624
|
+
Fours: fours,
|
|
625
|
+
Sixes: sixes,
|
|
626
|
+
Wickets: wickets,
|
|
627
|
+
StrikeRate: strikeRate(runs, balls),
|
|
628
|
+
RecentBalls: recent,
|
|
629
|
+
LastUpdateMS: lastUpdate,
|
|
630
|
+
SnapshotAt: time.Now().UTC().Format(time.RFC3339),
|
|
631
|
+
SourceRoute: primaryRef,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
allWarnings := append([]string{}, lookup.warnings...)
|
|
635
|
+
allWarnings = append(allWarnings, hydrationWarnings...)
|
|
636
|
+
allWarnings = append(allWarnings, warnings...)
|
|
637
|
+
result := NewDataResult(EntityMatchDuel, duel)
|
|
638
|
+
if len(allWarnings) > 0 {
|
|
639
|
+
result = NewPartialResult(EntityMatchDuel, duel, allWarnings...)
|
|
640
|
+
}
|
|
641
|
+
result.RequestedRef = lookup.resolved.RequestedRef
|
|
642
|
+
result.CanonicalRef = lookup.resolved.CanonicalRef
|
|
643
|
+
return result, nil
|
|
644
|
+
}
|
|
645
|
+
|
|
236
646
|
// Phases resolves and returns fan-oriented innings phase splits (powerplay/middle/death).
|
|
237
647
|
func (s *MatchService) Phases(ctx context.Context, query string, opts MatchLookupOptions) (NormalizedResult, error) {
|
|
238
648
|
lookup, passthrough := s.resolveMatchLookup(ctx, query, opts)
|
|
@@ -590,31 +1000,30 @@ func (s *MatchService) listFromEvents(ctx context.Context, opts MatchListOptions
|
|
|
590
1000
|
limit = defaultMatchListLimit
|
|
591
1001
|
}
|
|
592
1002
|
|
|
593
|
-
statusCache := map[string]matchStatusSnapshot{}
|
|
594
|
-
|
|
595
1003
|
matches := make([]Match, 0, limit)
|
|
596
1004
|
warnings := make([]string, 0)
|
|
597
|
-
|
|
598
|
-
|
|
1005
|
+
eventResults := s.fetchEventMatchesConcurrent(ctx, page.Items)
|
|
1006
|
+
candidates := make([]*Match, 0, len(page.Items))
|
|
1007
|
+
|
|
1008
|
+
for _, eventResult := range eventResults {
|
|
1009
|
+
if len(matches) >= limit && !liveOnly {
|
|
599
1010
|
break
|
|
600
1011
|
}
|
|
601
1012
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
warnings = append(warnings, fmt.Sprintf("event %s: %v", strings.TrimSpace(eventRef.URL), eventErr))
|
|
1013
|
+
if eventResult.err != nil {
|
|
1014
|
+
warnings = append(warnings, fmt.Sprintf("event %s: %v", strings.TrimSpace(eventResult.ref), eventResult.err))
|
|
605
1015
|
continue
|
|
606
1016
|
}
|
|
607
|
-
warnings = append(warnings,
|
|
1017
|
+
warnings = append(warnings, eventResult.warnings...)
|
|
608
1018
|
|
|
609
|
-
for
|
|
610
|
-
match :=
|
|
1019
|
+
for i := range eventResult.matches {
|
|
1020
|
+
match := eventResult.matches[i]
|
|
611
1021
|
s.enrichMatchTeamsFromIndex(&match)
|
|
612
|
-
if liveOnly
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
if liveOnly && !isLiveMatch(match) {
|
|
1022
|
+
if liveOnly {
|
|
1023
|
+
candidates = append(candidates, &match)
|
|
616
1024
|
continue
|
|
617
1025
|
}
|
|
1026
|
+
|
|
618
1027
|
match.ScoreSummary = matchScoreSummary(match.Teams)
|
|
619
1028
|
matches = append(matches, match)
|
|
620
1029
|
if len(matches) >= limit {
|
|
@@ -623,6 +1032,20 @@ func (s *MatchService) listFromEvents(ctx context.Context, opts MatchListOptions
|
|
|
623
1032
|
}
|
|
624
1033
|
}
|
|
625
1034
|
|
|
1035
|
+
if liveOnly {
|
|
1036
|
+
warnings = append(warnings, s.hydrateMatchStatusesConcurrent(ctx, candidates)...)
|
|
1037
|
+
for _, candidate := range candidates {
|
|
1038
|
+
if candidate == nil || !isLiveMatch(*candidate) {
|
|
1039
|
+
continue
|
|
1040
|
+
}
|
|
1041
|
+
candidate.ScoreSummary = matchScoreSummary(candidate.Teams)
|
|
1042
|
+
matches = append(matches, *candidate)
|
|
1043
|
+
if len(matches) >= limit {
|
|
1044
|
+
break
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
626
1049
|
items := make([]any, 0, len(matches))
|
|
627
1050
|
for i := range matches {
|
|
628
1051
|
items = append(items, matches[i])
|
|
@@ -637,6 +1060,86 @@ func (s *MatchService) listFromEvents(ctx context.Context, opts MatchListOptions
|
|
|
637
1060
|
return result, nil
|
|
638
1061
|
}
|
|
639
1062
|
|
|
1063
|
+
type eventMatchesResult struct {
|
|
1064
|
+
ref string
|
|
1065
|
+
matches []Match
|
|
1066
|
+
warnings []string
|
|
1067
|
+
err error
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
func (s *MatchService) fetchEventMatchesConcurrent(ctx context.Context, refs []Ref) []eventMatchesResult {
|
|
1071
|
+
results := make([]eventMatchesResult, len(refs))
|
|
1072
|
+
sem := make(chan struct{}, matchListEventFetchConcurrency)
|
|
1073
|
+
var wg sync.WaitGroup
|
|
1074
|
+
|
|
1075
|
+
for i, item := range refs {
|
|
1076
|
+
wg.Add(1)
|
|
1077
|
+
go func(index int, item Ref) {
|
|
1078
|
+
defer wg.Done()
|
|
1079
|
+
sem <- struct{}{}
|
|
1080
|
+
defer func() { <-sem }()
|
|
1081
|
+
|
|
1082
|
+
ref := strings.TrimSpace(item.URL)
|
|
1083
|
+
if ref == "" {
|
|
1084
|
+
results[index] = eventMatchesResult{
|
|
1085
|
+
ref: ref,
|
|
1086
|
+
err: fmt.Errorf("empty event ref"),
|
|
1087
|
+
}
|
|
1088
|
+
return
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
reqCtx, cancel := context.WithTimeout(ctx, matchListEventFetchTimeout)
|
|
1092
|
+
matches, warnings, err := s.matchesFromEventRef(reqCtx, ref)
|
|
1093
|
+
cancel()
|
|
1094
|
+
results[index] = eventMatchesResult{
|
|
1095
|
+
ref: ref,
|
|
1096
|
+
matches: matches,
|
|
1097
|
+
warnings: warnings,
|
|
1098
|
+
err: err,
|
|
1099
|
+
}
|
|
1100
|
+
}(i, item)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
wg.Wait()
|
|
1104
|
+
return results
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
func (s *MatchService) hydrateMatchStatusesConcurrent(ctx context.Context, matches []*Match) []string {
|
|
1108
|
+
type statusHydrationResult struct {
|
|
1109
|
+
warnings []string
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
results := make([]statusHydrationResult, len(matches))
|
|
1113
|
+
sem := make(chan struct{}, matchListStatusFetchConcurrency)
|
|
1114
|
+
var wg sync.WaitGroup
|
|
1115
|
+
|
|
1116
|
+
for i, match := range matches {
|
|
1117
|
+
if match == nil || isLiveMatch(*match) || strings.TrimSpace(match.StatusRef) == "" {
|
|
1118
|
+
continue
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
wg.Add(1)
|
|
1122
|
+
go func(index int, match *Match) {
|
|
1123
|
+
defer wg.Done()
|
|
1124
|
+
sem <- struct{}{}
|
|
1125
|
+
defer func() { <-sem }()
|
|
1126
|
+
|
|
1127
|
+
reqCtx, cancel := context.WithTimeout(ctx, matchListStatusFetchTimeout)
|
|
1128
|
+
warnings := s.hydrateMatchStatusOnly(reqCtx, match, map[string]matchStatusSnapshot{})
|
|
1129
|
+
cancel()
|
|
1130
|
+
results[index] = statusHydrationResult{warnings: warnings}
|
|
1131
|
+
}(i, match)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
wg.Wait()
|
|
1135
|
+
|
|
1136
|
+
warnings := make([]string, 0)
|
|
1137
|
+
for _, result := range results {
|
|
1138
|
+
warnings = append(warnings, result.warnings...)
|
|
1139
|
+
}
|
|
1140
|
+
return compactWarnings(warnings)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
640
1143
|
func (s *MatchService) lookupMatch(ctx context.Context, query string, opts MatchLookupOptions, statusOnly bool) (NormalizedResult, error) {
|
|
641
1144
|
lookup, passthrough := s.resolveMatchLookup(ctx, query, opts)
|
|
642
1145
|
if passthrough != nil {
|
|
@@ -718,7 +1221,7 @@ func (s *MatchService) resolveMatchLookup(ctx context.Context, query string, opt
|
|
|
718
1221
|
return nil, &result
|
|
719
1222
|
}
|
|
720
1223
|
|
|
721
|
-
resolved, err := s.
|
|
1224
|
+
resolved, err := s.resolveRefChainResilient(ctx, ref)
|
|
722
1225
|
if err != nil {
|
|
723
1226
|
result := NewTransportErrorResult(EntityMatch, ref, err)
|
|
724
1227
|
return nil, &result
|
|
@@ -1132,12 +1635,153 @@ func (s *MatchService) loadDeliveryEvents(ctx context.Context, refs []Ref) ([]De
|
|
|
1132
1635
|
continue
|
|
1133
1636
|
}
|
|
1134
1637
|
if result.delivery != nil {
|
|
1135
|
-
|
|
1638
|
+
if isRenderableDelivery(*result.delivery) {
|
|
1639
|
+
deliveries = append(deliveries, *result.delivery)
|
|
1640
|
+
}
|
|
1136
1641
|
}
|
|
1137
1642
|
}
|
|
1138
1643
|
return deliveries, compactWarnings(warnings)
|
|
1139
1644
|
}
|
|
1140
1645
|
|
|
1646
|
+
func (s *MatchService) deliveryEventsForMatchRefs(ctx context.Context, primaryRef string, alternateRefs ...string) ([]DeliveryEvent, []string, error) {
|
|
1647
|
+
primaryRef = strings.TrimSpace(primaryRef)
|
|
1648
|
+
primaryEvents, primaryWarnings, primaryErr := s.loadDeliveryEventsFromRoute(ctx, primaryRef)
|
|
1649
|
+
if primaryErr == nil && len(primaryEvents) > 0 && len(primaryWarnings) == 0 {
|
|
1650
|
+
return primaryEvents, nil, nil
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
merged := append([]DeliveryEvent{}, primaryEvents...)
|
|
1654
|
+
warnings := append([]string{}, primaryWarnings...)
|
|
1655
|
+
if primaryErr != nil {
|
|
1656
|
+
warnings = append(warnings, fmt.Sprintf("delivery route %s: %v", primaryRef, primaryErr))
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
for _, ref := range alternateRefs {
|
|
1660
|
+
ref = strings.TrimSpace(ref)
|
|
1661
|
+
if ref == "" || ref == primaryRef {
|
|
1662
|
+
continue
|
|
1663
|
+
}
|
|
1664
|
+
events, routeWarnings, err := s.loadDeliveryEventsFromRoute(ctx, ref)
|
|
1665
|
+
if err != nil {
|
|
1666
|
+
warnings = append(warnings, fmt.Sprintf("delivery route %s: %v", ref, err))
|
|
1667
|
+
continue
|
|
1668
|
+
}
|
|
1669
|
+
warnings = append(warnings, routeWarnings...)
|
|
1670
|
+
merged = append(merged, events...)
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
merged = dedupeDeliveryEvents(merged)
|
|
1674
|
+
sortDeliveryEvents(merged)
|
|
1675
|
+
if len(merged) == 0 && primaryErr != nil {
|
|
1676
|
+
return nil, compactWarnings(warnings), primaryErr
|
|
1677
|
+
}
|
|
1678
|
+
return merged, compactWarnings(warnings), nil
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
func (s *MatchService) loadDeliveryEventsFromRoute(ctx context.Context, ref string) ([]DeliveryEvent, []string, error) {
|
|
1682
|
+
ref = strings.TrimSpace(ref)
|
|
1683
|
+
if ref == "" {
|
|
1684
|
+
return nil, nil, fmt.Errorf("empty delivery route")
|
|
1685
|
+
}
|
|
1686
|
+
resolved, err := s.resolveRefChainResilient(ctx, ref)
|
|
1687
|
+
if err != nil {
|
|
1688
|
+
return nil, nil, err
|
|
1689
|
+
}
|
|
1690
|
+
pageItems, pageWarnings, err := s.resolvePageRefs(ctx, resolved)
|
|
1691
|
+
if err != nil {
|
|
1692
|
+
return nil, nil, err
|
|
1693
|
+
}
|
|
1694
|
+
loaded, loadWarnings := s.loadDeliveryEvents(ctx, pageItems)
|
|
1695
|
+
warnings := make([]string, 0, len(pageWarnings)+len(loadWarnings))
|
|
1696
|
+
warnings = append(warnings, pageWarnings...)
|
|
1697
|
+
warnings = append(warnings, loadWarnings...)
|
|
1698
|
+
return loaded, compactWarnings(warnings), nil
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
func (s *MatchService) loadRecentDeliveryEventsFromRoute(ctx context.Context, ref string, maxItems int) ([]DeliveryEvent, []string, error) {
|
|
1702
|
+
ref = strings.TrimSpace(ref)
|
|
1703
|
+
if ref == "" {
|
|
1704
|
+
return nil, nil, fmt.Errorf("empty delivery route")
|
|
1705
|
+
}
|
|
1706
|
+
resolved, err := s.resolveRefChainResilient(ctx, ref)
|
|
1707
|
+
if err != nil {
|
|
1708
|
+
return nil, nil, err
|
|
1709
|
+
}
|
|
1710
|
+
pageItems, pageWarnings, err := s.resolvePageRefs(ctx, resolved)
|
|
1711
|
+
if err != nil {
|
|
1712
|
+
return nil, nil, err
|
|
1713
|
+
}
|
|
1714
|
+
if maxItems > 0 && len(pageItems) > maxItems {
|
|
1715
|
+
pageItems = append([]Ref(nil), pageItems[len(pageItems)-maxItems:]...)
|
|
1716
|
+
}
|
|
1717
|
+
loaded, loadWarnings := s.loadDeliveryEvents(ctx, pageItems)
|
|
1718
|
+
warnings := make([]string, 0, len(pageWarnings)+len(loadWarnings))
|
|
1719
|
+
warnings = append(warnings, pageWarnings...)
|
|
1720
|
+
warnings = append(warnings, loadWarnings...)
|
|
1721
|
+
return loaded, compactWarnings(warnings), nil
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
func isRenderableDelivery(delivery DeliveryEvent) bool {
|
|
1725
|
+
if delivery.OverNumber > 0 && delivery.BallNumber > 0 {
|
|
1726
|
+
return true
|
|
1727
|
+
}
|
|
1728
|
+
text := strings.TrimSpace(firstNonEmpty(delivery.ShortText, delivery.Text))
|
|
1729
|
+
if text == "" || text == "/" || text == "-" {
|
|
1730
|
+
return false
|
|
1731
|
+
}
|
|
1732
|
+
lowered := strings.ToLower(text)
|
|
1733
|
+
return !strings.Contains(lowered, "over - ball")
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
func sortDeliveryEvents(deliveries []DeliveryEvent) {
|
|
1737
|
+
sort.SliceStable(deliveries, func(i, j int) bool {
|
|
1738
|
+
if deliveries[i].Period != deliveries[j].Period {
|
|
1739
|
+
return deliveries[i].Period < deliveries[j].Period
|
|
1740
|
+
}
|
|
1741
|
+
if deliveries[i].OverNumber != deliveries[j].OverNumber {
|
|
1742
|
+
return deliveries[i].OverNumber < deliveries[j].OverNumber
|
|
1743
|
+
}
|
|
1744
|
+
if deliveries[i].BallNumber != deliveries[j].BallNumber {
|
|
1745
|
+
return deliveries[i].BallNumber < deliveries[j].BallNumber
|
|
1746
|
+
}
|
|
1747
|
+
if deliveries[i].Sequence != deliveries[j].Sequence {
|
|
1748
|
+
return deliveries[i].Sequence < deliveries[j].Sequence
|
|
1749
|
+
}
|
|
1750
|
+
return deliveries[i].BBBTimestamp < deliveries[j].BBBTimestamp
|
|
1751
|
+
})
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
func dedupeDeliveryEvents(deliveries []DeliveryEvent) []DeliveryEvent {
|
|
1755
|
+
if len(deliveries) <= 1 {
|
|
1756
|
+
return deliveries
|
|
1757
|
+
}
|
|
1758
|
+
seen := map[string]int{}
|
|
1759
|
+
out := make([]DeliveryEvent, 0, len(deliveries))
|
|
1760
|
+
for _, delivery := range deliveries {
|
|
1761
|
+
key := strings.TrimSpace(delivery.ID)
|
|
1762
|
+
if key == "" {
|
|
1763
|
+
key = fmt.Sprintf("%d|%d|%d|%d|%s|%s|%s",
|
|
1764
|
+
delivery.Period,
|
|
1765
|
+
delivery.OverNumber,
|
|
1766
|
+
delivery.BallNumber,
|
|
1767
|
+
delivery.Sequence,
|
|
1768
|
+
strings.TrimSpace(delivery.ShortText),
|
|
1769
|
+
strings.TrimSpace(delivery.HomeScore),
|
|
1770
|
+
strings.TrimSpace(delivery.AwayScore),
|
|
1771
|
+
)
|
|
1772
|
+
}
|
|
1773
|
+
if idx, ok := seen[key]; ok {
|
|
1774
|
+
if delivery.Sequence > out[idx].Sequence || delivery.BBBTimestamp > out[idx].BBBTimestamp {
|
|
1775
|
+
out[idx] = delivery
|
|
1776
|
+
}
|
|
1777
|
+
continue
|
|
1778
|
+
}
|
|
1779
|
+
seen[key] = len(out)
|
|
1780
|
+
out = append(out, delivery)
|
|
1781
|
+
}
|
|
1782
|
+
return out
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1141
1785
|
type selectedInningsContext struct {
|
|
1142
1786
|
match Match
|
|
1143
1787
|
team Team
|
|
@@ -1988,11 +2632,381 @@ func extensionRef(extensions map[string]any, key string) string {
|
|
|
1988
2632
|
return strings.TrimSpace(stringField(refMap, "$ref"))
|
|
1989
2633
|
}
|
|
1990
2634
|
|
|
2635
|
+
func (s *MatchService) buildLiveView(ctx context.Context, match Match) (*MatchLiveView, []string) {
|
|
2636
|
+
detailsRef := nonEmpty(strings.TrimSpace(match.DetailsRef), matchSubresourceRef(match, "details", "details"))
|
|
2637
|
+
playsRef := matchSubresourceRef(match, "plays", "plays")
|
|
2638
|
+
if detailsRef == "" && playsRef == "" {
|
|
2639
|
+
return nil, nil
|
|
2640
|
+
}
|
|
2641
|
+
primaryRef := nonEmpty(detailsRef, playsRef)
|
|
2642
|
+
deliveries, warnings, err := s.loadRecentDeliveryEventsFromRoute(ctx, primaryRef, liveViewRecentDeliveryFetchCount)
|
|
2643
|
+
if err != nil {
|
|
2644
|
+
warnings = append(warnings, fmt.Sprintf("live deliveries %s: %v", primaryRef, err))
|
|
2645
|
+
deliveries = nil
|
|
2646
|
+
}
|
|
2647
|
+
if len(deliveries) < 6 || len(warnings) > 0 {
|
|
2648
|
+
alternate := strings.TrimSpace(playsRef)
|
|
2649
|
+
if alternate != "" && alternate != primaryRef {
|
|
2650
|
+
altDeliveries, altWarnings, altErr := s.loadRecentDeliveryEventsFromRoute(ctx, alternate, liveViewRecentDeliveryFetchCount)
|
|
2651
|
+
if altErr != nil {
|
|
2652
|
+
warnings = append(warnings, fmt.Sprintf("live deliveries %s: %v", alternate, altErr))
|
|
2653
|
+
} else if len(altDeliveries) > 0 {
|
|
2654
|
+
deliveries = append(deliveries, altDeliveries...)
|
|
2655
|
+
warnings = append(warnings, altWarnings...)
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
deliveries = dedupeDeliveryEvents(deliveries)
|
|
2660
|
+
sortDeliveryEvents(deliveries)
|
|
2661
|
+
if len(deliveries) == 0 {
|
|
2662
|
+
return nil, warnings
|
|
2663
|
+
}
|
|
2664
|
+
latest := deliveries[len(deliveries)-1]
|
|
2665
|
+
|
|
2666
|
+
nameMap := map[string]string{}
|
|
2667
|
+
for _, event := range deliveries {
|
|
2668
|
+
bowlerName, batsmanName := parseNamesFromDeliveryShortText(event.ShortText)
|
|
2669
|
+
if strings.TrimSpace(event.BowlerPlayerID) != "" && strings.TrimSpace(bowlerName) != "" {
|
|
2670
|
+
if _, ok := nameMap[event.BowlerPlayerID]; !ok {
|
|
2671
|
+
nameMap[event.BowlerPlayerID] = bowlerName
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
if strings.TrimSpace(event.BatsmanPlayerID) != "" && strings.TrimSpace(batsmanName) != "" {
|
|
2675
|
+
if _, ok := nameMap[event.BatsmanPlayerID]; !ok {
|
|
2676
|
+
nameMap[event.BatsmanPlayerID] = batsmanName
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
score := firstNonEmpty(matchScoreLabel(latest.HomeScore), matchScoreLabel(latest.AwayScore))
|
|
2681
|
+
if score == "" {
|
|
2682
|
+
score = match.ScoreSummary
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
battingTeam := teamLabelByID(match, latest.TeamID)
|
|
2686
|
+
bowlingTeam := otherTeamLabelByID(match, latest.TeamID)
|
|
2687
|
+
if battingTeam == "" {
|
|
2688
|
+
battingTeam = firstNonEmpty(matchTeamsLabelFromMatch(match), latest.TeamID)
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
batters := make([]LiveBatterView, 0, 2)
|
|
2692
|
+
if latest.BatsmanPlayerID != "" {
|
|
2693
|
+
batters = append(batters, LiveBatterView{
|
|
2694
|
+
PlayerID: latest.BatsmanPlayerID,
|
|
2695
|
+
PlayerName: firstNonEmpty(nameMap[latest.BatsmanPlayerID], latest.BatsmanPlayerID),
|
|
2696
|
+
Runs: latest.BatsmanTotalRuns,
|
|
2697
|
+
Balls: latest.BatsmanBalls,
|
|
2698
|
+
Fours: latest.BatsmanFours,
|
|
2699
|
+
Sixes: latest.BatsmanSixes,
|
|
2700
|
+
StrikeRate: strikeRate(latest.BatsmanTotalRuns, latest.BatsmanBalls),
|
|
2701
|
+
OnStrike: true,
|
|
2702
|
+
})
|
|
2703
|
+
}
|
|
2704
|
+
if latest.OtherBatsmanID != "" {
|
|
2705
|
+
batters = append(batters, LiveBatterView{
|
|
2706
|
+
PlayerID: latest.OtherBatsmanID,
|
|
2707
|
+
PlayerName: firstNonEmpty(nameMap[latest.OtherBatsmanID], latest.OtherBatsmanID),
|
|
2708
|
+
Runs: latest.OtherBatterRuns,
|
|
2709
|
+
Balls: latest.OtherBatterBalls,
|
|
2710
|
+
Fours: latest.OtherBatterFours,
|
|
2711
|
+
Sixes: latest.OtherBatterSixes,
|
|
2712
|
+
StrikeRate: strikeRate(latest.OtherBatterRuns, latest.OtherBatterBalls),
|
|
2713
|
+
})
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
bowlers := make([]LiveBowlerView, 0, 2)
|
|
2717
|
+
if latest.BowlerPlayerID != "" {
|
|
2718
|
+
bowlers = append(bowlers, LiveBowlerView{
|
|
2719
|
+
PlayerID: latest.BowlerPlayerID,
|
|
2720
|
+
PlayerName: firstNonEmpty(nameMap[latest.BowlerPlayerID], latest.BowlerPlayerID),
|
|
2721
|
+
Overs: latest.BowlerOvers,
|
|
2722
|
+
Balls: latest.BowlerBalls,
|
|
2723
|
+
Maidens: latest.BowlerMaidens,
|
|
2724
|
+
Conceded: latest.BowlerConceded,
|
|
2725
|
+
Wickets: latest.BowlerWickets,
|
|
2726
|
+
Economy: economy(latest.BowlerConceded, latest.BowlerBalls, latest.BowlerOvers),
|
|
2727
|
+
})
|
|
2728
|
+
}
|
|
2729
|
+
if latest.OtherBowlerID != "" && latest.OtherBowlerID != latest.BowlerPlayerID {
|
|
2730
|
+
bowlers = append(bowlers, LiveBowlerView{
|
|
2731
|
+
PlayerID: latest.OtherBowlerID,
|
|
2732
|
+
PlayerName: firstNonEmpty(nameMap[latest.OtherBowlerID], latest.OtherBowlerID),
|
|
2733
|
+
Overs: latest.OtherBowlerOvers,
|
|
2734
|
+
Balls: latest.OtherBowlerBalls,
|
|
2735
|
+
Maidens: latest.OtherBowlerMaidens,
|
|
2736
|
+
Conceded: latest.OtherBowlerConceded,
|
|
2737
|
+
Wickets: latest.OtherBowlerWickets,
|
|
2738
|
+
Economy: economy(latest.OtherBowlerConceded, latest.OtherBowlerBalls, latest.OtherBowlerOvers),
|
|
2739
|
+
})
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
startRecent := len(deliveries) - 6
|
|
2743
|
+
if startRecent < 0 {
|
|
2744
|
+
startRecent = 0
|
|
2745
|
+
}
|
|
2746
|
+
recent := append([]DeliveryEvent(nil), deliveries[startRecent:]...)
|
|
2747
|
+
currentOver := make([]DeliveryEvent, 0, 6)
|
|
2748
|
+
for _, event := range deliveries {
|
|
2749
|
+
if event.OverNumber == latest.OverNumber && event.Period == latest.Period {
|
|
2750
|
+
currentOver = append(currentOver, event)
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
view := &MatchLiveView{
|
|
2755
|
+
Fixture: nonEmpty(match.ShortDescription, match.Description),
|
|
2756
|
+
Status: nonEmpty(match.MatchState, match.Note),
|
|
2757
|
+
Score: score,
|
|
2758
|
+
Overs: overBallString(latest.OverNumber, latest.BallNumber),
|
|
2759
|
+
CurrentOver: latest.OverNumber,
|
|
2760
|
+
BallInOver: latest.BallNumber,
|
|
2761
|
+
BattingTeam: battingTeam,
|
|
2762
|
+
BowlingTeam: bowlingTeam,
|
|
2763
|
+
Batters: batters,
|
|
2764
|
+
Bowlers: bowlers,
|
|
2765
|
+
RecentBalls: recent,
|
|
2766
|
+
CurrentBalls: currentOver,
|
|
2767
|
+
LastDetailID: strings.TrimSpace(latest.ID),
|
|
2768
|
+
LastUpdateMS: latest.BBBTimestamp,
|
|
2769
|
+
SnapshotAt: time.Now().UTC().Format(time.RFC3339),
|
|
2770
|
+
SourceRoute: primaryRef,
|
|
2771
|
+
}
|
|
2772
|
+
stale, reason := detectLiveStaleness(match.ScoreSummary, score)
|
|
2773
|
+
view.Stale = stale
|
|
2774
|
+
view.StaleReason = reason
|
|
2775
|
+
return view, compactWarnings(warnings)
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
func augmentScorecardFromLive(scorecard *MatchScorecard, live *MatchLiveView) {
|
|
2779
|
+
if scorecard == nil || live == nil {
|
|
2780
|
+
return
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
if len(scorecard.BattingCards) == 0 && len(live.Batters) > 0 {
|
|
2784
|
+
card := BattingCard{
|
|
2785
|
+
InningsNumber: 1,
|
|
2786
|
+
TeamName: live.BattingTeam,
|
|
2787
|
+
Runs: live.Score,
|
|
2788
|
+
Players: make([]BattingCardEntry, 0, len(live.Batters)),
|
|
2789
|
+
}
|
|
2790
|
+
for _, batter := range live.Batters {
|
|
2791
|
+
card.Players = append(card.Players, BattingCardEntry{
|
|
2792
|
+
PlayerID: batter.PlayerID,
|
|
2793
|
+
PlayerName: batter.PlayerName,
|
|
2794
|
+
Runs: strconv.Itoa(batter.Runs),
|
|
2795
|
+
BallsFaced: strconv.Itoa(batter.Balls),
|
|
2796
|
+
Fours: strconv.Itoa(batter.Fours),
|
|
2797
|
+
Sixes: strconv.Itoa(batter.Sixes),
|
|
2798
|
+
})
|
|
2799
|
+
}
|
|
2800
|
+
scorecard.BattingCards = append(scorecard.BattingCards, card)
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
if len(scorecard.BowlingCards) == 0 && len(live.Bowlers) > 0 {
|
|
2804
|
+
card := BowlingCard{
|
|
2805
|
+
InningsNumber: 1,
|
|
2806
|
+
TeamName: live.BowlingTeam,
|
|
2807
|
+
Players: make([]BowlingCardEntry, 0, len(live.Bowlers)),
|
|
2808
|
+
}
|
|
2809
|
+
for _, bowler := range live.Bowlers {
|
|
2810
|
+
card.Players = append(card.Players, BowlingCardEntry{
|
|
2811
|
+
PlayerID: bowler.PlayerID,
|
|
2812
|
+
PlayerName: bowler.PlayerName,
|
|
2813
|
+
Overs: overFromBallsOrFloat(bowler.Balls, bowler.Overs),
|
|
2814
|
+
Maidens: strconv.Itoa(bowler.Maidens),
|
|
2815
|
+
Conceded: strconv.Itoa(bowler.Conceded),
|
|
2816
|
+
Wickets: strconv.Itoa(bowler.Wickets),
|
|
2817
|
+
EconomyRate: fmt.Sprintf("%.2f", bowler.Economy),
|
|
2818
|
+
})
|
|
2819
|
+
}
|
|
2820
|
+
scorecard.BowlingCards = append(scorecard.BowlingCards, card)
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
func strikeRate(runs, balls int) float64 {
|
|
2825
|
+
if balls <= 0 {
|
|
2826
|
+
return 0
|
|
2827
|
+
}
|
|
2828
|
+
return float64(runs) * 100.0 / float64(balls)
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
func economy(conceded, balls int, overs float64) float64 {
|
|
2832
|
+
if balls > 0 {
|
|
2833
|
+
return float64(conceded) / (float64(balls) / 6.0)
|
|
2834
|
+
}
|
|
2835
|
+
if overs > 0 {
|
|
2836
|
+
return float64(conceded) / overs
|
|
2837
|
+
}
|
|
2838
|
+
return 0
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
func overFromBallsOrFloat(balls int, overs float64) string {
|
|
2842
|
+
if balls > 0 {
|
|
2843
|
+
return fmt.Sprintf("%d.%d", balls/6, balls%6)
|
|
2844
|
+
}
|
|
2845
|
+
if overs > 0 {
|
|
2846
|
+
return fmt.Sprintf("%.1f", overs)
|
|
2847
|
+
}
|
|
2848
|
+
return "0.0"
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
func teamLabelByID(match Match, teamID string) string {
|
|
2852
|
+
teamID = strings.TrimSpace(teamID)
|
|
2853
|
+
for _, team := range match.Teams {
|
|
2854
|
+
if strings.TrimSpace(team.ID) == teamID {
|
|
2855
|
+
return nonEmpty(strings.TrimSpace(team.ShortName), strings.TrimSpace(team.Name), teamID)
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return ""
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
func otherTeamLabelByID(match Match, teamID string) string {
|
|
2862
|
+
teamID = strings.TrimSpace(teamID)
|
|
2863
|
+
for _, team := range match.Teams {
|
|
2864
|
+
if strings.TrimSpace(team.ID) != teamID {
|
|
2865
|
+
return nonEmpty(strings.TrimSpace(team.ShortName), strings.TrimSpace(team.Name), strings.TrimSpace(team.ID))
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
return ""
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
func matchTeamsLabelFromMatch(match Match) string {
|
|
2872
|
+
parts := make([]string, 0, len(match.Teams))
|
|
2873
|
+
for _, team := range match.Teams {
|
|
2874
|
+
label := nonEmpty(strings.TrimSpace(team.ShortName), strings.TrimSpace(team.Name), strings.TrimSpace(team.ID))
|
|
2875
|
+
if label != "" {
|
|
2876
|
+
parts = append(parts, label)
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
return strings.Join(parts, ", ")
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
func parseNamesFromDeliveryShortText(shortText string) (string, string) {
|
|
2883
|
+
shortText = strings.TrimSpace(shortText)
|
|
2884
|
+
if shortText == "" {
|
|
2885
|
+
return "", ""
|
|
2886
|
+
}
|
|
2887
|
+
toParts := strings.SplitN(shortText, " to ", 2)
|
|
2888
|
+
if len(toParts) != 2 {
|
|
2889
|
+
return "", ""
|
|
2890
|
+
}
|
|
2891
|
+
bowler := strings.TrimSpace(toParts[0])
|
|
2892
|
+
right := toParts[1]
|
|
2893
|
+
commaParts := strings.SplitN(right, ",", 2)
|
|
2894
|
+
if len(commaParts) == 0 {
|
|
2895
|
+
return "", ""
|
|
2896
|
+
}
|
|
2897
|
+
batsman := strings.TrimSpace(commaParts[0])
|
|
2898
|
+
return bowler, batsman
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
func detectLiveStaleness(matchScore, liveScore string) (bool, string) {
|
|
2902
|
+
matchRuns, matchWkts, okMatch := parseRunsWkts(matchScore)
|
|
2903
|
+
liveRuns, liveWkts, okLive := parseRunsWkts(liveScore)
|
|
2904
|
+
if !okMatch || !okLive {
|
|
2905
|
+
return false, ""
|
|
2906
|
+
}
|
|
2907
|
+
if liveRuns < matchRuns || liveWkts < matchWkts {
|
|
2908
|
+
return true, fmt.Sprintf("live snapshot %d/%d trails match summary %d/%d", liveRuns, liveWkts, matchRuns, matchWkts)
|
|
2909
|
+
}
|
|
2910
|
+
return false, ""
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
func parseRunsWkts(raw string) (int, int, bool) {
|
|
2914
|
+
raw = strings.TrimSpace(raw)
|
|
2915
|
+
if raw == "" {
|
|
2916
|
+
return 0, 0, false
|
|
2917
|
+
}
|
|
2918
|
+
idx := strings.Index(raw, "/")
|
|
2919
|
+
if idx <= 0 || idx >= len(raw)-1 {
|
|
2920
|
+
return 0, 0, false
|
|
2921
|
+
}
|
|
2922
|
+
left := idx - 1
|
|
2923
|
+
for left >= 0 && raw[left] >= '0' && raw[left] <= '9' {
|
|
2924
|
+
left--
|
|
2925
|
+
}
|
|
2926
|
+
right := idx + 1
|
|
2927
|
+
for right < len(raw) && raw[right] >= '0' && raw[right] <= '9' {
|
|
2928
|
+
right++
|
|
2929
|
+
}
|
|
2930
|
+
runStr := strings.TrimSpace(raw[left+1 : idx])
|
|
2931
|
+
wktStr := strings.TrimSpace(raw[idx+1 : right])
|
|
2932
|
+
runs, err1 := strconv.Atoi(runStr)
|
|
2933
|
+
wkts, err2 := strconv.Atoi(wktStr)
|
|
2934
|
+
if err1 != nil || err2 != nil {
|
|
2935
|
+
return 0, 0, false
|
|
2936
|
+
}
|
|
2937
|
+
return runs, wkts, true
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
func resolveDuelIdentity(deliveries []DeliveryEvent, query string, isBatter bool) (string, string) {
|
|
2941
|
+
q := normalizeAlias(query)
|
|
2942
|
+
if q == "" {
|
|
2943
|
+
return "", ""
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
type candidate struct {
|
|
2947
|
+
id string
|
|
2948
|
+
name string
|
|
2949
|
+
score int
|
|
2950
|
+
}
|
|
2951
|
+
best := candidate{}
|
|
2952
|
+
for _, delivery := range deliveries {
|
|
2953
|
+
bowlerName, batsmanName := parseNamesFromDeliveryShortText(delivery.ShortText)
|
|
2954
|
+
id := delivery.BowlerPlayerID
|
|
2955
|
+
name := bowlerName
|
|
2956
|
+
if isBatter {
|
|
2957
|
+
id = delivery.BatsmanPlayerID
|
|
2958
|
+
name = batsmanName
|
|
2959
|
+
}
|
|
2960
|
+
if strings.TrimSpace(id) == "" && strings.TrimSpace(name) == "" {
|
|
2961
|
+
continue
|
|
2962
|
+
}
|
|
2963
|
+
score := aliasMatchScore(normalizeAlias(nonEmpty(name, id)), q, strings.Fields(q))
|
|
2964
|
+
if normalizeAlias(id) == q {
|
|
2965
|
+
score = 1000
|
|
2966
|
+
}
|
|
2967
|
+
if score > best.score {
|
|
2968
|
+
best = candidate{id: strings.TrimSpace(id), name: strings.TrimSpace(name), score: score}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
if best.score >= 300 || best.score == 1000 {
|
|
2972
|
+
return best.id, best.name
|
|
2973
|
+
}
|
|
2974
|
+
return "", ""
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
func deliveryMatchesDuel(delivery DeliveryEvent, batterID, batterName, bowlerID, bowlerName string) bool {
|
|
2978
|
+
shortBowler, shortBatter := parseNamesFromDeliveryShortText(delivery.ShortText)
|
|
2979
|
+
batterMatch := strings.TrimSpace(batterID) != "" && strings.TrimSpace(delivery.BatsmanPlayerID) == strings.TrimSpace(batterID)
|
|
2980
|
+
if !batterMatch && normalizeAlias(batterName) != "" {
|
|
2981
|
+
batterMatch = normalizeAlias(shortBatter) == normalizeAlias(batterName)
|
|
2982
|
+
}
|
|
2983
|
+
if !batterMatch {
|
|
2984
|
+
return false
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
bowlerMatch := strings.TrimSpace(bowlerID) != "" && strings.TrimSpace(delivery.BowlerPlayerID) == strings.TrimSpace(bowlerID)
|
|
2988
|
+
if !bowlerMatch && normalizeAlias(bowlerName) != "" {
|
|
2989
|
+
bowlerMatch = normalizeAlias(shortBowler) == normalizeAlias(bowlerName)
|
|
2990
|
+
}
|
|
2991
|
+
return bowlerMatch
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
func matchScoreLabel(raw string) string {
|
|
2995
|
+
raw = strings.TrimSpace(raw)
|
|
2996
|
+
if raw == "" {
|
|
2997
|
+
return ""
|
|
2998
|
+
}
|
|
2999
|
+
if strings.Contains(raw, "/") {
|
|
3000
|
+
return raw
|
|
3001
|
+
}
|
|
3002
|
+
return ""
|
|
3003
|
+
}
|
|
3004
|
+
|
|
1991
3005
|
func isSparseSituation(situation *MatchSituation) bool {
|
|
1992
3006
|
if situation == nil {
|
|
1993
3007
|
return true
|
|
1994
3008
|
}
|
|
1995
|
-
return len(situation.Data) == 0
|
|
3009
|
+
return len(situation.Data) == 0 && situation.Live == nil
|
|
1996
3010
|
}
|
|
1997
3011
|
|
|
1998
3012
|
func isLiveMatch(match Match) bool {
|