diff --git a/.golangci.yml b/.golangci.yml index 35faffd..6e4119b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,28 +6,72 @@ run: linters: disable-all: true enable: + - asasalint + - asciicheck + - bidichk - bodyclose + - containedctx + - decorder - dogsled + - dupword + - durationcheck - errcheck + - errchkjson + - errname + - errorlint + - execinquery - exportloopref + - forbidigo + - ginkgolinter + - gocheckcompilerdirectives - gochecknoglobals - gochecknoinits - goconst - gocritic - gocyclo + - gofmt + - gofumpt + - goheader + - goimports + - gomoddirectives + - gomodguard - goprintffuncname - gosec - gosimple + - gosmopolitan - govet + - grouper + - importas - ineffassign + - loggercheck + - makezero + - mirror - misspell + - musttag - nakedret + - nestif + - nilerr + - nosprintfhostport + - paralleltest + - predeclared + - promlinter + - reassign - revive - rowserrcheck + - sqlclosecheck - staticcheck - stylecheck + - tenv + - testableexamples + - thelper + - tparallel - typecheck - unconvert + - unparam + - unused + - wastedassign + - whitespace + - zerologlint issues: exclude-rules: diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index 60be307..6ee3e39 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -1,6 +1,4 @@ -// Package main is the gonic server entrypoint -// -//nolint:lll,gocyclo +//nolint:lll,gocyclo,forbidigo package main import ( @@ -370,8 +368,10 @@ func main() { const pathAliasSep = "->" -type pathAliases []pathAlias -type pathAlias struct{ alias, path string } +type ( + pathAliases []pathAlias + pathAlias struct{ alias, path string } +) func (pa pathAliases) String() string { var strs []string diff --git a/db/db_test.go b/db/db_test.go index d28e00c..9b353e5 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -21,6 +21,8 @@ func randKey() string { } func TestGetSetting(t *testing.T) { + t.Parallel() + key := SettingKey(randKey()) value := "howdy" diff --git a/db/model.go b/db/model.go index ade2c5f..43e7481 100644 --- a/db/model.go +++ b/db/model.go @@ -1,5 +1,3 @@ -// Package db provides database helpers and models -// //nolint:lll // struct tags get very long and can't be split package db @@ -238,7 +236,7 @@ func (a *Album) GenreStrings() []string { } func (a *Album) ArtistsStrings() []string { - var artists = append([]*Artist(nil), a.Artists...) + artists := append([]*Artist(nil), a.Artists...) sort.Slice(artists, func(i, j int) bool { return artists[i].ID < artists[j].ID }) diff --git a/fileutil/fileutil_test.go b/fileutil/fileutil_test.go index 88c552c..a8cb480 100644 --- a/fileutil/fileutil_test.go +++ b/fileutil/fileutil_test.go @@ -9,6 +9,8 @@ import ( ) func TestUniquePath(t *testing.T) { + t.Parallel() + unq := func(base, filename string, count uint) string { r, err := unique(base, filename, count) require.NoError(t, err) @@ -40,6 +42,8 @@ func TestUniquePath(t *testing.T) { } func TestFirst(t *testing.T) { + t.Parallel() + base := t.TempDir() name := filepath.Join(base, "test") _, err := os.Create(name) @@ -52,5 +56,4 @@ func TestFirst(t *testing.T) { r, err := First(p("one"), p("two"), p("test"), p("four")) require.NoError(t, err) require.Equal(t, p("test"), r) - } diff --git a/jukebox/jukebox.go b/jukebox/jukebox.go index 3a04aa0..c29cfdc 100644 --- a/jukebox/jukebox.go +++ b/jukebox/jukebox.go @@ -331,13 +331,15 @@ func (j *Jukebox) getDecode(dest any, property string) error { return nil } -type mpvPlaylist []mpvPlaylistItem -type mpvPlaylistItem struct { - ID int - Filename string - Current bool - Playing bool -} +type ( + mpvPlaylist []mpvPlaylistItem + mpvPlaylistItem struct { + ID int + Filename string + Current bool + Playing bool + } +) func waitUntil(timeout time.Duration, f func() bool) bool { quit := time.NewTicker(timeout) diff --git a/jukebox/jukebox_test.go b/jukebox/jukebox_test.go index c320d3e..f1c0f01 100644 --- a/jukebox/jukebox_test.go +++ b/jukebox/jukebox_test.go @@ -11,8 +11,10 @@ import ( "go.senan.xyz/gonic/jukebox" ) -func newJukebox(t *testing.T) *jukebox.Jukebox { - sockPath := filepath.Join(t.TempDir(), "mpv.sock") +func newJukebox(tb testing.TB) *jukebox.Jukebox { + tb.Helper() + + sockPath := filepath.Join(tb.TempDir(), "mpv.sock") j := jukebox.New() err := j.Start( @@ -20,12 +22,12 @@ func newJukebox(t *testing.T) *jukebox.Jukebox { []string{jukebox.MPVArg("--ao", "null")}, ) if errors.Is(err, jukebox.ErrMPVTooOld) { - t.Skip("old mpv found, skipping") + tb.Skip("old mpv found, skipping") } if err != nil { - t.Fatalf("start jukebox: %v", err) + tb.Fatalf("start jukebox: %v", err) } - t.Cleanup(func() { + tb.Cleanup(func() { j.Quit() }) return j diff --git a/mime/mime.go b/mime/mime.go index 04ffe56..8a9b533 100644 --- a/mime/mime.go +++ b/mime/mime.go @@ -28,9 +28,11 @@ func init() { } } -var TypeByExtension = stdmime.TypeByExtension -var ParseMediaType = stdmime.ParseMediaType -var FormatMediaType = stdmime.FormatMediaType +var ( + TypeByExtension = stdmime.TypeByExtension + ParseMediaType = stdmime.ParseMediaType + FormatMediaType = stdmime.FormatMediaType +) func TypeByAudioExtension(ext string) string { if _, ok := supportedAudioTypes[strings.ToLower(ext)]; !ok { diff --git a/mockfs/mockfs.go b/mockfs/mockfs.go index d1711d8..de9383e 100644 --- a/mockfs/mockfs.go +++ b/mockfs/mockfs.go @@ -1,3 +1,4 @@ +//nolint:thelper package mockfs import ( @@ -27,29 +28,31 @@ type MockFS struct { db *db.DB } -func New(t testing.TB) *MockFS { return newMockFS(t, []string{""}, "") } -func NewWithDirs(t testing.TB, dirs []string) *MockFS { return newMockFS(t, dirs, "") } -func NewWithExcludePattern(t testing.TB, excludePattern string) *MockFS { - return newMockFS(t, []string{""}, excludePattern) +func New(tb testing.TB) *MockFS { return newMockFS(tb, []string{""}, "") } +func NewWithDirs(tb testing.TB, dirs []string) *MockFS { return newMockFS(tb, dirs, "") } +func NewWithExcludePattern(tb testing.TB, excludePattern string) *MockFS { + return newMockFS(tb, []string{""}, excludePattern) } -func newMockFS(t testing.TB, dirs []string, excludePattern string) *MockFS { +func newMockFS(tb testing.TB, dirs []string, excludePattern string) *MockFS { + tb.Helper() + dbc, err := db.NewMock() if err != nil { - t.Fatalf("create db: %v", err) + tb.Fatalf("create db: %v", err) } - t.Cleanup(func() { + tb.Cleanup(func() { if err := dbc.Close(); err != nil { - t.Fatalf("close db: %v", err) + tb.Fatalf("close db: %v", err) } }) if err := dbc.Migrate(db.MigrationContext{}); err != nil { - t.Fatalf("migrate db db: %v", err) + tb.Fatalf("migrate db db: %v", err) } dbc.LogMode(false) - tmpDir := t.TempDir() + tmpDir := tb.TempDir() var absDirs []string for _, dir := range dirs { @@ -57,7 +60,7 @@ func newMockFS(t testing.TB, dirs []string, excludePattern string) *MockFS { } for _, absDir := range absDirs { if err := os.MkdirAll(absDir, os.ModePerm); err != nil { - t.Fatalf("mk abs dir: %v", err) + tb.Fatalf("mk abs dir: %v", err) } } @@ -70,7 +73,7 @@ func newMockFS(t testing.TB, dirs []string, excludePattern string) *MockFS { scanner := scanner.New(absDirs, dbc, multiValueSettings, tagReader, excludePattern) return &MockFS{ - t: t, + t: tb, scanner: scanner, dir: tmpDir, tagReader: tagReader, @@ -399,15 +402,6 @@ func (m *Tags) Bitrate() int { return firstInt(100, m.RawBitrate) } var _ tags.Parser = (*Tags)(nil) -func first(or string, strs ...string) string { - for _, str := range strs { - if str != "" { - return str - } - } - return or -} - func firstInt(or int, ints ...int) int { for _, int := range ints { if int > 0 { diff --git a/playlist/playlist.go b/playlist/playlist.go index bb8dd91..618af33 100644 --- a/playlist/playlist.go +++ b/playlist/playlist.go @@ -14,9 +14,11 @@ import ( "time" ) -var ErrInvalidPathFormat = errors.New("invalid path format") -var ErrInvalidBasePath = errors.New("invalid base path") -var ErrNoUserPrefix = errors.New("no user prefix") +var ( + ErrInvalidPathFormat = errors.New("invalid path format") + ErrInvalidBasePath = errors.New("invalid base path") + ErrNoUserPrefix = errors.New("no user prefix") +) const ( extM3U = ".m3u" @@ -31,7 +33,6 @@ type Store struct { func NewStore(basePath string) (*Store, error) { if basePath == "" { return nil, ErrInvalidBasePath - } // sanity check layout, just in case someone tries to use an existing folder @@ -108,6 +109,7 @@ const ( func encodeAttr(name, value string) string { return fmt.Sprintf("%s%s:%s", attrPrefix, name, strconv.Quote(value)) } + func decodeAttr(line string) (name, value string) { if !strings.HasPrefix(line, attrPrefix) { return "", "" @@ -169,10 +171,10 @@ func (s *Store) Write(relPath string, playlist *Playlist) error { defer lock(&s.mu)() absPath := filepath.Join(s.basePath, relPath) - if err := os.MkdirAll(filepath.Dir(absPath), 0777); err != nil { + if err := os.MkdirAll(filepath.Dir(absPath), 0o777); err != nil { return fmt.Errorf("make m3u base dir: %w", err) } - file, err := os.OpenFile(absPath, os.O_RDWR|os.O_CREATE, 0666) + file, err := os.OpenFile(absPath, os.O_RDWR|os.O_CREATE, 0o666) if err != nil { return fmt.Errorf("create m3u: %w", err) } diff --git a/playlist/playlist_test.go b/playlist/playlist_test.go index 52c1387..1d48dc5 100644 --- a/playlist/playlist_test.go +++ b/playlist/playlist_test.go @@ -8,6 +8,8 @@ import ( ) func TestPlaylist(t *testing.T) { + t.Parallel() + require := require.New(t) tmp := t.TempDir() diff --git a/podcasts/podcasts.go b/podcasts/podcasts.go index 5fc02c9..e0c872f 100644 --- a/podcasts/podcasts.go +++ b/podcasts/podcasts.go @@ -25,8 +25,10 @@ import ( var ErrNoAudioInFeedItem = errors.New("no audio in feed item") -const downloadAllWaitInterval = 3 * time.Second -const fetchUserAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11` +const ( + downloadAllWaitInterval = 3 * time.Second + fetchUserAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11` +) type Podcasts struct { db *db.DB @@ -96,7 +98,6 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed) (*db.Podcast, rootDir, err := fileutil.Unique(filepath.Join(p.baseDir, fileutil.Safe(feed.Title)), "") if err != nil { return nil, fmt.Errorf("find unique podcast dir: %w", err) - } podcast := db.Podcast{ Description: feed.Description, @@ -105,7 +106,7 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed) (*db.Podcast, URL: rssURL, RootDir: rootDir, } - if err := os.Mkdir(podcast.RootDir, 0755); err != nil && !os.IsExist(err) { + if err := os.Mkdir(podcast.RootDir, 0o755); err != nil && !os.IsExist(err) { return nil, err } if err := p.db.Save(&podcast).Error; err != nil { @@ -248,7 +249,8 @@ func isAudio(rawItemURL string) (bool, error) { } func itemToEpisode(podcastID, size, duration int, audio string, - item *gofeed.Item) *db.PodcastEpisode { + item *gofeed.Item, +) *db.PodcastEpisode { return &db.PodcastEpisode{ PodcastID: podcastID, Description: item.Description, diff --git a/podcasts/podcasts_test.go b/podcasts/podcasts_test.go index dc4fc57..4042850 100644 --- a/podcasts/podcasts_test.go +++ b/podcasts/podcasts_test.go @@ -17,6 +17,8 @@ import ( var testRSS []byte func TestPodcastsAndEpisodesWithSameName(t *testing.T) { + t.Parallel() + t.Skip("requires network access") m := mockfs.New(t) @@ -62,6 +64,8 @@ func TestPodcastsAndEpisodesWithSameName(t *testing.T) { } func TestGetMoreRecentEpisodes(t *testing.T) { + t.Parallel() + fp := gofeed.NewParser() newFeed, err := fp.Parse(bytes.NewReader(testRSS)) if err != nil { diff --git a/scanner/scanner.go b/scanner/scanner.go index 9ac1914..d532231 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -61,9 +61,11 @@ func New(musicDirs []string, db *db.DB, multiValueSettings map[Tag]MultiValueSet func (s *Scanner) IsScanning() bool { return atomic.LoadInt32(s.scanning) == 1 } + func (s *Scanner) StartScanning() bool { return atomic.CompareAndSwapInt32(s.scanning, 0, 1) } + func (s *Scanner) StopScanning() { defer atomic.StoreInt32(s.scanning, 0) } @@ -173,7 +175,6 @@ func (s *Scanner) ExecuteWatch() error { if err != nil { log.Printf("error walking: %v", err) } - } scanList = map[string]struct{}{} s.StopScanning() @@ -343,7 +344,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, alb trags, err := s.tagger.Read(absPath) if err != nil { - return fmt.Errorf("%v: %w", err, ErrReadingTags) + return fmt.Errorf("%w: %w", err, ErrReadingTags) } genreNames := parseMulti(trags, s.multiValueSettings[Genre], tags.MustGenres, tags.MustGenre) @@ -352,7 +353,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, alb return fmt.Errorf("populate genres: %w", err) } - // metadata for the album table comes only from the the first track's tags + // metadata for the album table comes only from the first track's tags if i == 0 { albumArtistNames := parseMulti(trags, s.multiValueSettings[AlbumArtist], tags.MustAlbumArtists, tags.MustAlbumArtist) var albumArtistIDs []int @@ -510,7 +511,7 @@ func populateAlbumGenres(tx *db.DB, album *db.Album, genreIDs []int) error { func populateAlbumArtists(tx *db.DB, album *db.Album, albumArtistIDs []int) error { if err := tx.Where("album_id=?", album.ID).Delete(db.AlbumArtist{}).Error; err != nil { - return fmt.Errorf("delete old album album artists: %w", err) + return fmt.Errorf("delete old album artists: %w", err) } if err := tx.InsertBulkLeftMany("album_artists", []string{"album_id", "artist_id"}, album.ID, albumArtistIDs); err != nil { @@ -583,7 +584,7 @@ func (s *Scanner) cleanArtists(c *Context) error { return nil } -func (s *Scanner) cleanGenres(c *Context) error { +func (s *Scanner) cleanGenres(c *Context) error { //nolint:unparam start := time.Now() defer func() { log.Printf("finished clean genres in %s, %d removed", durSince(start), c.GenresMissing()) }() diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index eb0aae2..032dbae 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -1,4 +1,4 @@ -//nolint:goconst +//nolint:goconst,errorlint package scanner_test import ( @@ -724,7 +724,6 @@ func TestMultiArtistSupport(t *testing.T) { }, state(), ) - } func TestMultiArtistPreload(t *testing.T) { diff --git a/scanner/tags/tags.go b/scanner/tags/tags.go index bfd0b3a..660b6a5 100644 --- a/scanner/tags/tags.go +++ b/scanner/tags/tags.go @@ -34,9 +34,11 @@ func (t *Tagger) Genres() []string { return find(t.raw, "genres") } func (t *Tagger) TrackNumber() int { return intSep("/" /* eg. 5/12 */, first(find(t.raw, "tracknumber"))) } + func (t *Tagger) DiscNumber() int { return intSep("/" /* eg. 1/2 */, first(find(t.raw, "discnumber"))) } + func (t *Tagger) Year() int { return intSep("-" /* 2023-12-01 */, first(find(t.raw, "originaldate", "date", "year"))) } @@ -65,15 +67,6 @@ type Parser interface { Year() int } -func fallback(or string, strs ...string) string { - for _, str := range strs { - if str != "" { - return str - } - } - return or -} - func first[T comparable](is []T) T { var z T for _, i := range is { diff --git a/scrobble/lastfm/client_test.go b/scrobble/lastfm/client_test.go index 58df4c7..8a51a8f 100644 --- a/scrobble/lastfm/client_test.go +++ b/scrobble/lastfm/client_test.go @@ -13,6 +13,8 @@ import ( ) func TestArtistGetInfo(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -93,6 +95,8 @@ func TestArtistGetInfo(t *testing.T) { } func TestArtistGetInfoClientRequestFails(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -117,6 +121,8 @@ func TestArtistGetInfoClientRequestFails(t *testing.T) { } func TestArtistGetTopTracks(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -185,6 +191,8 @@ func TestArtistGetTopTracks(t *testing.T) { } func TestArtistGetTopTracks_clientRequestFails(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -209,6 +217,8 @@ func TestArtistGetTopTracks_clientRequestFails(t *testing.T) { } func TestArtistGetSimilar(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -285,6 +295,8 @@ func TestArtistGetSimilar(t *testing.T) { } func TestArtistGetSimilar_clientRequestFails(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -309,6 +321,8 @@ func TestArtistGetSimilar_clientRequestFails(t *testing.T) { } func TestTrackGetSimilarTracks(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -375,6 +389,8 @@ func TestTrackGetSimilarTracks(t *testing.T) { } func TestTrackGetSimilarTracks_clientRequestFails(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -400,6 +416,8 @@ func TestTrackGetSimilarTracks_clientRequestFails(t *testing.T) { } func TestGetSession(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -426,6 +444,8 @@ func TestGetSession(t *testing.T) { } func TestGetSessioeClientRequestFails(t *testing.T) { + t.Parallel() + // arrange require := require.New(t) client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { @@ -451,6 +471,8 @@ func TestGetSessioeClientRequestFails(t *testing.T) { } func TestGetParamSignature(t *testing.T) { + t.Parallel() + params := url.Values{} params.Add("ccc", "CCC") params.Add("bbb", "BBB") diff --git a/scrobble/lastfm/mockclient/mockclient.go b/scrobble/lastfm/mockclient/mockclient.go index 2e3f8cf..593e656 100644 --- a/scrobble/lastfm/mockclient/mockclient.go +++ b/scrobble/lastfm/mockclient/mockclient.go @@ -10,9 +10,11 @@ import ( "testing" ) -func New(t testing.TB, handler http.HandlerFunc) *http.Client { +func New(tb testing.TB, handler http.HandlerFunc) *http.Client { + tb.Helper() + server := httptest.NewTLSServer(handler) - t.Cleanup(server.Close) + tb.Cleanup(server.Close) return &http.Client{ Transport: &http.Transport{ diff --git a/scrobble/listenbrainz/listenbrainz.go b/scrobble/listenbrainz/listenbrainz.go index 0e5affb..872fbf1 100644 --- a/scrobble/listenbrainz/listenbrainz.go +++ b/scrobble/listenbrainz/listenbrainz.go @@ -23,9 +23,7 @@ const ( listenTypePlayingNow = "playing_now" ) -var ( - ErrListenBrainz = errors.New("listenbrainz error") -) +var ErrListenBrainz = errors.New("listenbrainz error") type Scrobbler struct { httpClient *http.Client diff --git a/server/ctrladmin/ctrl.go b/server/ctrladmin/ctrl.go index a374349..31506c9 100644 --- a/server/ctrladmin/ctrl.go +++ b/server/ctrladmin/ctrl.go @@ -1,4 +1,3 @@ -// Package ctrladmin provides HTTP handlers for admin UI package ctrladmin import ( diff --git a/server/ctrlbase/ctrl.go b/server/ctrlbase/ctrl.go index 7d8e1b5..00d854b 100644 --- a/server/ctrlbase/ctrl.go +++ b/server/ctrlbase/ctrl.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "path" + "strings" "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/playlist" @@ -94,15 +95,20 @@ func (c *Controller) WithLogging(next http.Handler) http.Handler { } func (c *Controller) WithCORS(next http.Handler) http.Handler { + allowMethods := strings.Join( + []string{http.MethodPost, http.MethodGet, http.MethodOptions, http.MethodPut, http.MethodDelete}, + ", ", + ) + allowHeaders := strings.Join( + []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"}, + ", ", + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", - "POST, GET, OPTIONS, PUT, DELETE", - ) - w.Header().Set("Access-Control-Allow-Headers", - "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization", - ) - if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", allowMethods) + w.Header().Set("Access-Control-Allow-Headers", allowHeaders) + if r.Method == http.MethodOptions { return } next.ServeHTTP(w, r) diff --git a/server/ctrlsubsonic/artistinfocache/artistinfocache_test.go b/server/ctrlsubsonic/artistinfocache/artistinfocache_test.go index 29e923e..4c272ae 100644 --- a/server/ctrlsubsonic/artistinfocache/artistinfocache_test.go +++ b/server/ctrlsubsonic/artistinfocache/artistinfocache_test.go @@ -15,6 +15,8 @@ import ( ) func TestInfoCache(t *testing.T) { + t.Parallel() + m := mockfs.New(t) m.AddItems() m.ScanAndClean() diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index 7fc9234..889eb56 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -1,4 +1,3 @@ -// Package ctrlsubsonic provides HTTP handlers for subsonic API package ctrlsubsonic import ( diff --git a/server/ctrlsubsonic/ctrl_test.go b/server/ctrlsubsonic/ctrl_test.go index 02541e8..05f5f90 100644 --- a/server/ctrlsubsonic/ctrl_test.go +++ b/server/ctrlsubsonic/ctrl_test.go @@ -1,3 +1,4 @@ +//nolint:thelper package ctrlsubsonic import ( @@ -80,7 +81,10 @@ func makeHTTPMockWithAdmin(query url.Values) (*httptest.ResponseRecorder, *http. func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []*queryCase) { t.Helper() for _, qc := range cases { + qc := qc t.Run(qc.expectPath, func(t *testing.T) { + t.Parallel() + rr, req := makeHTTPMock(qc.params) contr.H(h).ServeHTTP(rr, req) body := rr.Body.String() @@ -91,7 +95,7 @@ func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []* goldenPath := makeGoldenPath(t.Name()) goldenRegen := os.Getenv("GONIC_REGEN") if goldenRegen == "*" || (goldenRegen != "" && strings.HasPrefix(t.Name(), goldenRegen)) { - _ = os.WriteFile(goldenPath, []byte(body), 0600) + _ = os.WriteFile(goldenPath, []byte(body), 0o600) t.Logf("golden file %q regenerated for %s", goldenPath, t.Name()) t.SkipNow() } @@ -120,14 +124,13 @@ func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []* } } -func makeController(t *testing.T) *Controller { return makec(t, []string{""}, false) } -func makeControllerRoots(t *testing.T, r []string) *Controller { return makec(t, r, false) } -func makeControllerAudio(t *testing.T) *Controller { return makec(t, []string{""}, true) } +func makeController(tb testing.TB) *Controller { return makec(tb, []string{""}, false) } +func makeControllerRoots(tb testing.TB, r []string) *Controller { return makec(tb, r, false) } -func makec(t *testing.T, roots []string, audio bool) *Controller { - t.Helper() +func makec(tb testing.TB, roots []string, audio bool) *Controller { + tb.Helper() - m := mockfs.NewWithDirs(t, roots) + m := mockfs.NewWithDirs(tb, roots) for _, root := range roots { m.AddItemsPrefixWithCovers(root) if !audio { diff --git a/server/ctrlsubsonic/handlers_by_folder.go b/server/ctrlsubsonic/handlers_by_folder.go index 677dbfb..ed7cb2d 100644 --- a/server/ctrlsubsonic/handlers_by_folder.go +++ b/server/ctrlsubsonic/handlers_by_folder.go @@ -115,7 +115,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response { } // ServeGetAlbumList handles the getAlbumList view. -// changes to this function should be reflected in in _by_tags.go's +// changes to this function should be reflected in _by_tags.go's // getAlbumListTwo() function func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response { params := r.Context().Value(CtxParams).(params.Params) @@ -130,8 +130,7 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response { case "alphabeticalByName": q = q.Order("right_path") case "byYear": - y1, y2 := - params.GetOrInt("fromYear", 1800), + y1, y2 := params.GetOrInt("fromYear", 1800), params.GetOrInt("toYear", 2200) // support some clients sending wrong order like DSub q = q.Where("tag_year BETWEEN ? AND ?", min(y1, y2), max(y1, y2)) diff --git a/server/ctrlsubsonic/handlers_by_folder_test.go b/server/ctrlsubsonic/handlers_by_folder_test.go index 4d6208c..27de6ad 100644 --- a/server/ctrlsubsonic/handlers_by_folder_test.go +++ b/server/ctrlsubsonic/handlers_by_folder_test.go @@ -8,6 +8,8 @@ import ( ) func TestGetIndexes(t *testing.T) { + t.Parallel() + contr := makeControllerRoots(t, []string{"m-0", "m-1"}) runQueryCases(t, contr, contr.ServeGetIndexes, []*queryCase{ @@ -18,6 +20,8 @@ func TestGetIndexes(t *testing.T) { } func TestGetMusicDirectory(t *testing.T) { + t.Parallel() + contr := makeController(t) runQueryCases(t, contr, contr.ServeGetMusicDirectory, []*queryCase{ diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index 3612d94..4ffb7e6 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -130,7 +130,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response { } // ServeGetAlbumListTwo handles the getAlbumList2 view. -// changes to this function should be reflected in in _by_folder.go's +// changes to this function should be reflected in _by_folder.go's // getAlbumList() function func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response { params := r.Context().Value(CtxParams).(params.Params) @@ -147,8 +147,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response { case "alphabeticalByName": q = q.Order("tag_title") case "byYear": - y1, y2 := - params.GetOrInt("fromYear", 1800), + y1, y2 := params.GetOrInt("fromYear", 1800), params.GetOrInt("toYear", 2200) // support some clients sending wrong order like DSub q = q.Where("tag_year BETWEEN ? AND ?", min(y1, y2), max(y1, y2)) diff --git a/server/ctrlsubsonic/handlers_internet_radio_test.go b/server/ctrlsubsonic/handlers_internet_radio_test.go index 18a0b4f..cd8c028 100644 --- a/server/ctrlsubsonic/handlers_internet_radio_test.go +++ b/server/ctrlsubsonic/handlers_internet_radio_test.go @@ -1,7 +1,9 @@ +//nolint:tparallel,paralleltest,thelper package ctrlsubsonic import ( "encoding/json" + "errors" "net/http" "net/http/httptest" "net/url" @@ -37,8 +39,10 @@ const ( newstation1HomepageURL = "https://www.kcrw.com/music/shows/eclectic24" ) -const newstation2StreamURL = "http://media.kcrw.com/pls/kcrwsantabarbara.pls" -const newstation2Name = "KCRW Santa Barbara" +const ( + newstation2StreamURL = "http://media.kcrw.com/pls/kcrwsantabarbara.pls" + newstation2Name = "KCRW Santa Barbara" +) const station3ID = "ir-3" @@ -48,16 +52,18 @@ func TestInternetRadio(t *testing.T) { t.Parallel() contr := makeController(t) - t.Run("TestInternetRadioInitialEmpty", func(t *testing.T) { testInternetRadioInitialEmpty(t, contr) }) - t.Run("TestInternetRadioBadCreates", func(t *testing.T) { testInternetRadioBadCreates(t, contr) }) - t.Run("TestInternetRadioInitialAdds", func(t *testing.T) { testInternetRadioInitialAdds(t, contr) }) - t.Run("TestInternetRadioUpdateHomepage", func(t *testing.T) { testInternetRadioUpdateHomepage(t, contr) }) - t.Run("TestInternetRadioNotAdmin", func(t *testing.T) { testInternetRadioNotAdmin(t, contr) }) - t.Run("TestInternetRadioUpdates", func(t *testing.T) { testInternetRadioUpdates(t, contr) }) - t.Run("TestInternetRadioDeletes", func(t *testing.T) { testInternetRadioDeletes(t, contr) }) + t.Run("initial empty", func(t *testing.T) { testInternetRadioInitialEmpty(t, contr) }) + t.Run("bad creates", func(t *testing.T) { testInternetRadioBadCreates(t, contr) }) + t.Run("initial adds", func(t *testing.T) { testInternetRadioInitialAdds(t, contr) }) + t.Run("update home page", func(t *testing.T) { testInternetRadioUpdateHomepage(t, contr) }) + t.Run("not admin", func(t *testing.T) { testInternetRadioNotAdmin(t, contr) }) + t.Run("updates", func(t *testing.T) { testInternetRadioUpdates(t, contr) }) + t.Run("deletes", func(t *testing.T) { testInternetRadioDeletes(t, contr) }) } func runTestCase(t *testing.T, contr *Controller, h handlerSubsonic, q url.Values, admin bool) *spec.SubsonicResponse { + t.Helper() + var rr *httptest.ResponseRecorder var req *http.Request @@ -74,18 +80,17 @@ func runTestCase(t *testing.T, contr *Controller, h handlerSubsonic, q url.Value var response spec.SubsonicResponse if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { - switch ty := err.(type) { - case *json.SyntaxError: - jsn := body[0:ty.Offset] - jsn += "<--(Invalid Character)" - t.Fatalf("invalid character at offset %v\n %s", ty.Offset, jsn) - case *json.UnmarshalTypeError: - jsn := body[0:ty.Offset] - jsn += "<--(Invalid Type)" - t.Fatalf("invalid type at offset %v\n %s", ty.Offset, jsn) - default: - t.Fatalf("json unmarshal failed: %s", err.Error()) + var jsonSyntaxError *json.SyntaxError + if errors.As(err, &jsonSyntaxError) { + t.Fatalf("invalid character at offset %v\n %s <--", jsonSyntaxError.Offset, body[0:jsonSyntaxError.Offset]) } + + var jsonUnmarshalTypeError *json.UnmarshalTypeError + if errors.As(err, &jsonSyntaxError) { + t.Fatalf("invalid type at offset %v\n %s <--", jsonUnmarshalTypeError.Offset, body[0:jsonUnmarshalTypeError.Offset]) + } + + t.Fatalf("json unmarshal failed: %v", err) } return &response diff --git a/server/ctrlsubsonic/handlers_playlist.go b/server/ctrlsubsonic/handlers_playlist.go index ac73242..1b240ce 100644 --- a/server/ctrlsubsonic/handlers_playlist.go +++ b/server/ctrlsubsonic/handlers_playlist.go @@ -180,6 +180,7 @@ func (c *Controller) ServeDeletePlaylist(r *http.Request) *spec.Response { func playlistIDEncode(path string) string { return base64.URLEncoding.EncodeToString([]byte(path)) } + func playlistIDDecode(id string) string { path, _ := base64.URLEncoding.DecodeString(id) return string(path) diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 21718f4..d39f241 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -62,8 +62,6 @@ func streamGetTranscodeMeta(dbc *db.DB, userID int, client string) spec.Transcod } } -var errUnknownMediaType = fmt.Errorf("media type is unknown") - func streamUpdateStats(dbc *db.DB, userID int, track *db.Track, playTime time.Time) error { var play db.Play err := dbc. diff --git a/server/ctrlsubsonic/params/params.go b/server/ctrlsubsonic/params/params.go index 93312ce..bb4658f 100644 --- a/server/ctrlsubsonic/params/params.go +++ b/server/ctrlsubsonic/params/params.go @@ -35,9 +35,7 @@ import ( "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) -var ( - ErrNoValues = errors.New("no values provided") -) +var ErrNoValues = errors.New("no values provided") // some thin wrappers // may be needed when cleaning up parse() below @@ -62,7 +60,6 @@ func parse(values []string, i interface{}) error { } var err error switch v := i.(type) { - // *T case *string: *v, err = parseStr(values[0]) diff --git a/server/ctrlsubsonic/specid/ids.go b/server/ctrlsubsonic/specid/ids.go index 75c6496..7bc0469 100644 --- a/server/ctrlsubsonic/specid/ids.go +++ b/server/ctrlsubsonic/specid/ids.go @@ -30,18 +30,17 @@ const ( separator = "-" ) +//nolint:musttag type ID struct { Type IDT Value int } func New(in string) (ID, error) { - parts := strings.Split(in, separator) - if len(parts) != 2 { + partType, partValue, ok := strings.Cut(in, separator) + if !ok { return ID{}, ErrBadSeparator } - partType := parts[0] - partValue := parts[1] val, err := strconv.Atoi(partValue) if err != nil { return ID{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt) diff --git a/server/ctrlsubsonic/specid/ids_test.go b/server/ctrlsubsonic/specid/ids_test.go index fadd3ff..d3e5303 100644 --- a/server/ctrlsubsonic/specid/ids_test.go +++ b/server/ctrlsubsonic/specid/ids_test.go @@ -6,6 +6,8 @@ import ( ) func TestParseID(t *testing.T) { + t.Parallel() + tcases := []struct { param string expType IDT @@ -20,9 +22,12 @@ func TestParseID(t *testing.T) { {param: "1", expErr: ErrBadSeparator}, {param: "al-howdy", expErr: ErrNotAnInt}, } + for _, tcase := range tcases { tcase := tcase // pin t.Run(tcase.param, func(t *testing.T) { + t.Parallel() + act, err := New(tcase.param) if !errors.Is(err, tcase.expErr) { t.Fatalf("expected err %q, got %q", tcase.expErr, err) diff --git a/server/ctrlsubsonic/specidpaths/specidpaths.go b/server/ctrlsubsonic/specidpaths/specidpaths.go index ecb05f1..c054ee1 100644 --- a/server/ctrlsubsonic/specidpaths/specidpaths.go +++ b/server/ctrlsubsonic/specidpaths/specidpaths.go @@ -9,8 +9,10 @@ import ( "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) -var ErrNotAbs = errors.New("not abs") -var ErrNotFound = errors.New("not found") +var ( + ErrNotAbs = errors.New("not abs") + ErrNotFound = errors.New("not found") +) type Result interface { SID() *specid.ID diff --git a/transcode/transcoder_caching.go b/transcode/transcoder_caching.go index 8695e7a..f1043b2 100644 --- a/transcode/transcoder_caching.go +++ b/transcode/transcoder_caching.go @@ -9,7 +9,7 @@ import ( "path/filepath" ) -const perm = 0644 +const perm = 0o644 type CachingTranscoder struct { cachePath string @@ -23,7 +23,7 @@ func NewCachingTranscoder(t Transcoder, cachePath string) *CachingTranscoder { } func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error { - if err := os.MkdirAll(t.cachePath, perm^0111); err != nil { + if err := os.MkdirAll(t.cachePath, perm^0o111); err != nil { return fmt.Errorf("make cache path: %w", err) } @@ -35,7 +35,7 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s key := cacheKey(name, args) path := filepath.Join(t.cachePath, key) - cf, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) + cf, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644) if err != nil { return fmt.Errorf("open cache file: %w", err) } diff --git a/transcode/transcoder_ffmpeg.go b/transcode/transcoder_ffmpeg.go index f0a471b..f71b13e 100644 --- a/transcode/transcoder_ffmpeg.go +++ b/transcode/transcoder_ffmpeg.go @@ -16,8 +16,10 @@ func NewFFmpegTranscoder() *FFmpegTranscoder { return &FFmpegTranscoder{} } -var ErrFFmpegKilled = fmt.Errorf("ffmpeg was killed early") -var ErrFFmpegExit = fmt.Errorf("ffmpeg exited with non 0 status code") +var ( + ErrFFmpegKilled = fmt.Errorf("ffmpeg was killed early") + ErrFFmpegExit = fmt.Errorf("ffmpeg exited with non 0 status code") +) func (*FFmpegTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error { name, args, err := parseProfile(profile, in) @@ -36,7 +38,7 @@ func (*FFmpegTranscoder) Transcode(ctx context.Context, profile Profile, in stri switch err := cmd.Wait(); { case errors.As(err, &exitErr): - return fmt.Errorf("waiting cmd: %v: %w", err, ErrFFmpegKilled) + return fmt.Errorf("waiting cmd: %w: %w", err, ErrFFmpegKilled) case err != nil: return fmt.Errorf("waiting cmd: %w", err) } diff --git a/version.go b/version.go index b23a444..06c99af 100644 --- a/version.go +++ b/version.go @@ -10,5 +10,7 @@ import ( var version string var Version = strings.TrimSpace(version) -const Name = "gonic" -const NameUpper = "GONIC" +const ( + Name = "gonic" + NameUpper = "GONIC" +)