feat(ci): add a bunch more linters

This commit is contained in:
sentriz
2023-09-22 19:05:20 +02:00
parent 33f1f2e0cf
commit e3dd812b6c
37 changed files with 233 additions and 139 deletions

View File

@@ -6,28 +6,72 @@ run:
linters: linters:
disable-all: true disable-all: true
enable: enable:
- asasalint
- asciicheck
- bidichk
- bodyclose - bodyclose
- containedctx
- decorder
- dogsled - dogsled
- dupword
- durationcheck
- errcheck - errcheck
- errchkjson
- errname
- errorlint
- execinquery
- exportloopref - exportloopref
- forbidigo
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals - gochecknoglobals
- gochecknoinits - gochecknoinits
- goconst - goconst
- gocritic - gocritic
- gocyclo - gocyclo
- gofmt
- gofumpt
- goheader
- goimports
- gomoddirectives
- gomodguard
- goprintffuncname - goprintffuncname
- gosec - gosec
- gosimple - gosimple
- gosmopolitan
- govet - govet
- grouper
- importas
- ineffassign - ineffassign
- loggercheck
- makezero
- mirror
- misspell - misspell
- musttag
- nakedret - nakedret
- nestif
- nilerr
- nosprintfhostport
- paralleltest
- predeclared
- promlinter
- reassign
- revive - revive
- rowserrcheck - rowserrcheck
- sqlclosecheck
- staticcheck - staticcheck
- stylecheck - stylecheck
- tenv
- testableexamples
- thelper
- tparallel
- typecheck - typecheck
- unconvert - unconvert
- unparam
- unused
- wastedassign
- whitespace
- zerologlint
issues: issues:
exclude-rules: exclude-rules:

View File

