put helpers last
This commit is contained in:
@@ -11,13 +11,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func randKey() string {
|
||||
letters := []rune("abcdef0123456789")
|
||||
b := make([]rune, 16)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetOutput(io.Discard)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestGetSetting(t *testing.T) {
|
||||
@@ -46,7 +42,11 @@ func TestGetSetting(t *testing.T) {
|
||||
require.Equal(t, value, actual)
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetOutput(io.Discard)
|
||||
os.Exit(m.Run())
|
||||
func randKey() string {
|
||||
letters := []rune("abcdef0123456789")
|
||||
b := make([]rune, 16)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
51
db/model.go
51
db/model.go
@@ -1,9 +1,6 @@
|
||||
//nolint:lll // struct tags get very long and can't be split
|
||||
package db
|
||||
|
||||
// see this db fiddle to mess around with the schema
|
||||
// https://www.db-fiddle.com/f/wJ7z8L7mu6ZKaYmWk1xr1p/5
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
@@ -17,30 +14,6 @@ import (
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
||||
)
|
||||
|
||||
func splitIDs(in, sep string) []specid.ID {
|
||||
if in == "" {
|
||||
return []specid.ID{}
|
||||
}
|
||||
parts := strings.Split(in, sep)
|
||||
ret := make([]specid.ID, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
id, _ := specid.New(p)
|
||||
ret = append(ret, id)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func join[T fmt.Stringer](in []T, sep string) string {
|
||||
if in == nil {
|
||||
return ""
|
||||
}
|
||||
strs := make([]string, 0, len(in))
|
||||
for _, id := range in {
|
||||
strs = append(strs, id.String())
|
||||
}
|
||||
return strings.Join(strs, sep)
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
ID int `gorm:"primary_key"`
|
||||
Name string `gorm:"not null; unique_index"`
|
||||
@@ -461,3 +434,27 @@ func (p *ArtistInfo) SetSimilarArtists(items []string) { p.SimilarArtists = stri
|
||||
|
||||
func (p *ArtistInfo) GetTopTracks() []string { return strings.Split(p.TopTracks, ";") }
|
||||
func (p *ArtistInfo) SetTopTracks(items []string) { p.TopTracks = strings.Join(items, ";") }
|
||||
|
||||
func splitIDs(in, sep string) []specid.ID {
|
||||
if in == "" {
|
||||
return []specid.ID{}
|
||||
}
|
||||
parts := strings.Split(in, sep)
|
||||
ret := make([]specid.ID, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
id, _ := specid.New(p)
|
||||
ret = append(ret, id)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func join[T fmt.Stringer](in []T, sep string) string {
|
||||
if in == nil {
|
||||
return ""
|
||||
}
|
||||
strs := make([]string, 0, len(in))
|
||||
for _, id := range in {
|
||||
strs = append(strs, id.String())
|
||||
}
|
||||
return strings.Join(strs, sep)
|
||||
}
|
||||
|
||||
@@ -11,28 +11,6 @@ import (
|
||||
"go.senan.xyz/gonic/jukebox"
|
||||
)
|
||||
|
||||
func newJukebox(tb testing.TB) *jukebox.Jukebox {
|
||||
tb.Helper()
|
||||
|
||||
sockPath := filepath.Join(tb.TempDir(), "mpv.sock")
|
||||
|
||||
j := jukebox.New()
|
||||
err := j.Start(
|
||||
sockPath,
|
||||
[]string{jukebox.MPVArg("--ao", "null")},
|
||||
)
|
||||
if errors.Is(err, jukebox.ErrMPVTooOld) {
|
||||
tb.Skip("old mpv found, skipping")
|
||||
}
|
||||
if err != nil {
|
||||
tb.Fatalf("start jukebox: %v", err)
|
||||
}
|
||||
tb.Cleanup(func() {
|
||||
j.Quit()
|
||||
})
|
||||
return j
|
||||
}
|
||||
|
||||
func TestPlaySkipReset(t *testing.T) {
|
||||
t.Skip("bit flakey currently")
|
||||
|
||||
@@ -187,6 +165,28 @@ func TestVolume(t *testing.T) {
|
||||
require.Equal(t, 0.0, vol)
|
||||
}
|
||||
|
||||
func newJukebox(tb testing.TB) *jukebox.Jukebox {
|
||||
tb.Helper()
|
||||
|
||||
sockPath := filepath.Join(tb.TempDir(), "mpv.sock")
|
||||
|
||||
j := jukebox.New()
|
||||
err := j.Start(
|
||||
sockPath,
|
||||
[]string{jukebox.MPVArg("--ao", "null")},
|
||||
)
|
||||
if errors.Is(err, jukebox.ErrMPVTooOld) {
|
||||
tb.Skip("old mpv found, skipping")
|
||||
}
|
||||
if err != nil {
|
||||
tb.Fatalf("start jukebox: %v", err)
|
||||
}
|
||||
tb.Cleanup(func() {
|
||||
j.Quit()
|
||||
})
|
||||
return j
|
||||
}
|
||||
|
||||
func testPath(path string) string {
|
||||
cwd, _ := os.Getwd()
|
||||
return filepath.Join(cwd, "testdata", path)
|
||||
|
||||
26
mime/mime.go
26
mime/mime.go
@@ -7,19 +7,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var supportedAudioTypes = map[string]string{
|
||||
".mp3": "audio/mpeg",
|
||||
".flac": "audio/x-flac",
|
||||
".aac": "audio/x-aac",
|
||||
".m4a": "audio/m4a",
|
||||
".m4b": "audio/m4b",
|
||||
".ogg": "audio/ogg",
|
||||
".opus": "audio/ogg",
|
||||
".wma": "audio/x-ms-wma",
|
||||
".wav": "audio/x-wav",
|
||||
".wv": "audio/x-wavpack",
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
for ext, mime := range supportedAudioTypes {
|
||||
@@ -41,3 +28,16 @@ func TypeByAudioExtension(ext string) string {
|
||||
}
|
||||
return stdmime.TypeByExtension(ext)
|
||||
}
|
||||
|
||||
var supportedAudioTypes = map[string]string{
|
||||
".mp3": "audio/mpeg",
|
||||
".flac": "audio/x-flac",
|
||||
".aac": "audio/x-aac",
|
||||
".m4a": "audio/m4a",
|
||||
".m4b": "audio/m4b",
|
||||
".ogg": "audio/ogg",
|
||||
".opus": "audio/ogg",
|
||||
".wma": "audio/x-ms-wma",
|
||||
".wav": "audio/x-wav",
|
||||
".wv": "audio/x-wavpack",
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ import (
|
||||
"go.senan.xyz/gonic/transcode"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
gonic.Version = ""
|
||||
log.SetOutput(io.Discard)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
var testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||
|
||||
const (
|
||||
@@ -156,9 +162,3 @@ func makec(tb testing.TB, roots []string, audio bool) *Controller {
|
||||
|
||||
return contr
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
gonic.Version = ""
|
||||
log.SetOutput(io.Discard)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
@@ -30,43 +30,46 @@ import (
|
||||
// b) return a non-nil spec.Response
|
||||
// _but not both_
|
||||
|
||||
func streamGetTransodePreference(dbc *db.DB, userID int, client string) (*db.TranscodePreference, error) {
|
||||
var pref db.TranscodePreference
|
||||
err := dbc.
|
||||
Where("user_id=?", userID).
|
||||
Where("client COLLATE NOCASE IN (?)", []string{"*", client}).
|
||||
Order("client DESC"). // ensure "*" is last if it's there
|
||||
First(&pref).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find transcode preference: %w", err)
|
||||
}
|
||||
return &pref, nil
|
||||
}
|
||||
|
||||
func streamGetTranscodeMeta(dbc *db.DB, userID int, client string) spec.TranscodeMeta {
|
||||
pref, _ := streamGetTransodePreference(dbc, userID, client)
|
||||
if pref == nil {
|
||||
return spec.TranscodeMeta{}
|
||||
}
|
||||
profile, ok := transcode.UserProfiles[pref.Profile]
|
||||
if !ok {
|
||||
return spec.TranscodeMeta{}
|
||||
}
|
||||
return spec.TranscodeMeta{
|
||||
TranscodedContentType: profile.MIME(),
|
||||
TranscodedSuffix: profile.Suffix(),
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
coverDefaultSize = 600
|
||||
coverCacheFormat = "png"
|
||||
)
|
||||
|
||||
func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
id, err := params.GetID("id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
size := params.GetOrInt("size", coverDefaultSize)
|
||||
cachePath := filepath.Join(
|
||||
c.cacheCoverPath,
|
||||
fmt.Sprintf("%s-%d.%s", id.String(), size, coverCacheFormat),
|
||||
)
|
||||
_, err = os.Stat(cachePath)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
reader, err := coverFor(c.dbc, c.artistInfoCache, id)
|
||||
if err != nil {
|
||||
return spec.NewError(10, "couldn't find cover `%s`: %v", id, err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if err := coverScaleAndSave(reader, cachePath, size); err != nil {
|
||||
log.Printf("error scaling cover: %v", err)
|
||||
return nil
|
||||
}
|
||||
case err != nil:
|
||||
log.Printf("error stating `%s`: %v", cachePath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
http.ServeFile(w, r, cachePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
errCoverNotFound = errors.New("could not find a cover with that id")
|
||||
errCoverEmpty = errors.New("no cover found")
|
||||
@@ -163,41 +166,6 @@ func coverScaleAndSave(reader io.Reader, cachePath string, size int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
id, err := params.GetID("id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
size := params.GetOrInt("size", coverDefaultSize)
|
||||
cachePath := filepath.Join(
|
||||
c.cacheCoverPath,
|
||||
fmt.Sprintf("%s-%d.%s", id.String(), size, coverCacheFormat),
|
||||
)
|
||||
_, err = os.Stat(cachePath)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
reader, err := coverFor(c.dbc, c.artistInfoCache, id)
|
||||
if err != nil {
|
||||
return spec.NewError(10, "couldn't find cover `%s`: %v", id, err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if err := coverScaleAndSave(reader, cachePath, size); err != nil {
|
||||
log.Printf("error scaling cover: %v", err)
|
||||
return nil
|
||||
}
|
||||
case err != nil:
|
||||
log.Printf("error stating `%s`: %v", cachePath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
http.ServeFile(w, r, cachePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
@@ -268,3 +236,35 @@ func (c *Controller) ServeGetAvatar(w http.ResponseWriter, r *http.Request) *spe
|
||||
http.ServeContent(w, r, "", time.Now(), bytes.NewReader(reqUser.Avatar))
|
||||
return nil
|
||||
}
|
||||
|
||||
func streamGetTransodePreference(dbc *db.DB, userID int, client string) (*db.TranscodePreference, error) {
|
||||
var pref db.TranscodePreference
|
||||
err := dbc.
|
||||
Where("user_id=?", userID).
|
||||
Where("client COLLATE NOCASE IN (?)", []string{"*", client}).
|
||||
Order("client DESC"). // ensure "*" is last if it's there
|
||||
First(&pref).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find transcode preference: %w", err)
|
||||
}
|
||||
return &pref, nil
|
||||
}
|
||||
|
||||
func streamGetTranscodeMeta(dbc *db.DB, userID int, client string) spec.TranscodeMeta {
|
||||
pref, _ := streamGetTransodePreference(dbc, userID, client)
|
||||
if pref == nil {
|
||||
return spec.TranscodeMeta{}
|
||||
}
|
||||
profile, ok := transcode.UserProfiles[pref.Profile]
|
||||
if !ok {
|
||||
return spec.TranscodeMeta{}
|
||||
}
|
||||
return spec.TranscodeMeta{
|
||||
TranscodedContentType: profile.MIME(),
|
||||
TranscodedSuffix: profile.Suffix(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,96 +37,6 @@ import (
|
||||
|
||||
var ErrNoValues = errors.New("no values provided")
|
||||
|
||||
// some thin wrappers
|
||||
// may be needed when cleaning up parse() below
|
||||
func parseStr(in string) (string, error) { return in, nil }
|
||||
func parseInt(in string) (int, error) { return strconv.Atoi(in) }
|
||||
func parseFloat(in string) (float64, error) { return strconv.ParseFloat(in, 64) }
|
||||
func parseID(in string) (specid.ID, error) { return specid.New(in) }
|
||||
func parseBool(in string) (bool, error) { return strconv.ParseBool(in) }
|
||||
|
||||
func parseTime(in string) (time.Time, error) {
|
||||
ms, err := strconv.Atoi(in)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
ns := int64(ms) * 1e6
|
||||
return time.Unix(0, ns), nil
|
||||
}
|
||||
|
||||
func parse(values []string, i interface{}) error {
|
||||
if len(values) == 0 {
|
||||
return ErrNoValues
|
||||
}
|
||||
var err error
|
||||
switch v := i.(type) {
|
||||
// *T
|
||||
case *string:
|
||||
*v, err = parseStr(values[0])
|
||||
case *int:
|
||||
*v, err = parseInt(values[0])
|
||||
case *float64:
|
||||
*v, err = parseFloat(values[0])
|
||||
case *specid.ID:
|
||||
*v, err = parseID(values[0])
|
||||
case *bool:
|
||||
*v, err = parseBool(values[0])
|
||||
case *time.Time:
|
||||
*v, err = parseTime(values[0])
|
||||
|
||||
// *[]T
|
||||
case *[]string:
|
||||
for _, value := range values {
|
||||
parsed, err := parseStr(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]int:
|
||||
for _, value := range values {
|
||||
parsed, err := parseInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]float64:
|
||||
for _, value := range values {
|
||||
parsed, err := parseFloat(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]specid.ID:
|
||||
for _, value := range values {
|
||||
parsed, err := parseID(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]bool:
|
||||
for _, value := range values {
|
||||
parsed, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]time.Time:
|
||||
for _, value := range values {
|
||||
parsed, err := parseTime(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type Params url.Values
|
||||
|
||||
func New(r *http.Request) Params {
|
||||
@@ -461,3 +371,93 @@ func (p Params) GetFirstOrTime(or time.Time, keys ...string) time.Time {
|
||||
}
|
||||
return or
|
||||
}
|
||||
|
||||
// some thin wrappers
|
||||
// may be needed when cleaning up parse() below
|
||||
func parseStr(in string) (string, error) { return in, nil }
|
||||
func parseInt(in string) (int, error) { return strconv.Atoi(in) }
|
||||
func parseFloat(in string) (float64, error) { return strconv.ParseFloat(in, 64) }
|
||||
func parseID(in string) (specid.ID, error) { return specid.New(in) }
|
||||
func parseBool(in string) (bool, error) { return strconv.ParseBool(in) }
|
||||
|
||||
func parseTime(in string) (time.Time, error) {
|
||||
ms, err := strconv.Atoi(in)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
ns := int64(ms) * 1e6
|
||||
return time.Unix(0, ns), nil
|
||||
}
|
||||
|
||||
func parse(values []string, i interface{}) error {
|
||||
if len(values) == 0 {
|
||||
return ErrNoValues
|
||||
}
|
||||
var err error
|
||||
switch v := i.(type) {
|
||||
// *T
|
||||
case *string:
|
||||
*v, err = parseStr(values[0])
|
||||
case *int:
|
||||
*v, err = parseInt(values[0])
|
||||
case *float64:
|
||||
*v, err = parseFloat(values[0])
|
||||
case *specid.ID:
|
||||
*v, err = parseID(values[0])
|
||||
case *bool:
|
||||
*v, err = parseBool(values[0])
|
||||
case *time.Time:
|
||||
*v, err = parseTime(values[0])
|
||||
|
||||
// *[]T
|
||||
case *[]string:
|
||||
for _, value := range values {
|
||||
parsed, err := parseStr(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]int:
|
||||
for _, value := range values {
|
||||
parsed, err := parseInt(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]float64:
|
||||
for _, value := range values {
|
||||
parsed, err := parseFloat(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]specid.ID:
|
||||
for _, value := range values {
|
||||
parsed, err := parseID(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]bool:
|
||||
for _, value := range values {
|
||||
parsed, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
case *[]time.Time:
|
||||
for _, value := range values {
|
||||
parsed, err := parseTime(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = append(*v, parsed)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user