diff --git a/go.sum b/go.sum index 5e28c02..4e5629d 100644 --- a/go.sum +++ b/go.sum @@ -208,6 +208,10 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b h1:mSUCVIwDx4hfXJfWsOPfdzEHxzb2Xjl6BQ8YgPnazQA= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index 9822fa1..68e4e14 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -50,7 +50,7 @@ func writeResp(w http.ResponseWriter, r *http.Request, resp *spec.Response) erro res := metaResponse{Response: resp} params := r.Context().Value(CtxParams).(params.Params) ew := &errWriter{w: w} - switch params.Get("f") { + switch v, _ := params.Get("f"); v { case "json": w.Header().Set("Content-Type", "application/json") data, err := json.Marshal(res) diff --git a/server/ctrlsubsonic/handlers_by_folder.go b/server/ctrlsubsonic/handlers_by_folder.go index b05561d..f303ca4 100644 --- a/server/ctrlsubsonic/handlers_by_folder.go +++ b/server/ctrlsubsonic/handlers_by_folder.go @@ -79,7 +79,7 @@ func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response { Find(&childTracks) for _, c := range childTracks { toAppend := spec.NewTCTrackByFolder(c, folder) - if params.Get("c") == "Jamstash" { + if v, _ := params.Get("c"); v == "Jamstash" { // jamstash thinks it can't play flacs toAppend.ContentType = "audio/mpeg" toAppend.Suffix = "mp3" diff --git a/server/ctrlsubsonic/params/params.go b/server/ctrlsubsonic/params/params.go index e786152..2742122 100644 --- a/server/ctrlsubsonic/params/params.go +++ b/server/ctrlsubsonic/params/params.go @@ -1,79 +1,262 @@ package params +// {get, get first, get or} * {str, int, id} * {list, not list} = 18 + import ( + "errors" "fmt" "net/http" "net/url" "strconv" + "strings" ) -type Params struct { - values url.Values +var ( + ErrKeyNotFound = errors.New("key(s) not found") + ErrIDInvalid = errors.New("invalid id") + ErrIDNotAnInt = errors.New("not an int") +) + +const IDSeparator = "-" + +type IDType string + +const ( + // type values copied from subsonic + IDTypeArtist IDType = "ar" + IDTypeAlbum IDType = "al" + IDTypeTrack IDType = "tr" +) + +type ID struct { + Type IDType + Value int } +func IDArtist(id int) string { return fmt.Sprintf("%d-%s", id, IDTypeArtist) } +func IDAlbum(id int) string { return fmt.Sprintf("%d-%s", id, IDTypeAlbum) } +func IDTrack(id int) string { return fmt.Sprintf("%d-%s", id, IDTypeTrack) } + +// ** begin type parsing, support {[],}{string,int,ID} => 6 types + +func parse(values []string, i interface{}) error { + if len(values) == 0 { + return ErrKeyNotFound + } + var err error + switch v := i.(type) { + case *string: + *v, err = parseStr(values[0]) + case *int: + *v, err = parseInt(values[0]) + case *ID: + *v, err = parseID(values[0]) + case *[]string: + for _, val := range values { + parsed, err := parseStr(val) + if err != nil { + return err + } + *v = append(*v, parsed) + } + case *[]int: + for _, val := range values { + parsed, err := parseInt(val) + if err != nil { + return err + } + *v = append(*v, parsed) + } + case *[]ID: + for _, val := range values { + parsed, err := parseID(val) + if err != nil { + return err + } + *v = append(*v, parsed) + } + } + return err +} + +// ** begin parse funcs + +func parseStr(in string) (string, error) { + return in, nil +} + +func parseInt(in string) (int, error) { + if v, err := strconv.Atoi(in); err == nil { + return v, nil + } + return 0, ErrIDNotAnInt +} + +func parseID(in string) (ID, error) { + parts := strings.Split(in, IDSeparator) + if len(parts) != 2 { + return ID{}, fmt.Errorf("bad separator: %w", ErrIDInvalid) + } + partType := parts[0] + partValue := parts[1] + val, err := parseInt(partValue) + if err != nil { + return ID{}, fmt.Errorf("%s: %w", partValue, err) + } + switch partType { + case string(IDTypeArtist): + return ID{Type: IDTypeArtist, Value: val}, nil + case string(IDTypeAlbum): + return ID{Type: IDTypeAlbum, Value: val}, nil + case string(IDTypeTrack): + return ID{Type: IDTypeTrack, Value: val}, nil + } + return ID{}, ErrIDInvalid +} + +type Params url.Values + func New(r *http.Request) Params { // first load params from the url params := r.URL.Query() // also if there's any in the post body, use those too - if err := r.ParseForm(); err != nil { - return Params{params} + if err := r.ParseForm(); err == nil { + for k, v := range r.Form { + params[k] = v + } } - for k, v := range r.Form { - params[k] = v - } - return Params{params} + return Params(params) } -func (p Params) Get(key string) string { - return p.values.Get(key) +func (p Params) get(key string) []string { + return p[key] } -func (p Params) GetOr(key, or string) string { - val := p.Get(key) - if val == "" { - return or - } - return val -} - -func (p Params) GetInt(key string) (int, error) { - strVal := p.values.Get(key) - if strVal == "" { - return 0, fmt.Errorf("no param with key `%s`", key) - } - val, err := strconv.Atoi(strVal) - if err != nil { - return 0, fmt.Errorf("not an int `%s`", strVal) - } - return val, nil -} - -func (p Params) GetIntOr(key string, or int) int { - val, err := p.GetInt(key) - if err != nil { - return or - } - return val -} - -func (p Params) GetFirstList(keys ...string) []string { - for _, key := range keys { - if v, ok := p.values[key]; ok && len(v) > 0 { +func (p Params) getFirst(keys []string) []string { + for _, k := range keys { + if v, ok := p[k]; ok { return v } } return nil } -func (p Params) GetFirstListInt(keys ...string) []int { - v := p.GetFirstList(keys...) - if v == nil { - return nil - } - ret := make([]int, 0, len(v)) - for _, p := range v { - i, _ := strconv.Atoi(p) - ret = append(ret, i) - } - return ret +// ** begin str {get, get first, get or} + +func (p Params) Get(key string) (string, error) { + var ret string + return ret, parse(p.get(key), &ret) +} + +func (p Params) GetFirst(keys ...string) (string, error) { + var ret string + return ret, parse(p.getFirst(keys), &ret) +} + +func (p Params) GetOr(key string, or string) string { + var ret string + if err := parse(p.get(key), &ret); err == nil { + return ret + } + return or +} + +// ** begin []str {get, get first, get or} + +func (p Params) GetList(key string) ([]string, error) { + var ret []string + return ret, parse(p.get(key), &ret) +} + +func (p Params) GetFirstList(keys ...string) ([]string, error) { + var ret []string + return ret, parse(p.getFirst(keys), &ret) +} + +func (p Params) GetOrList(key string, or []string) []string { + var ret []string + if err := parse(p.get(key), &ret); err == nil { + return ret + } + return or +} + +// ** begin int {get, get first, get or} + +func (p Params) GetInt(key string) (int, error) { + var ret int + return ret, parse(p.get(key), &ret) +} + +func (p Params) GetFirstInt(keys ...string) (int, error) { + var ret int + return ret, parse(p.getFirst(keys), &ret) +} + +func (p Params) GetOrInt(key string, or int) int { + var ret int + if err := parse(p.get(key), &ret); err == nil { + return ret + } + return or +} + +// ** begin []int {get, get first, get or} + +func (p Params) GetIntList(key string) ([]int, error) { + var ret []int + return ret, parse(p.get(key), &ret) +} + +func (p Params) GetFirstIntList(keys ...string) ([]int, error) { + var ret []int + return ret, parse(p.getFirst(keys), &ret) +} + +func (p Params) GetOrIntList(key string, or []int) []int { + var ret []int + if err := parse(p.get(key), &ret); err == nil { + return ret + } + return or +} + +// ** begin ID {get, get first, get or} + +func (p Params) GetID(key string) (ID, error) { + var ret ID + return ret, parse(p.get(key), &ret) +} + +func (p Params) GetFirstID(keys ...string) (ID, error) { + var ret ID + return ret, parse(p.getFirst(keys), &ret) +} + +func (p Params) GetOrID(key string, or ID) ID { + var ret ID + if err := parse(p.get(key), &ret); err == nil { + return ret + } + return or +} + +// ** begin []ID {get, get first, get or} + +func (p Params) GetIDList(key string) ([]ID, error) { + var ret []ID + return ret, parse(p.get(key), &ret) +} + +func (p Params) GetFirstIDList(keys ...string) ([]ID, error) { + var ret []ID + return ret, parse(p.getFirst(keys), &ret) +} + +func (p Params) GetOrIDList(key string, or []ID) []ID { + var ret []ID + if err := parse(p.get(key), &ret); err == nil { + return ret + } + return or } diff --git a/server/ctrlsubsonic/params/params_test.go b/server/ctrlsubsonic/params/params_test.go new file mode 100644 index 0000000..cede1ae --- /dev/null +++ b/server/ctrlsubsonic/params/params_test.go @@ -0,0 +1,55 @@ +package params + +import ( + "errors" + "testing" +) + +func TestParseID(t *testing.T) { + tcases := []struct { + param string + expType IDType + expValue int + expErr error + }{ + {param: "al-45", expType: IDTypeAlbum, expValue: 45}, + {param: "ar-2", expType: IDTypeArtist, expValue: 2}, + {param: "tr-43", expType: IDTypeTrack, expValue: 43}, + {param: "xx-1", expErr: ErrIDInvalid}, + {param: "al-howdy", expErr: ErrIDNotAnInt}, + } + for _, tcase := range tcases { + t.Run(tcase.param, func(t *testing.T) { + act, err := parseID(tcase.param) + if err != nil && !errors.Is(err, tcase.expErr) { + t.Fatalf("expected err %q, got %q", tcase.expErr, err) + } + if act.Value != tcase.expValue { + t.Errorf("expected value %d, got %d", tcase.expValue, act.Value) + } + if act.Type != tcase.expType { + t.Errorf("expected type %v, got %v", tcase.expType, act.Type) + } + }) + } +} + +// TODO? +func TestGet(t *testing.T) {} +func TestGetFirst(t *testing.T) {} +func TestGetOr(t *testing.T) {} +func TestGetList(t *testing.T) {} +func TestGetFirstList(t *testing.T) {} +func TestGetOrList(t *testing.T) {} +func TestGetInt(t *testing.T) {} +func TestGetFirstInt(t *testing.T) {} +func TestGetOrInt(t *testing.T) {} +func TestGetIntList(t *testing.T) {} +func TestGetFirstIntList(t *testing.T) {} +func TestGetOrIntList(t *testing.T) {} +func TestGetID(t *testing.T) {} +func TestGetFirstID(t *testing.T) {} +func TestGetOrID(t *testing.T) {} +func TestGetIDList(t *testing.T) {} +func TestGetFirstIDList(t *testing.T) {} +func TestGetOrIDList(t *testing.T) {}