@@ -1,6 +1,4 @@
// Package main is the gonic server entrypoint //nolint:lll,gocyclo,forbidigo
//
//nolint:lll,gocyclo
package main package main
import ( import (
@@ -370,8 +368,10 @@ func main() {
const pathAliasSep = "->" const pathAliasSep = "->"
type pathAliases []pathAlias type (
type pathAlias struct{ alias, path string } pathAliases []pathAlias
pathAlias struct{ alias, path string }
)
func (pa pathAliases) String() string { func (pa pathAliases) String() string {
var strs []string var strs []string

View File

@@ -21,6 +21,8 @@ func randKey() string {
} }
func TestGetSetting(t *testing.T) { func TestGetSetting(t *testing.T) {
t.Parallel()
key := SettingKey(randKey()) key := SettingKey(randKey())
value := "howdy" value := "howdy"

View File

@@ -1,5 +1,3 @@
// Package db provides database helpers and models
//
//nolint:lll // struct tags get very long and can't be split //nolint:lll // struct tags get very long and can't be split
package db package db
@@ -238,7 +236,7 @@ func (a *Album) GenreStrings() []string {
} }
func (a *Album) ArtistsStrings() []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 { sort.Slice(artists, func(i, j int) bool {
return artists[i].ID < artists[j].ID return artists[i].ID < artists[j].ID
}) })

View File

@@ -9,6 +9,8 @@ import (
) )
func TestUniquePath(t *testing.T) { func TestUniquePath(t *testing.T) {
t.Parallel()
unq := func(base, filename string, count uint) string { unq := func(base, filename string, count uint) string {
r, err := unique(base, filename, count) r, err := unique(base, filename, count)
require.NoError(t, err) require.NoError(t, err)
@@ -40,6 +42,8 @@ func TestUniquePath(t *testing.T) {
} }
func TestFirst(t *testing.T) { func TestFirst(t *testing.T) {
t.Parallel()
base := t.TempDir() base := t.TempDir()
name := filepath.Join(base, "test") name := filepath.Join(base, "test")
_, err := os.Create(name) _, err := os.Create(name)
@@ -52,5 +56,4 @@ func TestFirst(t *testing.T) {
r, err := First(p("one"), p("two"), p("test"), p("four")) r, err := First(p("one"), p("two"), p("test"), p("four"))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, p("test"), r) require.Equal(t, p("test"), r)
} }

View File

@@ -331,13 +331,15 @@ func (j *Jukebox) getDecode(dest any, property string) error {
return nil return nil
} }
type mpvPlaylist []mpvPlaylistItem type (
type mpvPlaylistItem struct { mpvPlaylist []mpvPlaylistItem
mpvPlaylistItem struct {
ID int ID int
Filename string Filename string
Current bool Current bool
Playing bool Playing bool
} }
)
func waitUntil(timeout time.Duration, f func() bool) bool { func waitUntil(timeout time.Duration, f func() bool) bool {
quit := time.NewTicker(timeout) quit := time.NewTicker(timeout)

View File

@@ -11,8 +11,10 @@ import (
"go.senan.xyz/gonic/jukebox" "go.senan.xyz/gonic/jukebox"
) )
func newJukebox(t *testing.T) *jukebox.Jukebox { func newJukebox(tb testing.TB) *jukebox.Jukebox {
sockPath := filepath.Join(t.TempDir(), "mpv.sock") tb.Helper()
sockPath := filepath.Join(tb.TempDir(), "mpv.sock")
j := jukebox.New() j := jukebox.New()
err := j.Start( err := j.Start(
@@ -20,12 +22,12 @@ func newJukebox(t *testing.T) *jukebox.Jukebox {
[]string{jukebox.MPVArg("--ao", "null")}, []string{jukebox.MPVArg("--ao", "null")},
) )
if errors.Is(err, jukebox.ErrMPVTooOld) { if errors.Is(err, jukebox.ErrMPVTooOld) {
t.Skip("old mpv found, skipping") tb.Skip("old mpv found, skipping")
} }
if err != nil { if err != nil {
t.Fatalf("start jukebox: %v", err) tb.Fatalf("start jukebox: %v", err)
} }
t.Cleanup(func() { tb.Cleanup(func() {
j.Quit() j.Quit()
}) })
return j return j

View File

@@ -28,9 +28,11 @@ func init() {
} }
} }
var TypeByExtension = stdmime.TypeByExtension var (
var ParseMediaType = stdmime.ParseMediaType TypeByExtension = stdmime.TypeByExtension
var FormatMediaType = stdmime.FormatMediaType ParseMediaType = stdmime.ParseMediaType
FormatMediaType = stdmime.FormatMediaType
)
func TypeByAudioExtension(ext string) string { func TypeByAudioExtension(ext string) string {
if _, ok := supportedAudioTypes[strings.ToLower(ext)]; !ok { if _, ok := supportedAudioTypes[strings.ToLower(ext)]; !ok {

View File

@@ -1,3 +1,4 @@
//nolint:thelper
package mockfs package mockfs
import ( import (
@@ -27,29 +28,31 @@ type MockFS struct {
db *db.DB db *db.DB
} }
func New(t testing.TB) *MockFS { return newMockFS(t, []string{""}, "") } func New(tb testing.TB) *MockFS { return newMockFS(tb, []string{""}, "") }
func NewWithDirs(t testing.TB, dirs []string) *MockFS { return newMockFS(t, dirs, "") } func NewWithDirs(tb testing.TB, dirs []string) *MockFS { return newMockFS(tb, dirs, "") }
func NewWithExcludePattern(t testing.TB, excludePattern string) *MockFS { func NewWithExcludePattern(tb testing.TB, excludePattern string) *MockFS {
return newMockFS(t, []string{""}, excludePattern) 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() dbc, err := db.NewMock()
if err != nil { 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 { 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 { 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) dbc.LogMode(false)
tmpDir := t.TempDir() tmpDir := tb.TempDir()
var absDirs []string var absDirs []string
for _, dir := range dirs { for _, dir := range dirs {
@@ -57,7 +60,7 @@ func newMockFS(t testing.TB, dirs []string, excludePattern string) *MockFS {
} }
for _, absDir := range absDirs { for _, absDir := range absDirs {
if err := os.MkdirAll(absDir, os.ModePerm); err != nil { 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) scanner := scanner.New(absDirs, dbc, multiValueSettings, tagReader, excludePattern)
return &MockFS{ return &MockFS{
t: t, t: tb,
scanner: scanner, scanner: scanner,
dir: tmpDir, dir: tmpDir,
tagReader: tagReader, tagReader: tagReader,
@@ -399,15 +402,6 @@ func (m *Tags) Bitrate() int { return firstInt(100, m.RawBitrate) }
var _ tags.Parser = (*Tags)(nil) 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 { func firstInt(or int, ints ...int) int {
for _, int := range ints { for _, int := range ints {
if int > 0 { if int > 0 {

View File

@@ -14,9 +14,11 @@ import (
"time" "time"
) )
var ErrInvalidPathFormat = errors.New("invalid path format") var (
var ErrInvalidBasePath = errors.New("invalid base path") ErrInvalidPathFormat = errors.New("invalid path format")
var ErrNoUserPrefix = errors.New("no user prefix") ErrInvalidBasePath = errors.New("invalid base path")
ErrNoUserPrefix = errors.New("no user prefix")
)
const ( const (
extM3U = ".m3u" extM3U = ".m3u"
@@ -31,7 +33,6 @@ type Store struct {
func NewStore(basePath string) (*Store, error) { func NewStore(basePath string) (*Store, error) {
if basePath == "" { if basePath == "" {
return nil, ErrInvalidBasePath return nil, ErrInvalidBasePath
} }
// sanity check layout, just in case someone tries to use an existing folder // sanity check layout, just in case someone tries to use an existing folder
@@ -108,6 +109,7 @@ const (
func encodeAttr(name, value string) string { func encodeAttr(name, value string) string {
return fmt.Sprintf("%s%s:%s", attrPrefix, name, strconv.Quote(value)) return fmt.Sprintf("%s%s:%s", attrPrefix, name, strconv.Quote(value))
} }
func decodeAttr(line string) (name, value string) { func decodeAttr(line string) (name, value string) {
if !strings.HasPrefix(line, attrPrefix) { if !strings.HasPrefix(line, attrPrefix) {
return "", "" return "", ""
@@ -169,10 +171,10 @@ func (s *Store) Write(relPath string, playlist *Playlist) error {
defer lock(&s.mu)() defer lock(&s.mu)()
absPath := filepath.Join(s.basePath, relPath) 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) 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 { if err != nil {
return fmt.Errorf("create m3u: %w", err) return fmt.Errorf("create m3u: %w", err)
} }

View File

@@ -8,6 +8,8 @@ import (
) )
func TestPlaylist(t *testing.T) { func TestPlaylist(t *testing.T) {
t.Parallel()
require := require.New(t) require := require.New(t)
tmp := t.TempDir() tmp := t.TempDir()

View File

@@ -25,8 +25,10 @@ import (
var ErrNoAudioInFeedItem = errors.New("no audio in feed item") var ErrNoAudioInFeedItem = errors.New("no audio in feed item")
const downloadAllWaitInterval = 3 * time.Second const (
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` 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 { type Podcasts struct {
db *db.DB 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)), "") rootDir, err := fileutil.Unique(filepath.Join(p.baseDir, fileutil.Safe(feed.Title)), "")
if err != nil { if err != nil {
return nil, fmt.Errorf("find unique podcast dir: %w", err) return nil, fmt.Errorf("find unique podcast dir: %w", err)
} }
podcast := db.Podcast{ podcast := db.Podcast{
Description: feed.Description, Description: feed.Description,
@@ -105,7 +106,7 @@ func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed) (*db.Podcast,
URL: rssURL, URL: rssURL,
RootDir: rootDir, 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 return nil, err
} }
if err := p.db.Save(&podcast).Error; err != nil { 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, func itemToEpisode(podcastID, size, duration int, audio string,
item *gofeed.Item) *db.PodcastEpisode { item *gofeed.Item,
) *db.PodcastEpisode {
return &db.PodcastEpisode{ return &db.PodcastEpisode{
PodcastID: podcastID, PodcastID: podcastID,
Description: item.Description, Description: item.Description,

View File

@@ -17,6 +17,8 @@ import (
var testRSS []byte var testRSS []byte
func TestPodcastsAndEpisodesWithSameName(t *testing.T) { func TestPodcastsAndEpisodesWithSameName(t *testing.T) {
t.Parallel()
t.Skip("requires network access") t.Skip("requires network access")
m := mockfs.New(t) m := mockfs.New(t)
@@ -62,6 +64,8 @@ func TestPodcastsAndEpisodesWithSameName(t *testing.T) {
} }
func TestGetMoreRecentEpisodes(t *testing.T) { func TestGetMoreRecentEpisodes(t *testing.T) {
t.Parallel()
fp := gofeed.NewParser() fp := gofeed.NewParser()
newFeed, err := fp.Parse(bytes.NewReader(testRSS)) newFeed, err := fp.Parse(bytes.NewReader(testRSS))
if err != nil { if err != nil {

View File

@@ -61,9 +61,11 @@ func New(musicDirs []string, db *db.DB, multiValueSettings map[Tag]MultiValueSet
func (s *Scanner) IsScanning() bool { func (s *Scanner) IsScanning() bool {
return atomic.LoadInt32(s.scanning) == 1 return atomic.LoadInt32(s.scanning) == 1
} }
func (s *Scanner) StartScanning() bool { func (s *Scanner) StartScanning() bool {
return atomic.CompareAndSwapInt32(s.scanning, 0, 1) return atomic.CompareAndSwapInt32(s.scanning, 0, 1)
} }
func (s *Scanner) StopScanning() { func (s *Scanner) StopScanning() {
defer atomic.StoreInt32(s.scanning, 0) defer atomic.StoreInt32(s.scanning, 0)
} }
@@ -173,7 +175,6 @@ func (s *Scanner) ExecuteWatch() error {
if err != nil { if err != nil {
log.Printf("error walking: %v", err) log.Printf("error walking: %v", err)
} }
} }
scanList = map[string]struct{}{} scanList = map[string]struct{}{}
s.StopScanning() s.StopScanning()
@@ -343,7 +344,7 @@ func (s *Scanner) populateTrackAndAlbumArtists(tx *db.DB, c *Context, i int, alb
trags, err := s.tagger.Read(absPath) trags, err := s.tagger.Read(absPath)
if err != nil { 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) 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) 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 { if i == 0 {
albumArtistNames := parseMulti(trags, s.multiValueSettings[AlbumArtist], tags.MustAlbumArtists, tags.MustAlbumArtist) albumArtistNames := parseMulti(trags, s.multiValueSettings[AlbumArtist], tags.MustAlbumArtists, tags.MustAlbumArtist)
var albumArtistIDs []int 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 { 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 { 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 { 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 return nil
} }
func (s *Scanner) cleanGenres(c *Context) error { func (s *Scanner) cleanGenres(c *Context) error { //nolint:unparam
start := time.Now() start := time.Now()
defer func() { log.Printf("finished clean genres in %s, %d removed", durSince(start), c.GenresMissing()) }() defer func() { log.Printf("finished clean genres in %s, %d removed", durSince(start), c.GenresMissing()) }()

View File

@@ -1,4 +1,4 @@
//nolint:goconst //nolint:goconst,errorlint
package scanner_test package scanner_test
import ( import (
@@ -724,7 +724,6 @@ func TestMultiArtistSupport(t *testing.T) {
}, },
state(), state(),
) )
} }
func TestMultiArtistPreload(t *testing.T) { func TestMultiArtistPreload(t *testing.T) {

View File

@@ -34,9 +34,11 @@ func (t *Tagger) Genres() []string { return find(t.raw, "genres") }
func (t *Tagger) TrackNumber() int { func (t *Tagger) TrackNumber() int {
return intSep("/" /* eg. 5/12 */, first(find(t.raw, "tracknumber"))) return intSep("/" /* eg. 5/12 */, first(find(t.raw, "tracknumber")))
} }
func (t *Tagger) DiscNumber() int { func (t *Tagger) DiscNumber() int {
return intSep("/" /* eg. 1/2 */, first(find(t.raw, "discnumber"))) return intSep("/" /* eg. 1/2 */, first(find(t.raw, "discnumber")))
} }
func (t *Tagger) Year() int { func (t *Tagger) Year() int {
return intSep("-" /* 2023-12-01 */, first(find(t.raw, "originaldate", "date", "year"))) return intSep("-" /* 2023-12-01 */, first(find(t.raw, "originaldate", "date", "year")))
} }
@@ -65,15 +67,6 @@ type Parser interface {
Year() int 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 { func first[T comparable](is []T) T {
var z T var z T
for _, i := range is { for _, i := range is {

View File

@@ -13,6 +13,8 @@ import (
) )
func TestArtistGetInfo(t *testing.T) { func TestArtistGetInfo(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestArtistGetInfoClientRequestFails(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestArtistGetTopTracks(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestArtistGetTopTracks_clientRequestFails(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestArtistGetSimilar(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestArtistGetSimilar_clientRequestFails(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestTrackGetSimilarTracks(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestTrackGetSimilarTracks_clientRequestFails(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestGetSession(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestGetSessioeClientRequestFails(t *testing.T) {
t.Parallel()
// arrange // arrange
require := require.New(t) require := require.New(t)
client := Client{mockclient.New(t, func(w http.ResponseWriter, r *http.Request) { 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) { func TestGetParamSignature(t *testing.T) {
t.Parallel()
params := url.Values{} params := url.Values{}
params.Add("ccc", "CCC") params.Add("ccc", "CCC")
params.Add("bbb", "BBB") params.Add("bbb", "BBB")

View File

@@ -10,9 +10,11 @@ import (
"testing" "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) server := httptest.NewTLSServer(handler)
t.Cleanup(server.Close) tb.Cleanup(server.Close)
return &http.Client{ return &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{

View File

@@ -23,9 +23,7 @@ const (
listenTypePlayingNow = "playing_now" listenTypePlayingNow = "playing_now"
) )
var ( var ErrListenBrainz = errors.New("listenbrainz error")
ErrListenBrainz = errors.New("listenbrainz error")
)
type Scrobbler struct { type Scrobbler struct {
httpClient *http.Client httpClient *http.Client

View File

@@ -1,4 +1,3 @@
// Package ctrladmin provides HTTP handlers for admin UI
package ctrladmin package ctrladmin
import ( import (

View File

@@ -5,6 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"path" "path"
"strings"
"go.senan.xyz/gonic/db" "go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/playlist" "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 { 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", w.Header().Set("Access-Control-Allow-Methods", allowMethods)
"POST, GET, OPTIONS, PUT, DELETE", w.Header().Set("Access-Control-Allow-Headers", allowHeaders)
) if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Headers",
"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization",
)
if r.Method == "OPTIONS" {
return return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View File

@@ -15,6 +15,8 @@ import (
) )
func TestInfoCache(t *testing.T) { func TestInfoCache(t *testing.T) {
t.Parallel()
m := mockfs.New(t) m := mockfs.New(t)
m.AddItems() m.AddItems()
m.ScanAndClean() m.ScanAndClean()

View File

@@ -1,4 +1,3 @@
// Package ctrlsubsonic provides HTTP handlers for subsonic API
package ctrlsubsonic package ctrlsubsonic
import ( import (

View File

@@ -1,3 +1,4 @@
//nolint:thelper
package ctrlsubsonic package ctrlsubsonic
import ( import (
@@ -80,7 +81,10 @@ func makeHTTPMockWithAdmin(query url.Values) (*httptest.ResponseRecorder, *http.
func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []*queryCase) { func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []*queryCase) {
t.Helper() t.Helper()
for _, qc := range cases { for _, qc := range cases {
qc := qc
t.Run(qc.expectPath, func(t *testing.T) { t.Run(qc.expectPath, func(t *testing.T) {
t.Parallel()
rr, req := makeHTTPMock(qc.params) rr, req := makeHTTPMock(qc.params)
contr.H(h).ServeHTTP(rr, req) contr.H(h).ServeHTTP(rr, req)
body := rr.Body.String() body := rr.Body.String()
@@ -91,7 +95,7 @@ func runQueryCases(t *testing.T, contr *Controller, h handlerSubsonic, cases []*
goldenPath := makeGoldenPath(t.Name()) goldenPath := makeGoldenPath(t.Name())
goldenRegen := os.Getenv("GONIC_REGEN") goldenRegen := os.Getenv("GONIC_REGEN")
if goldenRegen == "*" || (goldenRegen != "" && strings.HasPrefix(t.Name(), goldenRegen)) { 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.Logf("golden file %q regenerated for %s", goldenPath, t.Name())
t.SkipNow() 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 makeController(tb testing.TB) *Controller { return makec(tb, []string{""}, false) }
func makeControllerRoots(t *testing.T, r []string) *Controller { return makec(t, r, false) } func makeControllerRoots(tb testing.TB, r []string) *Controller { return makec(tb, r, false) }
func makeControllerAudio(t *testing.T) *Controller { return makec(t, []string{""}, true) }
func makec(t *testing.T, roots []string, audio bool) *Controller { func makec(tb testing.TB, roots []string, audio bool) *Controller {
t.Helper() tb.Helper()
m := mockfs.NewWithDirs(t, roots) m := mockfs.NewWithDirs(tb, roots)
for _, root := range roots { for _, root := range roots {
m.AddItemsPrefixWithCovers(root) m.AddItemsPrefixWithCovers(root)
if !audio { if !audio {

View File

@@ -115,7 +115,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
} }
// ServeGetAlbumList handles the getAlbumList view. // 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 // getAlbumListTwo() function
func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response { func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
@@ -130,8 +130,7 @@ func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
case "alphabeticalByName": case "alphabeticalByName":
q = q.Order("right_path") q = q.Order("right_path")
case "byYear": case "byYear":
y1, y2 := y1, y2 := params.GetOrInt("fromYear", 1800),
params.GetOrInt("fromYear", 1800),
params.GetOrInt("toYear", 2200) params.GetOrInt("toYear", 2200)
// support some clients sending wrong order like DSub // support some clients sending wrong order like DSub
q = q.Where("tag_year BETWEEN ? AND ?", min(y1, y2), max(y1, y2)) q = q.Where("tag_year BETWEEN ? AND ?", min(y1, y2), max(y1, y2))

View File

@@ -8,6 +8,8 @@ import (
) )
func TestGetIndexes(t *testing.T) { func TestGetIndexes(t *testing.T) {
t.Parallel()
contr := makeControllerRoots(t, []string{"m-0", "m-1"}) contr := makeControllerRoots(t, []string{"m-0", "m-1"})
runQueryCases(t, contr, contr.ServeGetIndexes, []*queryCase{ runQueryCases(t, contr, contr.ServeGetIndexes, []*queryCase{
@@ -18,6 +20,8 @@ func TestGetIndexes(t *testing.T) {
} }
func TestGetMusicDirectory(t *testing.T) { func TestGetMusicDirectory(t *testing.T) {
t.Parallel()
contr := makeController(t) contr := makeController(t)
runQueryCases(t, contr, contr.ServeGetMusicDirectory, []*queryCase{ runQueryCases(t, contr, contr.ServeGetMusicDirectory, []*queryCase{

View File

@@ -130,7 +130,7 @@ func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
} }
// ServeGetAlbumListTwo handles the getAlbumList2 view. // 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 // getAlbumList() function
func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response { func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
@@ -147,8 +147,7 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
case "alphabeticalByName": case "alphabeticalByName":
q = q.Order("tag_title") q = q.Order("tag_title")
case "byYear": case "byYear":
y1, y2 := y1, y2 := params.GetOrInt("fromYear", 1800),
params.GetOrInt("fromYear", 1800),
params.GetOrInt("toYear", 2200) params.GetOrInt("toYear", 2200)
// support some clients sending wrong order like DSub // support some clients sending wrong order like DSub
q = q.Where("tag_year BETWEEN ? AND ?", min(y1, y2), max(y1, y2)) q = q.Where("tag_year BETWEEN ? AND ?", min(y1, y2), max(y1, y2))

View File

@@ -1,7 +1,9 @@
//nolint:tparallel,paralleltest,thelper
package ctrlsubsonic package ctrlsubsonic
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@@ -37,8 +39,10 @@ const (
newstation1HomepageURL = "https://www.kcrw.com/music/shows/eclectic24" newstation1HomepageURL = "https://www.kcrw.com/music/shows/eclectic24"
) )
const newstation2StreamURL = "http://media.kcrw.com/pls/kcrwsantabarbara.pls" const (
const newstation2Name = "KCRW Santa Barbara" newstation2StreamURL = "http://media.kcrw.com/pls/kcrwsantabarbara.pls"
newstation2Name = "KCRW Santa Barbara"
)
const station3ID = "ir-3" const station3ID = "ir-3"
@@ -48,16 +52,18 @@ func TestInternetRadio(t *testing.T) {
t.Parallel() t.Parallel()
contr := makeController(t) contr := makeController(t)
t.Run("TestInternetRadioInitialEmpty", func(t *testing.T) { testInternetRadioInitialEmpty(t, contr) }) t.Run("initial empty", func(t *testing.T) { testInternetRadioInitialEmpty(t, contr) })
t.Run("TestInternetRadioBadCreates", func(t *testing.T) { testInternetRadioBadCreates(t, contr) }) t.Run("bad creates", func(t *testing.T) { testInternetRadioBadCreates(t, contr) })
t.Run("TestInternetRadioInitialAdds", func(t *testing.T) { testInternetRadioInitialAdds(t, contr) }) t.Run("initial adds", func(t *testing.T) { testInternetRadioInitialAdds(t, contr) })
t.Run("TestInternetRadioUpdateHomepage", func(t *testing.T) { testInternetRadioUpdateHomepage(t, contr) }) t.Run("update home page", func(t *testing.T) { testInternetRadioUpdateHomepage(t, contr) })
t.Run("TestInternetRadioNotAdmin", func(t *testing.T) { testInternetRadioNotAdmin(t, contr) }) t.Run("not admin", func(t *testing.T) { testInternetRadioNotAdmin(t, contr) })
t.Run("TestInternetRadioUpdates", func(t *testing.T) { testInternetRadioUpdates(t, contr) }) t.Run("updates", func(t *testing.T) { testInternetRadioUpdates(t, contr) })
t.Run("TestInternetRadioDeletes", func(t *testing.T) { testInternetRadioDeletes(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 { func runTestCase(t *testing.T, contr *Controller, h handlerSubsonic, q url.Values, admin bool) *spec.SubsonicResponse {
t.Helper()
var rr *httptest.ResponseRecorder var rr *httptest.ResponseRecorder
var req *http.Request var req *http.Request
@@ -74,18 +80,17 @@ func runTestCase(t *testing.T, contr *Controller, h handlerSubsonic, q url.Value
var response spec.SubsonicResponse var response spec.SubsonicResponse
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil { if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
switch ty := err.(type) { var jsonSyntaxError *json.SyntaxError
case *json.SyntaxError: if errors.As(err, &jsonSyntaxError) {
jsn := body[0:ty.Offset] t.Fatalf("invalid character at offset %v\n %s <--", jsonSyntaxError.Offset, body[0:jsonSyntaxError.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 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 return &response

View File

@@ -180,6 +180,7 @@ func (c *Controller) ServeDeletePlaylist(r *http.Request) *spec.Response {
func playlistIDEncode(path string) string { func playlistIDEncode(path string) string {
return base64.URLEncoding.EncodeToString([]byte(path)) return base64.URLEncoding.EncodeToString([]byte(path))
} }
func playlistIDDecode(id string) string { func playlistIDDecode(id string) string {
path, _ := base64.URLEncoding.DecodeString(id) path, _ := base64.URLEncoding.DecodeString(id)
return string(path) return string(path)

View File

@@ -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 { func streamUpdateStats(dbc *db.DB, userID int, track *db.Track, playTime time.Time) error {
var play db.Play var play db.Play
err := dbc. err := dbc.

View File

@@ -35,9 +35,7 @@ import (
"go.senan.xyz/gonic/server/ctrlsubsonic/specid" "go.senan.xyz/gonic/server/ctrlsubsonic/specid"
) )
var ( var ErrNoValues = errors.New("no values provided")
ErrNoValues = errors.New("no values provided")
)
// some thin wrappers // some thin wrappers
// may be needed when cleaning up parse() below // may be needed when cleaning up parse() below
@@ -62,7 +60,6 @@ func parse(values []string, i interface{}) error {
} }
var err error var err error
switch v := i.(type) { switch v := i.(type) {
// *T // *T
case *string: case *string:
*v, err = parseStr(values[0]) *v, err = parseStr(values[0])

View File

@@ -30,18 +30,17 @@ const (
separator = "-" separator = "-"
) )
//nolint:musttag
type ID struct { type ID struct {
Type IDT Type IDT
Value int Value int
} }
func New(in string) (ID, error) { func New(in string) (ID, error) {
parts := strings.Split(in, separator) partType, partValue, ok := strings.Cut(in, separator)
if len(parts) != 2 { if !ok {
return ID{}, ErrBadSeparator return ID{}, ErrBadSeparator
} }
partType := parts[0]
partValue := parts[1]
val, err := strconv.Atoi(partValue) val, err := strconv.Atoi(partValue)
if err != nil { if err != nil {
return ID{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt) return ID{}, fmt.Errorf("%q: %w", partValue, ErrNotAnInt)

View File

@@ -6,6 +6,8 @@ import (
) )
func TestParseID(t *testing.T) { func TestParseID(t *testing.T) {
t.Parallel()
tcases := []struct { tcases := []struct {
param string param string
expType IDT expType IDT
@@ -20,9 +22,12 @@ func TestParseID(t *testing.T) {
{param: "1", expErr: ErrBadSeparator}, {param: "1", expErr: ErrBadSeparator},
{param: "al-howdy", expErr: ErrNotAnInt}, {param: "al-howdy", expErr: ErrNotAnInt},
} }
for _, tcase := range tcases { for _, tcase := range tcases {
tcase := tcase // pin tcase := tcase // pin
t.Run(tcase.param, func(t *testing.T) { t.Run(tcase.param, func(t *testing.T) {
t.Parallel()
act, err := New(tcase.param) act, err := New(tcase.param)
if !errors.Is(err, tcase.expErr) { if !errors.Is(err, tcase.expErr) {
t.Fatalf("expected err %q, got %q", tcase.expErr, err) t.Fatalf("expected err %q, got %q", tcase.expErr, err)

View File

@@ -9,8 +9,10 @@ import (
"go.senan.xyz/gonic/server/ctrlsubsonic/specid" "go.senan.xyz/gonic/server/ctrlsubsonic/specid"
) )
var ErrNotAbs = errors.New("not abs") var (
var ErrNotFound = errors.New("not found") ErrNotAbs = errors.New("not abs")
ErrNotFound = errors.New("not found")
)
type Result interface { type Result interface {
SID() *specid.ID SID() *specid.ID

View File

@@ -9,7 +9,7 @@ import (
"path/filepath" "path/filepath"
) )
const perm = 0644 const perm = 0o644
type CachingTranscoder struct { type CachingTranscoder struct {
cachePath string 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 { 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) 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) key := cacheKey(name, args)
path := filepath.Join(t.cachePath, key) 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 { if err != nil {
return fmt.Errorf("open cache file: %w", err) return fmt.Errorf("open cache file: %w", err)
} }

View File

@@ -16,8 +16,10 @@ func NewFFmpegTranscoder() *FFmpegTranscoder {
return &FFmpegTranscoder{} return &FFmpegTranscoder{}
} }
var ErrFFmpegKilled = fmt.Errorf("ffmpeg was killed early") var (
var ErrFFmpegExit = fmt.Errorf("ffmpeg exited with non 0 status code") 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 { func (*FFmpegTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error {
name, args, err := parseProfile(profile, in) name, args, err := parseProfile(profile, in)
@@ -36,7 +38,7 @@ func (*FFmpegTranscoder) Transcode(ctx context.Context, profile Profile, in stri
switch err := cmd.Wait(); { switch err := cmd.Wait(); {
case errors.As(err, &exitErr): 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: case err != nil:
return fmt.Errorf("waiting cmd: %w", err) return fmt.Errorf("waiting cmd: %w", err)
} }

View File

@@ -10,5 +10,7 @@ import (
var version string var version string
var Version = strings.TrimSpace(version) var Version = strings.TrimSpace(version)
const Name = "gonic" const (
const NameUpper = "GONIC" Name = "gonic"
NameUpper = "GONIC"
)