de2fee2617298ac588ed981f0ca28551f8bcce5e

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add album guessing

Diff

This diff is truncated to protect this page.

  1diff --git a/AGENTS.md b/AGENTS.md
  2index 1cf7d32c6ab507366277ddcbcf3f324ee0f635e1..01610213db82d62d58acfa58eb61fda80c260095 100644
  3--- a/AGENTS.md
  4+++ b/AGENTS.md
  5@@ -36,4 +36,4 @@ Single `main` package, one file per concern:
  6 - Keep the single-package layout; don't split into subpackages.
  7 - External dependencies are ffmpeg and aplay — shell out to them rather
  8   than adding Go audio libraries.
  9-- Build with `go build ./...` and run `go test ./...` after changes.
 10+- Check with `golangci-lint ./...` and run `go test ./...` after changes.
 11diff --git a/audio.go b/audio.go
 12index 676731098d6a69cd857814bb4fe227bc8d86a831..842c3884342b0e5a25f61ed6ed645f25f11cc036 100644
 13--- a/audio.go
 14+++ b/audio.go
 15@@ -107,7 +107,7 @@ func playSegment(songPath string, threshold, seconds int) error {
 16 	if err != nil {
 17 		return err
 18 	}
 19-	defer os.RemoveAll(dir)
 20+	defer func() { _ = os.RemoveAll(dir) }()
 21 
 22 	wavPath := filepath.Join(dir, "segment.wav")
 23 	if err := writeWAV(wavPath, samples[start:end]); err != nil {
 24diff --git a/catalog.go b/catalog.go
 25index 34dc52f724ebf6a85f5db6c62c4d64c97f983ad4..75adaaf28d9114fe4c473647cda1d52a646c5a2e 100644
 26--- a/catalog.go
 27+++ b/catalog.go
 28@@ -18,6 +18,7 @@ type Song struct {
 29 	Path   string
 30 	Title  string
 31 	Artist string
 32+	Album  string
 33 }
 34 
 35 var musicExts = map[string]bool{
 36@@ -86,14 +87,14 @@ func buildCatalog(dir, blacklistPath string, onProgress func(count int, current
 37 			onProgress(count, filepath.Base(fr.path))
 38 		}
 39 		if e, ok := cached[fr.path]; ok && e.Mtime == fr.mtime {
 40-			songs = append(songs, Song{Path: fr.path, Title: e.Title, Artist: e.Artist})
 41+			songs = append(songs, Song{Path: fr.path, Title: e.Title, Artist: e.Artist, Album: e.Album})
 42 			newCache[fr.path] = e
 43 			stats.FromCache++
 44 			continue
 45 		}
 46 		s := readSong(fr.path)
 47 		songs = append(songs, s)
 48-		newCache[fr.path] = cacheEntry{Title: s.Title, Artist: s.Artist, Mtime: fr.mtime}
 49+		newCache[fr.path] = cacheEntry{Title: s.Title, Artist: s.Artist, Album: s.Album, Mtime: fr.mtime}
 50 		stats.Scanned++
 51 	}
 52 
 53@@ -112,7 +113,7 @@ func loadBlacklistDirs(dir, path string) ([]string, error) {
 54 	if err != nil {
 55 		return nil, fmt.Errorf("open blacklist: %w", err)
 56 	}
 57-	defer f.Close()
 58+	defer func() { _ = f.Close() }()
 59 
 60 	var dirs []string
 61 	scanner := bufio.NewScanner(f)
 62@@ -146,18 +147,19 @@ func isExcluded(path string, dirs []string) bool {
 63 // every music file stays in the catalog.
 64 func readSong(path string) Song {
 65 	fallback := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
 66-	title, artist := fallback, ""
 67+	title, artist, album := fallback, "", ""
 68 
 69 	if f, err := os.Open(path); err == nil {
 70-		defer f.Close()
 71+		defer func() { _ = f.Close() }()
 72 		if m, err := tag.ReadFrom(f); err == nil {
 73 			if t := m.Title(); t != "" {
 74 				title = t
 75 			}
 76 			artist = m.Artist()
 77+			album = m.Album()
 78 		}
 79 	}
 80-	return Song{Path: path, Title: title, Artist: artist}
 81+	return Song{Path: path, Title: title, Artist: artist, Album: album}
 82 }
 83 
 84 // cacheEntry is the cached tag data for a single file, plus the mtime it was
 85@@ -165,6 +167,7 @@ func readSong(path string) Song {
 86 type cacheEntry struct {
 87 	Title  string `json:"title"`
 88 	Artist string `json:"artist"`
 89+	Album  string `json:"album"`
 90 	Mtime  int64  `json:"mtime"`
 91 }
 92 
 93@@ -178,9 +181,9 @@ type cacheFile struct {
 94 // directories and blacklists get separate caches.
 95 func cacheKey(absDir, blacklistPath string) string {
 96 	h := fnv.New64a()
 97-	io.WriteString(h, absDir)
 98-	io.WriteString(h, "\x00")
 99-	io.WriteString(h, blacklistPath)
100+	_, _ = io.WriteString(h, absDir)
101+	_, _ = io.WriteString(h, "\x00")
102+	_, _ = io.WriteString(h, blacklistPath)
103 	return fmt.Sprintf("%x", h.Sum64())
104 }
105 
106diff --git a/matching.go b/matching.go
107index 64db2a840de33c6de6b804bbe658c2195eb7960c..f9d619044f641f51789ddf5e4cc2f9ce16837c2d 100644
108--- a/matching.go
109+++ b/matching.go
110@@ -9,7 +9,8 @@ const matchThreshold = 0.7
111 const (
112 	pointsTitle    = 4
113 	pointsArtist   = 2
114-	pointsPerRound = pointsTitle + pointsArtist
115+	pointsAlbum    = 4
116+	pointsPerRound = pointsTitle + pointsArtist + pointsAlbum
117 )
118 
119 func fuzzyMatch(guess, answer string) bool {
120diff --git a/matching_test.go b/matching_test.go
121index 0ba358ffd7244f178c8337a7b56200e9cf064f8f..ab45bd87b4a840672121ccd408dd1bb47332447e 100644
122--- a/matching_test.go
123+++ b/matching_test.go
124@@ -33,27 +33,29 @@ func TestFuzzyMatch(t *testing.T) {
125 
126 func TestGuessScoring(t *testing.T) {
127 	for _, tt := range []struct {
128-		title, artist string
129-		hinted        bool
130-		points        int
131-		message       string
132+		title, artist, album string
133+		hinted               bool
134+		points               int
135+		message              string
136 	}{
137-		{"Hello", "Adele", false, 6, "Correct! +6 points"},
138-		{"Wrong", "Adele", false, 2, "Artist correct! +2 points"},
139-		{"Hello", "Wrong", false, 4, "Title correct! +4 points"},
140-		{"Wrong", "Wrong", false, 0, "Wrong."},
141-		{"", "", false, 0, "Wrong."},
142-		{"Hello", "Adele", true, 3, "Correct! +3 points (halved for 2s hint)"},
143-		{"Hello", "Wrong", true, 2, "Title correct! +2 points (halved for 2s hint)"},
144-		{"Wrong", "Adele", true, 1, "Artist correct! +1 point (halved for 2s hint)"},
145+		{"Hello", "Adele", "21", false, 10, "Correct! +10 points"},
146+		{"Wrong", "Adele", "21", false, 6, "Artist, Album correct! +6 points"},
147+		{"Hello", "Wrong", "21", false, 8, "Title, Album correct! +8 points"},
148+		{"Hello", "Adele", "Wrong", false, 6, "Title, Artist correct! +6 points"},
149+		{"Wrong", "Wrong", "Wrong", false, 0, "Wrong."},
150+		{"", "", "", false, 0, "Wrong."},
151+		{"Hello", "Adele", "21", true, 5, "Correct! +5 points (halved for 2s hint)"},
152+		{"Hello", "Wrong", "21", true, 4, "Title, Album correct! +4 points (halved for 2s hint)"},
153+		{"Wrong", "Adele", "21", true, 3, "Artist, Album correct! +3 points (halved for 2s hint)"},
154 	} {
155-		m := newModel([]Song{{Title: "Hello", Artist: "Adele"}}, 500, 1)
156+		m := newModel([]Song{{Title: "Hello", Artist: "Adele", Album: "21"}}, 500, 1)
157 		m.titleInput.SetValue(tt.title)
158 		m.artistInput.SetValue(tt.artist)
159+		m.albumInput.SetValue(tt.album)
160 		m.hinted = tt.hinted
161 		m.submitGuess()
162 		if m.score != tt.points || m.resultMsg != tt.message || m.st != stateResult {
163-			t.Fatalf("guess %q / %q (hinted=%v): score=%d, message=%q, state=%v", tt.title, tt.artist, tt.hinted, m.score, m.resultMsg, m.st)
164+			t.Fatalf("guess %q / %q / %q (hinted=%v): score=%d, message=%q, state=%v", tt.title, tt.artist, tt.album, tt.hinted, m.score, m.resultMsg, m.st)
165 		}
166 	}
167 }
168diff --git a/ui.go b/ui.go
169index ad4a5494e8b62a036671b342cc4ef18c718fbf53..29f40626228e5f914dcb464db205d23bd11ad21e 100644
170--- a/ui.go
171+++ b/ui.go
172@@ -33,7 +33,8 @@ type model struct {
173 
174 	titleInput  textinput.Model
175 	artistInput textinput.Model
176-	active      int // 0 = title, 1 = artist
177+	albumInput  textinput.Model
178+	active      int // 0 = title, 1 = artist, 2 = album
179 
180 	st        state
181 	playing   bool
182@@ -48,6 +49,8 @@ func newModel(catalog []Song, threshold, rounds int) model {
183 	ti.Prompt = "Title:  "
184 	tt := textinput.New()
185 	tt.Prompt = "Artist: "
186+	ta := textinput.New()
187+	ta.Prompt = "Album:  "
188 
189 	m := model{
190 		catalog:     catalog,
191@@ -55,6 +58,7 @@ func newModel(catalog []Song, threshold, rounds int) model {
192 		threshold:   threshold,
193 		titleInput:  ti,
194 		artistInput: tt,
195+		albumInput:  ta,
196 		order:       shuffled(len(catalog)),
197 		st:          statePlaying,
198 		playing:     true,
199@@ -127,10 +131,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
200 				}
201 				return m, m.submitGuess()
202 			default:
203-				if m.active == 0 {
204+				switch m.active {
205+				case 0:
206 					m.titleInput, _ = m.titleInput.Update(msg)
207-				} else if m.active == 1 {
208+				case 1:
209 					m.artistInput, _ = m.artistInput.Update(msg)
210+				default:
211+					m.albumInput, _ = m.albumInput.Update(msg)
212 				}
213 				return m, nil
214 			}
215@@ -168,20 +175,26 @@ func (m *model) beginRound(song Song) tea.Cmd {
216 	m.hinted = false
217 	m.titleInput.SetValue("")
218 	m.artistInput.SetValue("")
219+	m.albumInput.SetValue("")
220 	m.active = 0
221 	m.titleInput.Focus()
222 	m.artistInput.Blur()
223+	m.albumInput.Blur()
224 	return startAudio(song, m.threshold, 1)
225 }
226 
227 func (m *model) toggleField() {
228-	m.active = 1 - m.active
229-	if m.active == 0 {
230+	m.active = (m.active + 1) % 3
231+	for _, in := range []textinput.Model{m.titleInput, m.artistInput, m.albumInput} {
232+		in.Blur()
233+	}
234+	switch m.active {
235+	case 0:
236 		m.titleInput.Focus()
237-		m.artistInput.Blur()
238-	} else {
239+	case 1:
240 		m.artistInput.Focus()
241-		m.titleInput.Blur()
242+	default:
243+		m.albumInput.Focus()
244 	}
245 }
246 
247@@ -195,10 +208,11 @@ func (m *model) nextSong() Song {
248 	return m.catalog[idx]
249 }
250 
251-// submitGuess compares the entered title and artist against the answer.
252+// submitGuess compares the entered title, artist and album against the answer.
253 func (m *model) submitGuess() tea.Cmd {
254 	titleOK := fuzzyMatch(m.titleInput.Value(), m.answer.Title)
255 	artistOK := fuzzyMatch(m.artistInput.Value(), m.answer.Artist)
256+	albumOK := fuzzyMatch(m.albumInput.Value(), m.answer.Album)
257 
258 	points := 0
259 	m.correct = 0
260@@ -210,18 +224,30 @@ func (m *model) submitGuess() tea.Cmd {
261 		points += pointsArtist
262 		m.correct++
263 	}
264+	if albumOK {
265+		points += pointsAlbum
266+		m.correct++
267+	}
268 	if m.hinted {
269 		points /= 2
270 	}
271 	m.score += points