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:
+52
-24
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user