diff --git a/.golangci.yml b/.golangci.yml index a78482c..68516b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,7 +28,7 @@ linters: - usestdlibvars - usetesting - whitespace - - wsl + - wsl_v5 formatters: enable: - gci diff --git a/backend/events/cmd/genevents/main.go b/backend/events/cmd/genevents/main.go index 7bea094..d83467a 100644 --- a/backend/events/cmd/genevents/main.go +++ b/backend/events/cmd/genevents/main.go @@ -20,6 +20,7 @@ import ( func main() { source := flag.String("source", "events.go", "path to Go events source file") output := flag.String("output", "", "path to TypeScript output file (stdout if empty)") + flag.Parse() consts, err := parseEvents(*source) diff --git a/backend/fileutil/atomicwrite_test.go b/backend/fileutil/atomicwrite_test.go index 2196f5b..2571263 100644 --- a/backend/fileutil/atomicwrite_test.go +++ b/backend/fileutil/atomicwrite_test.go @@ -261,6 +261,7 @@ func TestAtomicWrite_SyncAndClose(t *testing.T) { // Write 1 MiB of data. const size = 1 << 20 //nolint:mnd + data := bytes.Repeat([]byte("x"), size) err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { diff --git a/backend/library/library.go b/backend/library/library.go index 74f8ce5..ca85bd5 100644 --- a/backend/library/library.go +++ b/backend/library/library.go @@ -420,7 +420,6 @@ func (l *Library) scanInternal( return nil }, ) - if walkErr != nil { metrics.addWarning( "", "walk", diff --git a/backend/player/buffered_streamer_test.go b/backend/player/buffered_streamer_test.go index 2eb4074..3d90528 100644 --- a/backend/player/buffered_streamer_test.go +++ b/backend/player/buffered_streamer_test.go @@ -54,6 +54,7 @@ func finiteStreamer(n int) beep.Streamer { func TestBufferedStreamer_BasicStream(t *testing.T) { const total = 1000 + src := finiteStreamer(total) bs := NewBufferedStreamer(src, 2048) @@ -120,6 +121,7 @@ func TestBufferedStreamer_BasicStream(t *testing.T) { func TestBufferedStreamer_SmallReads(t *testing.T) { const total = 200 + src := finiteStreamer(total) bs := NewBufferedStreamer(src, 512) @@ -185,6 +187,7 @@ func TestBufferedStreamer_SmallReads(t *testing.T) { func TestBufferedStreamer_SourceDrained(t *testing.T) { const total = 100 + src := finiteStreamer(total) bs := NewBufferedStreamer(src, 256) diff --git a/backend/player/player.go b/backend/player/player.go index 696a2e5..31426c9 100644 --- a/backend/player/player.go +++ b/backend/player/player.go @@ -802,7 +802,6 @@ func (p *Player) seekLocked(targetSeconds int) error { return p.seeker.Seek(samples) }() - if seekErr != nil { speaker.Unlock() diff --git a/backend/tagwriter/dbsync.go b/backend/tagwriter/dbsync.go index 013a124..b240d75 100644 --- a/backend/tagwriter/dbsync.go +++ b/backend/tagwriter/dbsync.go @@ -514,6 +514,7 @@ func generateSizedVariants(logger *slog.Logger, imgData []byte, dir, hashStr str src, _, err := image.Decode(bytes.NewReader(imgData)) if err != nil { logger.Warn("could not decode image for thumbnails", "err", err) + return } @@ -537,6 +538,7 @@ func generateSizedVariants(logger *slog.Logger, imgData []byte, dir, hashStr str var buf bytes.Buffer if encErr := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: tier.quality}); encErr != nil { logger.Warn("could not encode thumbnail", "tier", tier.suffix, "err", encErr) + continue } diff --git a/backend/tagwriter/mp3_test.go b/backend/tagwriter/mp3_test.go index 29ccd91..1817ccb 100644 --- a/backend/tagwriter/mp3_test.go +++ b/backend/tagwriter/mp3_test.go @@ -227,10 +227,10 @@ func TestWriteMp3Tags_AtomicSafety(t *testing.T) { // Attempt to write to a non-existent directory to force AtomicWrite // to fail (the temp file creation will fail). badPath := filepath.Join(dir, "nonexistent", "subdir", "file.mp3") + writeErr := writeMp3Tags(testLogger(), badPath, TagChanges{ FieldTitle: "After", }) - if writeErr == nil { t.Fatal("expected error for non-existent path, got nil") } diff --git a/backend/tagwriter/ogg.go b/backend/tagwriter/ogg.go index 2d57571..92cf02e 100644 --- a/backend/tagwriter/ogg.go +++ b/backend/tagwriter/ogg.go @@ -14,14 +14,14 @@ import ( // Sentinel errors for OGG operations. var ( - errNotOgg = errors.New("not an OGG file") - errNotVorbis = errors.New("not an OGG Vorbis file") - errOggMultiStream = errors.New("this OGG file contains multiple streams and cannot be edited") - errOggTruncated = errors.New("OGG file is truncated") - errOggInvalidPage = errors.New("invalid OGG page") - errOggBadVersion = errors.New("unsupported OGG version") - errOggNoPages = errors.New("OGG file contains no pages") - errOggMissingSetup = errors.New("OGG file is missing the setup header packet") + errNotOgg = errors.New("not an OGG file") + errNotVorbis = errors.New("not an OGG Vorbis file") + errOggMultiStream = errors.New("this OGG file contains multiple streams and cannot be edited") + errOggTruncated = errors.New("OGG file is truncated") + errOggBadVersion = errors.New("unsupported OGG version") + errOggTooFewPackets = errors.New("ogg vorbis: expected at least 3 header packets") + errOggNoPages = errors.New("OGG file contains no pages") + errOggMissingSetup = errors.New("OGG file is missing the setup header packet") ) // oggCRCTable is the pre-computed 256-entry CRC32 lookup table for OGG. @@ -117,7 +117,11 @@ func parseOggPages(logger *slog.Logger, filePath string) ([]oggPage, error) { segEnd := segStart + numSegments if segEnd > len(raw) { - return nil, fmt.Errorf("%w: segment table truncated at offset %d", errOggTruncated, offset) + return nil, fmt.Errorf( + "%w: segment table truncated at offset %d", + errOggTruncated, + offset, + ) } segmentTable := make([]byte, numSegments) @@ -192,7 +196,8 @@ func parseOggPages(logger *slog.Logger, filePath string) ([]oggPage, error) { // Validate: first page is Vorbis identification header. firstPacketData := pages[0].data - if len(firstPacketData) < 7 || firstPacketData[0] != 0x01 || //nolint:mnd // Vorbis ID header magic + if len(firstPacketData) < 7 || + firstPacketData[0] != 0x01 || //nolint:mnd // Vorbis ID header magic string(firstPacketData[1:7]) != "vorbis" { return nil, errNotVorbis } @@ -249,6 +254,7 @@ func writeOggPage(w io.Writer, page oggPage) error { // a value <255 terminates it. func extractPackets(pages []oggPage) [][]byte { var packets [][]byte + var current []byte for _, page := range pages { @@ -299,7 +305,12 @@ func splitPacketIntoSegments(packet []byte) []byte { // respecting the 255-segment-per-page limit. The first page gets the // headerType as-is; continuation pages get the continued flag (0x01) // ORed in. All pages share the same serialNo and granulePos. -func buildPagesFromSegments(segments []byte, packetData []byte, serialNo uint32, granulePos int64) []oggPage { +func buildPagesFromSegments( + segments []byte, + packetData []byte, + serialNo uint32, + granulePos int64, +) []oggPage { const maxSegmentsPerPage = 255 var pages []oggPage @@ -400,7 +411,7 @@ func writeOggTags(logger *slog.Logger, filePath string, changes TagChanges) erro const minHeaderPackets = 3 if len(packets) < minHeaderPackets { - return fmt.Errorf("ogg vorbis: expected at least 3 header packets, got %d", len(packets)) + return fmt.Errorf("%w: got %d", errOggTooFewPackets, len(packets)) } _ = packets[0] // identification packet — preserved as page 0 unchanged @@ -503,72 +514,10 @@ func findAudioPageStart(pages []oggPage) int { return len(pages) } -// serializePacketsToPages is a helper that builds pages from multiple -// packets concatenated into the same page stream. This is used for -// the comment + setup header packets which share pages per Vorbis spec. -func serializePacketsToPages(packets [][]byte, serialNo uint32, granulePos int64) []oggPage { - var allSegs []byte - var allData []byte - - for _, pkt := range packets { - segs := splitPacketIntoSegments(pkt) - allSegs = append(allSegs, segs...) - allData = append(allData, pkt...) - } - - return buildPagesFromSegments(allSegs, allData, serialNo, granulePos) -} - -// computePacketBoundaryInPages determines how many complete packets -// exist starting from the beginning of a sequence of pages, using -// lacing values. It returns the number of complete packets and -// whether the reading ended mid-packet (a continued packet). -func computePacketBoundaryInPages(pages []oggPage) (completedPackets int, midPacket bool) { - inPacket := false - - for _, page := range pages { - for _, lacing := range page.segmentTable { - if lacing > 0 { - inPacket = true - } - - if lacing < 255 { //nolint:mnd // OGG lacing terminates packet - if inPacket || lacing == 0 { - completedPackets++ - inPacket = false - } - } - } - } - - return completedPackets, inPacket -} - -// oggPagesCopy creates a deep copy of a slice of oggPage. -func oggPagesCopy(pages []oggPage) []oggPage { - cp := make([]oggPage, len(pages)) - - for i, p := range pages { - cp[i] = oggPage{ - headerType: p.headerType, - granulePos: p.granulePos, - serialNo: p.serialNo, - seqNo: p.seqNo, - } - - cp[i].segmentTable = make([]byte, len(p.segmentTable)) - copy(cp[i].segmentTable, p.segmentTable) - - cp[i].data = make([]byte, len(p.data)) - copy(cp[i].data, p.data) - } - - return cp -} - // pageBytes serializes an OGG page to raw bytes (for CRC verification etc.). func pageBytes(page oggPage) []byte { var buf bytes.Buffer + _ = writeOggPage(&buf, page) return buf.Bytes() diff --git a/backend/tagwriter/ogg_test.go b/backend/tagwriter/ogg_test.go index 6990083..72309c5 100644 --- a/backend/tagwriter/ogg_test.go +++ b/backend/tagwriter/ogg_test.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "os" "path/filepath" + "strings" "testing" "yellowjacket/backend/metadata" @@ -601,6 +602,7 @@ func TestWriteOggTags_RejectMultiStream(t *testing.T) { identSegs := splitPacketIntoSegments(identPacket) const serial1 uint32 = 0x11111111 + const serial2 uint32 = 0x22222222 page1 := oggPage{ @@ -652,8 +654,5 @@ func errorContains(err error, substr string) bool { return false } - return bytes.Contains( - []byte(err.Error()), - []byte(substr), - ) + return strings.Contains(err.Error(), substr) } diff --git a/backend/tagwriter/ogg_vorbis.go b/backend/tagwriter/ogg_vorbis.go index eb27211..44d8d86 100644 --- a/backend/tagwriter/ogg_vorbis.go +++ b/backend/tagwriter/ogg_vorbis.go @@ -4,11 +4,18 @@ import ( "bytes" "encoding/base64" "encoding/binary" + "errors" "fmt" "strconv" "strings" ) +// Sentinel errors for Vorbis Comment operations. +var ( + errVorbisPacketShort = errors.New("vorbis comment packet too short") + errVorbisInvalidMagic = errors.New("invalid vorbis comment header magic") +) + // oggVorbisComment holds a parsed Vorbis Comment structure with raw // byte preservation for non-edited fields (even if they contain // invalid UTF-8). @@ -24,11 +31,12 @@ func parseVorbisCommentPacket(packet []byte) (*oggVorbisComment, error) { const prefixLen = 7 // \x03 + "vorbis" if len(packet) < prefixLen { - return nil, fmt.Errorf("vorbis comment packet too short: %d bytes", len(packet)) + return nil, fmt.Errorf("%w: %d bytes", errVorbisPacketShort, len(packet)) } - if packet[0] != 0x03 || string(packet[1:7]) != "vorbis" { //nolint:mnd // Vorbis comment header magic - return nil, fmt.Errorf("invalid vorbis comment header magic") + if packet[0] != 0x03 || + string(packet[1:7]) != "vorbis" { //nolint:mnd // Vorbis comment header magic + return nil, errVorbisInvalidMagic } r := bytes.NewReader(packet[prefixLen:]) @@ -200,7 +208,13 @@ func buildMetadataBlockPicture(imageData []byte) []byte { // Calculate size: type(4) + mimeLen(4) + mime + descLen(4) + desc // + width(4) + height(4) + depth(4) + colors(4) + dataLen(4) + data - size := 4 + 4 + len(mime) + 4 + len(desc) + 4*4 + 4 + len(imageData) //nolint:mnd // FLAC PICTURE block fields + size := 4 + 4 + len( + mime, + ) + 4 + len( + desc, + ) + 4*4 + 4 + len( + imageData, + ) //nolint:mnd // FLAC PICTURE block fields buf := make([]byte, 0, size) diff --git a/backend/tagwriter/pipeline.go b/backend/tagwriter/pipeline.go index b5a7185..6ccec26 100644 --- a/backend/tagwriter/pipeline.go +++ b/backend/tagwriter/pipeline.go @@ -244,11 +244,13 @@ func (tw *TagWriter) BatchWriteTrackTags( // Set up cancellation channel. tw.cancelBatch = make(chan struct{}) + defer func() { tw.cancelBatch = nil }() // Suppress per-track TrackMetadataChanged events — we emit one // at the end instead. tw.suppressEvents = true + defer func() { tw.suppressEvents = false }() for i, filePath := range filePaths { @@ -256,12 +258,11 @@ func (tw *TagWriter) BatchWriteTrackTags( select { case <-tw.cancelBatch: result.Cancelled = true + tw.logger.Info("batch write cancelled", "at", i, "total", total, ) - - break default: } @@ -276,6 +277,7 @@ func (tw *TagWriter) BatchWriteTrackTags( FilePath: filePath, Error: err.Error(), }) + tw.logger.Warn("batch track failed", "path", filePath, "err", err, diff --git a/backend/tagwriter/tagwriter.go b/backend/tagwriter/tagwriter.go index 2e08ce1..433adcf 100644 --- a/backend/tagwriter/tagwriter.go +++ b/backend/tagwriter/tagwriter.go @@ -99,6 +99,7 @@ func asBytes(v any) ([]byte, bool) { } out := make([]byte, len(arr)) + for i, elem := range arr { f, ok := elem.(float64) if !ok { diff --git a/backend/tagwriter/wav_test.go b/backend/tagwriter/wav_test.go index 7c28ed6..3b54280 100644 --- a/backend/tagwriter/wav_test.go +++ b/backend/tagwriter/wav_test.go @@ -32,6 +32,7 @@ func createTestWAV( // --- data chunk: 200 bytes of silence --- const silenceLen = 200 //nolint:mnd + silence := make([]byte, silenceLen) // --- optional id3 chunk --- @@ -401,7 +402,6 @@ func TestWriteWavTags_AtomicSafety(t *testing.T) { writeErr := writeWavTags(testLogger(), badPath, TagChanges{ FieldTitle: "After", }) - if writeErr == nil { t.Fatal("expected error for non-existent path, got nil") } @@ -443,7 +443,6 @@ func TestWriteWavTags_RejectsRF64(t *testing.T) { err := writeWavTags(testLogger(), path, TagChanges{ FieldTitle: "Should Fail", }) - if err == nil { t.Fatal("expected error for RF64 file, got nil") } @@ -601,6 +600,7 @@ func createTestWAVWithExtraChunks( // 200 bytes of silence for data chunk. const silenceLen = 200 //nolint:mnd + silence := make([]byte, silenceLen) // Calculate RIFF payload.