put helpers last

This commit is contained in:
sentriz
2023-10-01 03:19:07 +01:00
parent e9accfb71f
commit ae82153d79
7 changed files with 232 additions and 235 deletions

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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",
}

View File

@@ -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())
}

View File

@@ -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(),
}
}

View File

@@ -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
}