Skip to content

Commit a170e3d

Browse files
authored
Merge pull request #54 from 0xjuanma/replay-improvements
feat[replay]: enhance goal replay matching, ui and caching improvements
2 parents 7f45e64 + 8f8295e commit a170e3d

File tree

10 files changed

+168
-115
lines changed

10 files changed

+168
-115
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
### Changed
13+
- **Goal Link Indicator** - Replaced 📺 emoji with [▶REPLAY] text indicator for better terminal compatibility
14+
- **Goal Link Alignment** - Positioned replay links between player name and goal symbol for proper home/away expansion
15+
- **Goal Display** - Removed assist information from goal events, showing only the scorer's name
1316

1417
### Fixed
18+
- **Goal Link Cache Logic** - Improved caching behavior for goal replay links and fixed cache expiration logic for not-found
1519

1620
## [0.10.0] - 2026-01-03
1721

internal/reddit/cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const (
1818
CacheTTL = 7 * 24 * time.Hour // 7 days
1919
// NotFoundTTL defines how long to cache "not found" results.
2020
// Shorter than CacheTTL since links might appear later.
21-
NotFoundTTL = 24 * time.Hour // 1 day
21+
NotFoundTTL = 5 * time.Minute // 5 minutes
2222
// NotFoundMarker is a special URL indicating "searched but not found"
2323
NotFoundMarker = "__NOT_FOUND__"
2424
)

