put helpers last
This commit is contained in:
@@ -11,13 +11,9 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func randKey() string {
|
func TestMain(m *testing.M) {
|
||||||
letters := []rune("abcdef0123456789")
|
log.SetOutput(io.Discard)
|
||||||
b := make([]rune, 16)
|
os.Exit(m.Run())
|
||||||
for i := range b {
|
|
||||||
b[i] = letters[rand.Intn(len(letters))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetSetting(t *testing.T) {
|
func TestGetSetting(t *testing.T) {
|
||||||
@@ -46,7 +42,11 @@ func TestGetSetting(t *testing.T) {
|
|||||||
require.Equal(t, value, actual)
|
require.Equal(t, value, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func randKey() string {
|
||||||
log.SetOutput(io.Discard)
|
letters := []rune("abcdef0123456789")
|
||||||
os.Exit(m.Run())
|
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
|
//nolint:lll // struct tags get very long and can't be split
|
||||||
package db
|
package db
|
||||||
|
|
||||||
// see this db fiddle to mess around with the schema
|
|
||||||
// https://www.db-fiddle.com/f/wJ7z8L7mu6ZKaYmWk1xr1p/5
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -17,30 +14,6 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
"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 {
|
type Artist struct {
|
||||||
ID int `gorm:"primary_key"`
|
ID int `gorm:"primary_key"`
|
||||||
Name string `gorm:"not null; unique_index"`
|
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) GetTopTracks() []string { return strings.Split(p.TopTracks, ";") }
|
||||||
func (p *ArtistInfo) SetTopTracks(items []string) { p.TopTracks = strings.Join(items, ";") }
|
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"
|
"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) {
|
func TestPlaySkipReset(t *testing.T) {
|
||||||
t.Skip("bit flakey currently")
|
t.Skip("bit flakey currently")
|
||||||
|
|
||||||
@@ -187,6 +165,28 @@ func TestVolume(t *testing.T) {
|
|||||||
require.Equal(t, 0.0, vol)
|
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 {
|
func testPath(path string) string {
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
return filepath.Join(cwd, "testdata", path)
|
return filepath.Join(cwd, "testdata", path)
|
||||||
|
|||||||
26
mime/mime.go
26
mime/mime.go
@@ -7,19 +7,6 @@ import (
|
|||||||
"strings"
|
"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
|
//nolint:gochecknoinits
|
||||||
func init() {
|
func init() {
|
||||||
for ext, mime := range supportedAudioTypes {
|
for ext, mime := range supportedAudioTypes {
|
||||||
@@ -41,3 +28,16 @@ func TypeByAudioExtension(ext string) string {
|
|||||||
}
|
}
|
||||||
return stdmime.TypeByExtension(ext)
|
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"
|
"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])")
|
var testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -156,9 +162,3 @@ func makec(tb testing.TB, roots []string, audio bool) *Controller {
|
|||||||
|
|
||||||
return contr
|
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
|
// b) return a non-nil spec.Response
|
||||||
// _but not both_
|
// _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 (
|
const (
|
||||||
coverDefaultSize = 600
|
coverDefaultSize = 600
|
||||||
coverCacheFormat = "png"
|
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 (
|
var (
|
||||||
errCoverNotFound = errors.New("could not find a cover with that id")
|
errCoverNotFound = errors.New("could not find a cover with that id")
|
||||||
errCoverEmpty = errors.New("no cover found")
|
errCoverEmpty = errors.New("no cover found")
|
||||||
@@ -163,41 +166,6 @@ func coverScaleAndSave(reader io.Reader, cachePath string, size int) error {
|
|||||||
return nil
|
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 {
|
func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||||
params := r.Context().Value(CtxParams).(params.Params)
|
params := r.Context().Value(CtxParams).(params.Params)
|
||||||
user := r.Context().Value(CtxUser).(*db.User)
|
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))
|
http.ServeContent(w, r, "", time.Now(), bytes.NewReader(reqUser.Avatar))
|
||||||
return nil
|
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")
|
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
|
type Params url.Values
|
||||||
|
|
||||||
func New(r *http.Request) Params {
|
func New(r *http.Request) Params {
|
||||||
@@ -461,3 +371,93 @@ func (p Params) GetFirstOrTime(or time.Time, keys ...string) time.Time {
|
|||||||
}
|
}
|
||||||
return or
|
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