fix: resolve all lint errors and make linting a required CI check (#62)

- Fix 10 err113 violations: extract dynamic errors to package-level sentinels
- Fix 12 errcheck violations: handle unchecked error returns in player,
  metadata, and config packages
- Fix 4 revive stutter warnings: rename player.PlayerState to player.State,
  player.PlayerVolume to player.Volume, queue.QueueTrack to queue.Track,
  queue.QueueState to queue.State
- Fix 2 staticcheck SA4001: simplify *&x to x in assets handler
- Fix 5 unused constants: remove dead AudioFileType iota block in models
- Fix gci/gofumpt/wsl formatting issues across multiple files
- Add gofumpt module-path setting to .golangci.yml for correct import grouping
- Fix player test: gate integration test behind YELLOWJACKET_INTEGRATION env var
  instead of only skipping in CI, and replace t.Errorf+t.Failed with t.Fatalf
- Remove continue-on-error from golangci-lint CI step so linting is now required
This commit is contained in:
2026-02-14 01:52:27 -06:00
committed by GitHub
parent 2d6e22105d
commit 30b2480df4
16 changed files with 159 additions and 98 deletions
+52 -24
View File
@@ -28,7 +28,7 @@ type Player struct {
ctx context.Context
logger *slog.Logger
db *database.DB
state PlayerState
state State
currentFile *os.File
format beep.Format
baseStreamer beep.Streamer
@@ -40,14 +40,22 @@ type Player struct {
playbackFinishedHandler func()
}
// PlayerState represents the current playback state.
type PlayerState string
// State represents the current playback state.
type State string
// Playback state values.
const (
Playing PlayerState = "playing"
Paused PlayerState = "paused"
Stopped PlayerState = "stopped"
Playing State = "playing"
Paused State = "paused"
Stopped State = "stopped"
)
// Sentinel errors for player operations.
var (
errNoControlStreamer = errors.New("no control streamer")
errNoAudioFileLoaded = errors.New("no audio file loaded")
errNoStreamerToPlay = errors.New("no streamer to play")
errNoAudioStream = errors.New("no audio stream to pause")
)
var speakerSampleRate = beep.SampleRate(44100)
@@ -96,11 +104,17 @@ func (p *Player) registerEventHandlers() {
runtime.EventsOn(p.ctx, events.RequestPlay, func(_ ...any) {
p.logger.Info("Received RequestPlayEvent")
p.Play()
if err := p.Play(); err != nil {
p.logger.Error("failed to play", "err", err)
}
})
runtime.EventsOn(p.ctx, events.RequestPause, func(_ ...any) {
p.logger.Info("Received RequestPauseEvent")
p.Pause()
if err := p.Pause(); err != nil {
p.logger.Error("failed to pause", "err", err)
}
})
runtime.EventsOn(p.ctx, events.RequestLoadFile, func(data ...any) {
p.logger.Info("Received RequestLoadFileEvent")
@@ -140,7 +154,7 @@ func (p *Player) registerEventHandlers() {
}
// emitPlaybackStateChanged emits a playback state change event.
func (p *Player) emitPlaybackStateChanged(state PlayerState) {
func (p *Player) emitPlaybackStateChanged(state State) {
if p.ctx == nil {
p.logger.Error("Context is nil, cannot emit event")
@@ -202,6 +216,7 @@ func (p *Player) emitTrackChanged() {
// Compute current seek position in seconds.
seekPosition := 0
if p.seeker != nil {
speaker.Lock()
seekPosition = p.seeker.Position() / int(p.format.SampleRate)
@@ -312,12 +327,17 @@ func (p *Player) LoadFile(filePath string) error {
speaker.Unlock()
if p.currentFile != nil {
p.currentFile.Close()
if closeErr := p.currentFile.Close(); closeErr != nil {
p.logger.Warn("failed to close previous audio file", "err", closeErr)
}
}
p.currentFile = f
p.updateStreamers(streamer, format.SampleRate)
if err := p.updateStreamers(streamer, format.SampleRate); err != nil {
return fmt.Errorf("failed to update streamers: %w", err)
}
p.startPaused()
p.emitPlaybackStateChanged(p.state)
p.emitTrackChanged()
@@ -328,15 +348,15 @@ func (p *Player) LoadFile(filePath string) error {
func (p *Player) validateReadyToPlay() error {
if p.control == nil {
return errors.New("no control streamer")
return errNoControlStreamer
}
if p.currentFile == nil {
return errors.New("no audio file loaded")
return errNoAudioFileLoaded
}
if p.speakerStreamer == nil {
return errors.New("no streamer to play")
return errNoStreamerToPlay
}
return nil
@@ -365,7 +385,10 @@ func (p *Player) Play() error {
return fmt.Errorf("failed to seek to beginning: %w", err)
}
p.updateStreamers(p.seeker, p.format.SampleRate)
if err := p.updateStreamers(p.seeker, p.format.SampleRate); err != nil {
return fmt.Errorf("failed to update streamers for replay: %w", err)
}
p.startPaused()
p.logger.Info("Rebuilt streamers for replay")
}
@@ -385,7 +408,7 @@ func (p *Player) Play() error {
// Pause pauses the current playback.
func (p *Player) Pause() error {
if p.control == nil {
return errors.New("no audio stream to pause")
return errNoAudioStream
}
if p.state == Paused {
@@ -416,7 +439,7 @@ func (p *Player) SetVolume(desiredVolume UserVolume) error {
volume := clampVolume(desiredVolume)
// Apply the volume settings
p.volume.Volume = float64(volume.ToPlayerVolume())
p.volume.Volume = float64(volume.ToVolume())
p.volume.Silent = volume == MinUserVol
speaker.Unlock()
@@ -429,7 +452,7 @@ func (p *Player) ChangeVolume(deltaVolume int) error {
}
func (p *Player) getUserVolume() UserVolume {
return PlayerVolume(p.volume.Volume).ToUserVolume()
return Volume(p.volume.Volume).ToUserVolume()
}
// MuteToggle toggles the mute state.
@@ -442,7 +465,7 @@ func (p *Player) MuteToggle() error {
// CurrentPositionSeconds returns the current playback position in seconds.
func (p *Player) CurrentPositionSeconds() (int, error) {
if p.seeker == nil {
return 0, errors.New("no audio file loaded")
return 0, errNoAudioFileLoaded
}
speaker.Lock()
@@ -455,7 +478,7 @@ func (p *Player) CurrentPositionSeconds() (int, error) {
// CurrentPosition returns the playback position as a percentage (0-100).
func (p *Player) CurrentPosition() (int, error) {
if p.seeker == nil {
return 0, errors.New("no audio file loaded")
return 0, errNoAudioFileLoaded
}
speaker.Lock()
@@ -470,7 +493,7 @@ func (p *Player) Seek(targetSeconds int) error {
if p.seeker == nil {
runtime.EventsEmit(p.ctx, events.SeekFailed)
return errors.New("no audio file loaded")
return errNoAudioFileLoaded
}
lengthSecs, err := p.TrackLengthInSeconds()
@@ -491,7 +514,13 @@ func (p *Player) Seek(targetSeconds int) error {
"samples",
samples,
)
p.seeker.Seek(samples)
if seekErr := p.seeker.Seek(samples); seekErr != nil {
speaker.Unlock()
return fmt.Errorf("failed to seek: %w", seekErr)
}
speaker.Unlock()
return nil
@@ -553,7 +582,7 @@ func (p *Player) GetCurrentTrackInfo() (map[string]interface{}, error) {
// TrackLengthInSeconds returns the duration of the current track.
func (p *Player) TrackLengthInSeconds() (int, error) {
if p.seeker == nil {
return 0, errors.New("no audio file loaded")
return 0, errNoAudioFileLoaded
}
speaker.Lock()
@@ -677,7 +706,6 @@ func (p *Player) RestoreState() {
)
}
}
}
p.logger.Info("Player state restored",
+13 -10
View File
@@ -14,20 +14,25 @@ var testQueue = []string{
}
func TestPlayer(t *testing.T) {
// This test requires a Wails runtime context for event registration and
// an audio device for playback. Skip in CI where neither is available.
if os.Getenv("CI") != "" {
t.Skip("skipping: requires Wails runtime context and audio device")
// This is an integration test that requires:
// 1. A Wails runtime context (SetContext calls runtime.EventsOn)
// 2. An audio output device (speaker.Init)
//
// Skip unless the caller explicitly opts in via YELLOWJACKET_INTEGRATION=1.
if os.Getenv("YELLOWJACKET_INTEGRATION") == "" {
t.Skip(
"skipping: integration test requires Wails runtime and audio device (set YELLOWJACKET_INTEGRATION=1 to run)",
)
}
t.Logf("Starting test")
p, err := NewPlayer(context.Background(), slog.Default(), nil)
if err != nil {
t.Errorf("could not create player\n%s", err.Error())
t.Failed()
t.Fatalf("could not create player: %s", err.Error())
}
// SetContext registers Wails event handlers; only works with a real Wails context.
p.SetContext(t.Context())
t.Logf("initializing player")
@@ -36,14 +41,12 @@ func TestPlayer(t *testing.T) {
err = p.LoadFile(track)
if err != nil {
t.Errorf("could not load file\n%s\n%s", track, err.Error())
t.Failed()
t.Fatalf("could not load file %s: %s", track, err.Error())
}
err = p.Play()
if err != nil {
t.Errorf("could not play file\n%s\n%s", track, err.Error())
t.Failed()
t.Fatalf("could not play file %s: %s", track, err.Error())
}
}
}
+14 -14
View File
@@ -3,8 +3,8 @@ package player
// UserVolume represents volume on a user-facing scale (0-100).
type UserVolume int
// PlayerVolume represents volume on an internal scale (-10 to 10).
type PlayerVolume float64
// Volume represents volume on an internal scale (-4 to 0).
type Volume float64
// User volume range bounds.
const (
@@ -12,31 +12,31 @@ const (
MaxUserVol UserVolume = 100
)
// Player volume range bounds.
// Internal volume range bounds.
const (
MinPlayerVol PlayerVolume = -4
MaxPlayerVol PlayerVolume = 0
MinVol Volume = -4
MaxVol Volume = 0
)
// ToPlayerVolume converts user volume to internal player volume.
func (oldVol UserVolume) ToPlayerVolume() PlayerVolume {
var newVol PlayerVolume
// ToVolume converts user volume to internal player volume.
func (oldVol UserVolume) ToVolume() Volume {
var newVol Volume
if oldVol >= MinUserVol && oldVol <= MaxUserVol {
ratio := PlayerVolume(oldVol-MinUserVol) / PlayerVolume(MaxUserVol-MinUserVol)
newVol = ratio*(MaxPlayerVol-MinPlayerVol) + MinPlayerVol
ratio := Volume(oldVol-MinUserVol) / Volume(MaxUserVol-MinUserVol)
newVol = ratio*(MaxVol-MinVol) + MinVol
}
return newVol
}
// ToUserVolume converts internal player volume to user volume.
func (oldVolFloat PlayerVolume) ToUserVolume() UserVolume {
func (oldVolFloat Volume) ToUserVolume() UserVolume {
var newVol UserVolume
if oldVolFloat >= MinPlayerVol && oldVolFloat <= MaxPlayerVol {
ratio := (oldVolFloat - MinPlayerVol) / (MaxPlayerVol - MinPlayerVol)
newVol = UserVolume(ratio*PlayerVolume(MaxUserVol-MinUserVol)) + MinUserVol
if oldVolFloat >= MinVol && oldVolFloat <= MaxVol {
ratio := (oldVolFloat - MinVol) / (MaxVol - MinVol)
newVol = UserVolume(ratio*Volume(MaxUserVol-MinUserVol)) + MinUserVol
}
return newVol