internal/reddit/client.go

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,17 +243,56 @@ func (c *Client) GetGoalLinks(goals []GoalInfo) map[GoalLinkKey]*GoalLink {
243243

244244
// searchForGoal searches Reddit for a specific goal.
245245
func (c *Client) searchForGoal(goal GoalInfo) (*GoalLink, error) {
246-
// Build search query with team names and minute
247-
// Posts usually follow: "Team A [1] - 0 Team B - Player Name 45'"
248-
query := fmt.Sprintf("%s %s %d'", goal.HomeTeam, goal.AwayTeam, goal.Minute)
249-
250-
results, err := c.fetcher.Search(query, 10, goal.MatchTime)
251-
if err != nil {
252-
return nil, fmt.Errorf("search reddit: %w", err)
246+
// Strategy 1: Both teams + minute (most specific, try first)
247+
query1 := fmt.Sprintf("%s %s %d'", goal.HomeTeam, goal.AwayTeam, goal.Minute)
248+
results1, err := c.fetcher.Search(query1, 15, goal.MatchTime)
249+
if err == nil {
250+
// Check if we found a good match with the first strategy
251+
match := findBestMatch(results1, goal)
252+
if match != nil {
253+
// Found a match, return it immediately to avoid additional API calls
254+
return &GoalLink{
255+
MatchID: goal.MatchID,
256+
Minute: goal.Minute,
257+
URL: match.URL,
258+
Title: match.Title,
259+
PostURL: match.PostURL,
260+
FetchedAt: time.Now(),
261+
}, nil
262+
}
263+
}
264+
265+
// Strategy 1 didn't find a match, try broader searches
266+
// Only try one additional strategy to balance coverage vs rate limiting
267+
var allResults []SearchResult
268+
if err == nil {
269+
allResults = append(allResults, results1...)
270+
}
271+
272+
// Strategy 2: Try with just the scoring team + minute
273+
// Determine which team scored
274+
scoringTeam := goal.AwayTeam
275+
if goal.IsHomeTeam {
276+
scoringTeam = goal.HomeTeam
277+
}
278+
query2 := fmt.Sprintf("%s %d'", scoringTeam, goal.Minute)
279+
results2, err := c.fetcher.Search(query2, 15, goal.MatchTime)
280+
if err == nil {
281+
allResults = append(allResults, results2...)
282+
}
283+
284+
// Remove duplicates based on URL
285+
seen := make(map[string]bool)
286+
uniqueResults := make([]SearchResult, 0, len(allResults))
287+
for _, result := range allResults {
288+
if !seen[result.URL] {
289+
seen[result.URL] = true
290+
uniqueResults = append(uniqueResults, result)
291+
}
253292
}
254293

255294
// Find the best matching result
256-
match := findBestMatch(results, goal)
295+
match := findBestMatch(uniqueResults, goal)
257296
if match == nil {
258297
return nil, nil // No match found, but not an error
259298
}

internal/reddit/matcher.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ func normalizeTeamName(name string) string {
101101
// Convert to lowercase
102102
norm := strings.ToLower(name)
103103

104+
// Remove common prefixes (e.g., "fc barcelona" -> "barcelona")
105+
prefixes := []string{"fc ", "cf ", "sc ", "afc ", "ac ", "as "}
106+
for _, prefix := range prefixes {
107+
norm = strings.TrimPrefix(norm, prefix)
108+
}
109+
104110
// Remove common suffixes
105111
suffixes := []string{" fc", " cf", " sc", " afc", " united", " city"}
106112
for _, suffix := range suffixes {
@@ -122,23 +128,40 @@ func normalizeName(name string) string {
122128
}
123129

124130
// containsTeamName checks if a title contains a team name (or part of it).
131+
// Normalizes the title first to handle variations like "FC Barcelona" vs "Barcelona".
125132
func containsTeamName(title, teamNorm string) bool {
126-
// First try exact match
127-
if strings.Contains(title, teamNorm) {
133+
// Normalize the title for comparison (handles "FC Barcelona" -> "barcelona")
134+
titleNorm := normalizeTeamName(title)
135+
136+
// First try exact match on normalized title
137+
if strings.Contains(titleNorm, teamNorm) {
128138
return true
129139
}
130140

131141
// Try matching individual words (for multi-word team names)
132142
words := strings.Fields(teamNorm)
133143
if len(words) > 1 {
134-
// Check if the main word (usually the first significant word) is present
144+
// Check if significant words are present
135145
for _, word := range words {
136-
if len(word) > 3 && strings.Contains(title, word) {
146+
if len(word) > 3 && strings.Contains(titleNorm, word) {
137147
return true
138148
}
139149
}
140150
}
141151

152+
// Also check original title (case-insensitive) for better coverage
153+
titleLower := strings.ToLower(title)
154+
if strings.Contains(titleLower, teamNorm) {
155+
return true
156+
}
157+
158+
// Check individual words in original title too
159+
for _, word := range words {
160+
if len(word) > 3 && strings.Contains(titleLower, word) {
161+
return true
162+
}
163+
}
164+
142165
return false
143166
}
144167

internal/ui/colors.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package ui
22

3-
import "github.com/charmbracelet/lipgloss"
4-
53
// Consolidated color palette for all views - Red & Cyan theme
4+
// These aliases reference the main color definitions in neon_styles.go
65
var (
76
// Primary colors
8-
textColor = lipgloss.Color("15") // White
9-
accentColor = lipgloss.Color("51") // Bright cyan
10-
secondaryColor = lipgloss.Color("196") // Bright red
11-
dimColor = lipgloss.Color("244") // Gray
12-
highlightColor = lipgloss.Color("51") // Cyan highlight (same as accent)
13-
borderColor = lipgloss.Color("51") // Cyan borders (same as accent)
7+
textColor = neonWhiteAlt // Standard white
8+
accentColor = neonCyan // Bright cyan
9+
secondaryColor = neonRed // Bright red
10+
dimColor = neonDim // Gray
11+
highlightColor = neonCyan // Cyan highlight (same as accent)
12+
borderColor = neonCyan // Cyan borders (same as accent)
1413
)

internal/ui/hyperlink.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func CreateGoalLinkDisplay(goalText, replayURL string) string {
6767
// Otherwise, return unchanged text (no visible change to user)
6868
if supportsHyperlinks() {
6969
// Create a clickable indicator
70-
indicator := "📺"
70+
indicator := ReplayLinkIndicator
7171
linkedIndicator := Hyperlink(indicator, replayURL)
7272
if goalText == "" {
7373
return linkedIndicator
@@ -157,7 +157,7 @@ func OpenURL(url string) error {
157157
}
158158

159159
// ReplayLinkIndicator is the visual indicator for replay links.
160-
const ReplayLinkIndicator = "📺"
160+
const ReplayLinkIndicator = "[▶REPLAY]"
161161

162162
// ReplayLinkIndicatorAlt is an alternative ASCII indicator for terminals without emoji.
163-
const ReplayLinkIndicatorAlt = "[]"
163+
const ReplayLinkIndicatorAlt = "[replay]"

internal/ui/list_delegate.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import (
55
"github.com/charmbracelet/lipgloss"
66
)
77

8-
// Neon colors used across delegates.
8+
// Use consolidated neon colors from neon_styles.go
9+
// These aliases are kept for backward compatibility but reference the main color definitions
910
var (
10-
delegateNeonRed = lipgloss.Color("196")
11-
delegateNeonCyan = lipgloss.Color("51")
12-
delegateNeonWhite = lipgloss.Color("255")
13-
delegateNeonGray = lipgloss.Color("244")
14-
delegateNeonDim = lipgloss.Color("238")
11+
delegateNeonRed = neonRed
12+
delegateNeonCyan = neonCyan
13+
delegateNeonWhite = neonWhite
14+
delegateNeonGray = neonDim
15+
delegateNeonDim = neonDimGray
1516
)
1617

1718
// NewMatchListDelegate creates a custom list delegate for match items.
@@ -23,12 +24,7 @@ func NewMatchListDelegate() list.DefaultDelegate {
2324
// Set height to 3 lines: title (1) + description with KO time (2)
2425
d.SetHeight(3)
2526

26-
// Neon colors
27-
neonRed := lipgloss.Color("196")
28-
neonCyan := lipgloss.Color("51")
29-
neonWhite := lipgloss.Color("255")
30-
neonGray := lipgloss.Color("244")
31-
neonDim := lipgloss.Color("238")
27+
// Use consolidated neon colors from neon_styles.go
3228

3329
// Selected items: Neon red title, cyan description, red left border
3430
d.Styles.SelectedTitle = lipgloss.NewStyle().

internal/ui/list_panels.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ func renderStatsMatchDetailsPanel(width, height int, details *api.MatchDetails,
469469
lines = append(lines, largeScore)
470470
} else {
471471
vsText := lipgloss.NewStyle().
472-
Foreground(lipgloss.Color("244")).
472+
Foreground(neonDim).
473473
Width(contentWidth).
474474
Align(lipgloss.Center).
475475
Render("vs")
@@ -516,19 +516,16 @@ func renderStatsMatchDetailsPanel(width, height int, details *api.MatchDetails,
516516
player = *g.Player
517517
}
518518
playerDetails := neonValueStyle.Render(player)
519-
if g.Assist != nil && *g.Assist != "" {
520-
playerDetails += neonDimStyle.Render(fmt.Sprintf(" (%s)", *g.Assist))
521-
}
522519

523-
// Check for replay link and add indicator
520+
// Check for replay link and create indicator
524521
replayURL := goalLinks.GetReplayURL(details.ID, g.Minute)
525522
replayIndicator := ""
526523
if replayURL != "" {
527-
// Add clickable replay indicator with hyperlink
528-
replayIndicator = " " + CreateGoalLinkDisplay("", replayURL)
524+
// Create clickable replay indicator with hyperlink
525+
replayIndicator = CreateGoalLinkDisplay("", replayURL)
529526
}
530527

531-
goalContent := buildEventContent(playerDetails+replayIndicator, "●", neonScoreStyle.Render("GOAL"), isHome)
528+
goalContent := buildEventContent(playerDetails, replayIndicator, "●", neonScoreStyle.Render("GOAL"), isHome)
532529
goalLine := renderCenterAlignedEvent(fmt.Sprintf("%d'", g.Minute), goalContent, isHome, contentWidth)
533530
lines = append(lines, goalLine)
534531
}
@@ -565,7 +562,7 @@ func renderStatsMatchDetailsPanel(width, height int, details *api.MatchDetails,
565562

566563
// Build card content with symbol+type adjacent to center time
567564
playerDetails := neonValueStyle.Render(player)
568-
cardContent := buildEventContent(playerDetails, cardSymbol, cardStyle.Render("CARD"), isHome)
565+
cardContent := buildEventContent(playerDetails, "", cardSymbol, cardStyle.Render("CARD"), isHome)
569566
cardLine := renderCenterAlignedEvent(fmt.Sprintf("%d'", card.Minute), cardContent, isHome, contentWidth)
570567
lines = append(lines, cardLine)
571568
}
@@ -726,7 +723,7 @@ func renderStatComparison(label, homeVal, awayVal string, maxWidth int) string {
726723
}
727724
awayEmpty := halfBar - awayFilled
728725
awayBar := strings.Repeat("▪", awayFilled) + strings.Repeat(" ", awayEmpty)
729-
awayBarStyled := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(awayBar)
726+
awayBarStyled := lipgloss.NewStyle().Foreground(neonGray).Render(awayBar)
730727

731728
// Line 1: Label (centered via parent, no width constraint)
732729
labelStyle := lipgloss.NewStyle().Foreground(neonDim)

internal/ui/neon_styles.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@ const (
1515

1616
var (
1717
// Neon color palette - Golazo brand
18-
neonRed = lipgloss.Color("196") // Bright red
19-
neonCyan = lipgloss.Color("51") // Electric cyan
20-
neonYellow = lipgloss.Color("226") // Bright yellow for cards
21-
neonWhite = lipgloss.Color("255") // Pure white
18+
// Primary colors
19+
neonRed = lipgloss.Color("196") // Bright red
20+
neonCyan = lipgloss.Color("51") // Electric cyan
21+
neonYellow = lipgloss.Color("226") // Bright yellow for cards
22+
neonWhite = lipgloss.Color("255") // Pure white (bright)
23+
neonWhiteAlt = lipgloss.Color("15") // Standard white
24+
neonBlack = lipgloss.Color("0") // Black
25+
26+
// Gray scale
2227
neonDark = lipgloss.Color("236") // Dark background
23-
neonDim = lipgloss.Color("244") // Gray dim text
2428
neonDarkDim = lipgloss.Color("239") // Slightly lighter dark
29+
neonGray = lipgloss.Color("240") // Medium gray
30+
neonDim = lipgloss.Color("244") // Gray dim text
31+
neonDimGray = lipgloss.Color("238") // Dim gray (for delegates)
2532

2633
// Card styles - reusable across all views
2734
neonYellowCardStyle = lipgloss.NewStyle().Foreground(neonYellow).Bold(true)

0 commit comments

Comments
 (0)