diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8124e8f..94127fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,6 @@ jobs: run: go vet -tags webkit2_41 ./... - name: golangci-lint - continue-on-error: true uses: golangci/golangci-lint-action@v9 with: version: v2.5.0 diff --git a/.golangci.yml b/.golangci.yml index 1dd6fc4..a78482c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -42,3 +42,5 @@ formatters: - standard - default - prefix(yellowjacket) + gofumpt: + module-path: yellowjacket diff --git a/backend/assets/handler.go b/backend/assets/handler.go index 47d944d..abd5cf9 100644 --- a/backend/assets/handler.go +++ b/backend/assets/handler.go @@ -39,7 +39,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.logger.Debug( "custom handler for request not found, using wails asset handler", "path", - *&r.URL.Path, + r.URL.Path, ) h.wailsAssetHandler.ServeHTTP(w, r) @@ -49,7 +49,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.logger.Debug( "using custom handler for request", "path", - *&r.URL.Path, + r.URL.Path, ) h.serveMux.ServeHTTP(w, r) } diff --git a/backend/config/config.go b/backend/config/config.go index 4453f5a..5cf67a7 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -11,6 +11,7 @@ import ( "path" "github.com/BurntSushi/toml" + "yellowjacket/backend/library" "yellowjacket/backend/system" ) diff --git a/backend/config/httphandler.go b/backend/config/httphandler.go index 4ac6cce..3e6ab7b 100644 --- a/backend/config/httphandler.go +++ b/backend/config/httphandler.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/schema" "github.com/wailsapp/wails/v2/pkg/runtime" + "yellowjacket/backend/events" ) @@ -27,13 +28,20 @@ func (c *Config) handle(w http.ResponseWriter, r *http.Request) { case http.MethodPost: if err := c.handleConfigPost(r); err != nil { c.logger.Error("problem handling config post request", "err", err.Error()) - c.formSubmitError(err.Error()).Render(r.Context(), w) + + if renderErr := c.formSubmitError(err.Error()).Render(r.Context(), w); renderErr != nil { + c.logger.Error("problem rendering error response", "err", renderErr.Error()) + } + w.WriteHeader(http.StatusInternalServerError) return } - c.formSubmitSuccess().Render(r.Context(), w) + if err := c.formSubmitSuccess().Render(r.Context(), w); err != nil { + c.logger.Error("problem rendering success response", "err", err.Error()) + } + w.WriteHeader(http.StatusOK) } } diff --git a/backend/database/database.go b/backend/database/database.go index ee13d9c..19cc360 100644 --- a/backend/database/database.go +++ b/backend/database/database.go @@ -11,6 +11,7 @@ import ( "path" _ "modernc.org/sqlite" // Register sqlite driver. + "yellowjacket/backend/database/sql/sqlcgen" "yellowjacket/backend/system" ) @@ -61,6 +62,7 @@ func NewDB(logger *slog.Logger) (*DB, error) { for _, dirEntry := range dirEntries { if !dirEntry.IsDir() { filePath := path.Join("sql/schemas", dirEntry.Name()) + sqlContent, err := fs.ReadFile(schemas, filePath) if err != nil { return nil, fmt.Errorf("could not read file %s: %w", filePath, err) diff --git a/backend/library/config.go b/backend/library/config.go index 54aaa71..a834d55 100644 --- a/backend/library/config.go +++ b/backend/library/config.go @@ -2,10 +2,13 @@ package library import ( + "errors" "fmt" "os" ) +var errNotDirectory = errors.New("path is not a directory") + // Config holds Library config data. type Config struct { DirectoryPath Directory `form:"Directory" schema:"directory,required"` @@ -35,7 +38,7 @@ func (c *Config) Validate() error { } if !dirInfo.IsDir() { - return fmt.Errorf("%s is not a directory", c.DirectoryPath) + return fmt.Errorf("%s: %w", c.DirectoryPath, errNotDirectory) } } diff --git a/backend/library/library.go b/backend/library/library.go index 92855a5..0d739bd 100644 --- a/backend/library/library.go +++ b/backend/library/library.go @@ -17,12 +17,15 @@ import ( "github.com/wailsapp/wails/v2/pkg/runtime" "golang.org/x/sync/errgroup" + "yellowjacket/backend/database" "yellowjacket/backend/database/sql/sqlcgen" "yellowjacket/backend/events" "yellowjacket/backend/metadata" ) +var errLibraryDirNotConfigured = errors.New("library directory not configured") + // Library manages scanning and querying the music collection. type Library struct { ctx context.Context @@ -107,7 +110,7 @@ func (l *Library) Scan() error { l.logger.Info("beginning library scan", "workers", scanWorkerCount) if len(l.conf.DirectoryPath) == 0 { - return errors.New("library directory not configured") + return errLibraryDirNotConfigured } // Load existing file paths from the database into a sync.Map for concurrent access. diff --git a/backend/library/query.go b/backend/library/query.go index c9069da..ca33335 100644 --- a/backend/library/query.go +++ b/backend/library/query.go @@ -7,6 +7,12 @@ import ( "strconv" ) +// Sentinel errors for library queries. +var ( + errNoTracksInLibrary = errors.New("no tracks in library") + errNoTracksForAlbum = errors.New("no tracks found for album") +) + // Track represents a playable audio file in the library. type Track struct { TrackName string @@ -38,7 +44,7 @@ func (l *Library) GetAllTracks() ([]Track, error) { if len(audioFiles) == 0 { l.logger.Error("no tracks in library") - return nil, errors.New("no tracks in library") + return nil, errNoTracksInLibrary } var formattedTracks []Track @@ -68,7 +74,7 @@ func (l *Library) GetAlbumTracks(albumID int64) ([]Track, error) { } if len(rows) == 0 { - return nil, fmt.Errorf("no tracks found for album %d", albumID) + return nil, fmt.Errorf("%w %d", errNoTracksForAlbum, albumID) } tracks := make([]Track, 0, len(rows)) diff --git a/backend/metadata/metadata.go b/backend/metadata/metadata.go index 93088f2..35e7c55 100644 --- a/backend/metadata/metadata.go +++ b/backend/metadata/metadata.go @@ -39,14 +39,14 @@ func GetTrackLengthMillis(path string) (int64, error) { streamer, format, err := DecodeFile(f) if err != nil { - f.Close() + _ = f.Close() return 0, fmt.Errorf("error decoding file: %w", err) } lengthMillis := int64(float64(streamer.Len()*1000) / float64(format.SampleRate)) - streamer.Close() - f.Close() + _ = streamer.Close() + _ = f.Close() return lengthMillis, nil } diff --git a/backend/metadata/tags.go b/backend/metadata/tags.go index c9cf895..0d01d90 100644 --- a/backend/metadata/tags.go +++ b/backend/metadata/tags.go @@ -51,7 +51,8 @@ func ExtractTags(path string) (*TrackMetadata, error) { if err != nil { return nil, fmt.Errorf("could not open file for tag extraction: %w", err) } - defer f.Close() + + defer func() { _ = f.Close() }() return ExtractTagsFromReader(f) } diff --git a/backend/models/files.go b/backend/models/files.go index b3bc866..19e4ae2 100644 --- a/backend/models/files.go +++ b/backend/models/files.go @@ -5,14 +5,6 @@ import "time" // AudioFileType identifies the format of an audio file. type AudioFileType int -const ( - mp3 AudioFileType = iota - flac - wav - ogg - midi -) - // AudioFile represents a music file with its metadata. type AudioFile struct { Path string diff --git a/backend/player/player.go b/backend/player/player.go index 413387d..d491d11 100644 --- a/backend/player/player.go +++ b/backend/player/player.go @@ -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", diff --git a/backend/player/player_test.go b/backend/player/player_test.go index 7716c71..eb54a9e 100644 --- a/backend/player/player_test.go +++ b/backend/player/player_test.go @@ -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()) } } } diff --git a/backend/player/volume.go b/backend/player/volume.go index 05b19d7..80c271c 100644 --- a/backend/player/volume.go +++ b/backend/player/volume.go @@ -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 diff --git a/backend/queue/queue.go b/backend/queue/queue.go index 58e996a..0ca5af8 100644 --- a/backend/queue/queue.go +++ b/backend/queue/queue.go @@ -38,8 +38,8 @@ type TrackLoader interface { CurrentPositionSeconds() (int, error) } -// QueueTrack represents a track in the queue with its metadata. -type QueueTrack struct { +// Track represents a track in the queue with its metadata. +type Track struct { ID int64 `json:"id"` AudioFileID int64 `json:"audioFileId"` FilePath string `json:"filePath"` @@ -48,13 +48,13 @@ type QueueTrack struct { Artist string `json:"artist"` } -// QueueState is the full state emitted to the frontend. -type QueueState struct { - Tracks []QueueTrack `json:"tracks"` - CurrentIndex int `json:"currentIndex"` - ShuffleMode bool `json:"shuffleMode"` - RepeatMode RepeatMode `json:"repeatMode"` - SourcePlaylistID int64 `json:"sourcePlaylistId"` +// State is the full state emitted to the frontend. +type State struct { + Tracks []Track `json:"tracks"` + CurrentIndex int `json:"currentIndex"` + ShuffleMode bool `json:"shuffleMode"` + RepeatMode RepeatMode `json:"repeatMode"` + SourcePlaylistID int64 `json:"sourcePlaylistId"` } // Queue manages an ordered list of tracks for playback. @@ -65,7 +65,7 @@ type Queue struct { player TrackLoader mu sync.Mutex - tracks []QueueTrack + tracks []Track currentIndex int shuffleMode bool repeatMode RepeatMode @@ -331,7 +331,7 @@ func (q *Queue) SetQueue(filePaths []string, startIndex int) { defer q.mu.Unlock() // Look up audio file IDs and metadata for all paths. - tracks := make([]QueueTrack, 0, len(filePaths)) + tracks := make([]Track, 0, len(filePaths)) for i, fp := range filePaths { af, err := q.db.Queries.GetAudioFileByPath(q.db.Ctx, fp) @@ -341,7 +341,7 @@ func (q *Queue) SetQueue(filePaths []string, startIndex int) { continue } - track := QueueTrack{ + track := Track{ AudioFileID: af.ID, FilePath: fp, Position: int64(i), @@ -395,7 +395,7 @@ func (q *Queue) AddTrack(filePath string) { wasEmpty := len(q.tracks) == 0 - track := QueueTrack{ + track := Track{ AudioFileID: af.ID, FilePath: filePath, Position: int64(len(q.tracks)), @@ -449,7 +449,7 @@ func (q *Queue) AddTracks(filePaths []string) { continue } - track := QueueTrack{ + track := Track{ AudioFileID: af.ID, FilePath: fp, Position: int64(len(q.tracks)), @@ -490,7 +490,8 @@ func (q *Queue) InsertNextTracks(filePaths []string) { } wasEmpty := len(q.tracks) == 0 - var newTracks []QueueTrack + + var newTracks []Track for _, fp := range filePaths { af, err := q.db.Queries.GetAudioFileByPath(q.db.Ctx, fp) @@ -500,7 +501,7 @@ func (q *Queue) InsertNextTracks(filePaths []string) { continue } - track := QueueTrack{ + track := Track{ AudioFileID: af.ID, FilePath: fp, } @@ -519,7 +520,7 @@ func (q *Queue) InsertNextTracks(filePaths []string) { } // Insert the block into the slice at insertPos. - tail := make([]QueueTrack, len(q.tracks[insertPos:])) + tail := make([]Track, len(q.tracks[insertPos:])) copy(tail, q.tracks[insertPos:]) q.tracks = append(q.tracks[:insertPos], newTracks...) q.tracks = append(q.tracks, tail...) @@ -558,7 +559,7 @@ func (q *Queue) InsertNext(filePath string) { insertPos = len(q.tracks) } - track := QueueTrack{ + track := Track{ AudioFileID: af.ID, FilePath: filePath, Position: int64(insertPos), @@ -572,7 +573,7 @@ func (q *Queue) InsertNext(filePath string) { } // Insert into slice. - q.tracks = append(q.tracks, QueueTrack{}) + q.tracks = append(q.tracks, Track{}) copy(q.tracks[insertPos+1:], q.tracks[insertPos:]) q.tracks[insertPos] = track @@ -710,14 +711,14 @@ func (q *Queue) CycleRepeat() { } // GetState returns the current queue state for the frontend. -func (q *Queue) GetState() QueueState { +func (q *Queue) GetState() State { q.mu.Lock() defer q.mu.Unlock() - tracks := make([]QueueTrack, len(q.tracks)) + tracks := make([]Track, len(q.tracks)) copy(tracks, q.tracks) - return QueueState{ + return State{ Tracks: tracks, CurrentIndex: q.currentIndex, ShuffleMode: q.shuffleMode, @@ -790,10 +791,10 @@ func (q *Queue) RestoreState() { return } - q.tracks = make([]QueueTrack, 0, len(rows)) + q.tracks = make([]Track, 0, len(rows)) for _, row := range rows { - q.tracks = append(q.tracks, QueueTrack{ + q.tracks = append(q.tracks, Track{ ID: row.ID, AudioFileID: row.AudioFileID, FilePath: row.FilePath, @@ -954,13 +955,25 @@ func (q *Queue) playCurrentTrack() { } if q.currentIndex < 0 || q.currentIndex >= len(q.tracks) { - q.logger.Warn("Current index out of range", "index", q.currentIndex, "trackCount", len(q.tracks)) + q.logger.Warn( + "Current index out of range", + "index", + q.currentIndex, + "trackCount", + len(q.tracks), + ) return } track := q.tracks[q.currentIndex] - q.logger.Info("Playing track from queue", "filePath", track.FilePath, "position", q.currentIndex) + q.logger.Info( + "Playing track from queue", + "filePath", + track.FilePath, + "position", + q.currentIndex, + ) err := q.player.LoadFile(track.FilePath) if err != nil { @@ -980,8 +993,8 @@ func (q *Queue) playCurrentTrack() { // onQueueExhausted is called when there are no more tracks to play. // This is the extension point for a future fallback playlist feature. func (q *Queue) onQueueExhausted() { - q.logger.Info("Queue exhausted, stopping playback") // Future: load fallback playlist here. + q.logger.Info("Queue exhausted, stopping playback") } // reindexPositions updates the Position field of all tracks to match slice index. @@ -1047,7 +1060,7 @@ func (q *Queue) emitQueueChanged() { return } - state := QueueState{ + state := State{ Tracks: q.tracks, CurrentIndex: q.currentIndex, ShuffleMode: q.shuffleMode, @@ -1057,7 +1070,7 @@ func (q *Queue) emitQueueChanged() { // Ensure tracks is never nil in JSON. if state.Tracks == nil { - state.Tracks = []QueueTrack{} + state.Tracks = []Track{} } runtime.EventsEmit(q.ctx, events.QueueChanged, state)