feat(ci): add a bunch more linters
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
ID int
|
mpvPlaylistItem struct {
|
||||||
Filename string
|
ID int
|
||||||
Current bool
|
Filename string
|
||||||
Playing bool
|
Current 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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()) }()
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package ctrladmin provides HTTP handlers for admin UI
|
|
||||||
package ctrladmin
|
package ctrladmin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package ctrlsubsonic provides HTTP handlers for subsonic API
|
|
||||||
package ctrlsubsonic
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user