de2fee2617298ac588ed981f0ca28551f8bcce5e
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
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