seperate routes, provide robust handler types, use mux
This commit is contained in:
98
server/ctrlsubsonic/ctrl.go
Normal file
98
server/ctrlsubsonic/ctrl.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"senan.xyz/g/gonic/server/ctrlbase"
|
||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||
"senan.xyz/g/gonic/server/parsing"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
*ctrlbase.Controller
|
||||
}
|
||||
|
||||
func New(base *ctrlbase.Controller) *Controller {
|
||||
return &Controller{
|
||||
Controller: base,
|
||||
}
|
||||
}
|
||||
|
||||
type metaResponse struct {
|
||||
XMLName xml.Name `xml:"subsonic-response" json:"-"`
|
||||
*spec.Response `json:"subsonic-response"`
|
||||
}
|
||||
|
||||
type errWriter struct {
|
||||
w io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
func (ew *errWriter) write(buf []byte) {
|
||||
if ew.err != nil {
|
||||
return
|
||||
}
|
||||
_, ew.err = ew.w.Write(buf)
|
||||
}
|
||||
|
||||
func writeResp(w http.ResponseWriter, r *http.Request, resp *spec.Response) {
|
||||
res := metaResponse{Response: resp}
|
||||
ew := &errWriter{w: w}
|
||||
switch parsing.GetStrParam(r, "f") {
|
||||
case "json":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to json: %v\n", err)
|
||||
return
|
||||
}
|
||||
ew.write(data)
|
||||
case "jsonp":
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to json: %v\n", err)
|
||||
return
|
||||
}
|
||||
ew.write([]byte(parsing.GetStrParamOr(r, "callback", "cb")))
|
||||
ew.write([]byte("("))
|
||||
ew.write(data)
|
||||
ew.write([]byte(");"))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
data, err := xml.MarshalIndent(res, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("could not marshall to xml: %v\n", err)
|
||||
return
|
||||
}
|
||||
ew.write(data)
|
||||
}
|
||||
if ew.err != nil {
|
||||
log.Printf("error writing to response: %v\n", ew.err)
|
||||
}
|
||||
}
|
||||
|
||||
type subsonicHandler func(r *http.Request) *spec.Response
|
||||
|
||||
func (c *Controller) H(h subsonicHandler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: write a non 200 if has err
|
||||
response := h(r)
|
||||
writeResp(w, r, response)
|
||||
})
|
||||
}
|
||||
|
||||
type subsonicHandlerRaw func(w http.ResponseWriter, r *http.Request) *spec.Response
|
||||
|
||||
func (c *Controller) HR(h subsonicHandlerRaw) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: write a non 200 if has err
|
||||
// TODO: ensure no mixed return/writer
|
||||
response := h(w, r)
|
||||
writeResp(w, r, response)
|
||||
})
|
||||
}
|
||||
86
server/ctrlsubsonic/ctrl_test.go
Normal file
86
server/ctrlsubsonic/ctrl_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
jd "github.com/josephburnett/jd/lib"
|
||||
|
||||
"senan.xyz/g/gonic/db"
|
||||
"senan.xyz/g/gonic/server/ctrlbase"
|
||||
)
|
||||
|
||||
var (
|
||||
testDataDir = "testdata"
|
||||
testCamelExpr = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||
testDBPath = path.Join(testDataDir, "db")
|
||||
testController *Controller
|
||||
)
|
||||
|
||||
func init() {
|
||||
db, err := db.New(testDBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening database: %v\n", err)
|
||||
}
|
||||
testController = New(&ctrlbase.Controller{DB: db})
|
||||
}
|
||||
|
||||
type queryCase struct {
|
||||
params url.Values
|
||||
expectPath string
|
||||
listSet bool
|
||||
}
|
||||
|
||||
func runQueryCases(t *testing.T, h subsonicHandler, cases []*queryCase) {
|
||||
for _, qc := range cases {
|
||||
qc := qc // pin
|
||||
t.Run(qc.expectPath, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
//
|
||||
// ensure the handlers give us json
|
||||
qc.params.Add("f", "json")
|
||||
//
|
||||
// request from the handler in question
|
||||
req, _ := http.NewRequest("", "?"+qc.params.Encode(), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
testController.H(h).ServeHTTP(rr, req)
|
||||
body := rr.Body.String()
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Fatalf("didn't give a 200\n%s", body)
|
||||
}
|
||||
//
|
||||
// convert test name to query case path
|
||||
snake := testCamelExpr.ReplaceAllString(t.Name(), "${1}_${2}")
|
||||
lower := strings.ToLower(snake)
|
||||
relPath := strings.Replace(lower, "/", "_", -1)
|
||||
absExpPath := path.Join(testDataDir, relPath)
|
||||
//
|
||||
// read case to differ with handler result
|
||||
expected, err := jd.ReadJsonFile(absExpPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing expected: %v", err)
|
||||
}
|
||||
actual, _ := jd.ReadJsonString(body)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing actual: %v", err)
|
||||
}
|
||||
diffOpts := []jd.Metadata{}
|
||||
if qc.listSet {
|
||||
diffOpts = append(diffOpts, jd.SET)
|
||||
}
|
||||
diff := expected.Diff(actual, diffOpts...)
|
||||
//
|
||||
// pass or fail
|
||||
if len(diff) == 0 {
|
||||
return
|
||||
}
|
||||
t.Errorf("\u001b[31;1mdiffering json\u001b[0m\n%s", diff.Render())
|
||||
})
|
||||
}
|
||||
}
|
||||
216
server/ctrlsubsonic/handlers_by_folder.go
Normal file
216
server/ctrlsubsonic/handlers_by_folder.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"senan.xyz/g/gonic/model"
|
||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||
"senan.xyz/g/gonic/server/key"
|
||||
"senan.xyz/g/gonic/server/parsing"
|
||||
)
|
||||
|
||||
// the subsonic spec metions "artist" a lot when talking about the
|
||||
// browse by folder endpoints. but since we're not browsing by tag
|
||||
// we can't access artists. so instead we'll consider the artist of
|
||||
// an track to be the it's respective folder that comes directly
|
||||
// under the root directory
|
||||
|
||||
func (c *Controller) ServeGetIndexes(r *http.Request) *spec.Response {
|
||||
var folders []*model.Album
|
||||
c.DB.
|
||||
Select("*, count(sub.id) as child_count").
|
||||
Joins(`
|
||||
LEFT JOIN albums sub
|
||||
ON albums.id = sub.parent_id
|
||||
`).
|
||||
Where("albums.parent_id = 1").
|
||||
Group("albums.id").
|
||||
Find(&folders)
|
||||
// [a-z#] -> 27
|
||||
indexMap := make(map[string]*spec.Index, 27)
|
||||
resp := make([]*spec.Index, 0, 27)
|
||||
for _, folder := range folders {
|
||||
i := lowerUDecOrHash(folder.IndexRightPath())
|
||||
index, ok := indexMap[i]
|
||||
if !ok {
|
||||
index = &spec.Index{
|
||||
Name: i,
|
||||
Artists: []*spec.Artist{},
|
||||
}
|
||||
indexMap[i] = index
|
||||
resp = append(resp, index)
|
||||
}
|
||||
index.Artists = append(index.Artists,
|
||||
spec.NewArtistByFolder(folder))
|
||||
}
|
||||
sort.Slice(resp, func(i, j int) bool {
|
||||
return resp[i].Name < resp[j].Name
|
||||
})
|
||||
sub := spec.NewResponse()
|
||||
sub.Indexes = &spec.Indexes{
|
||||
LastModified: 0,
|
||||
Index: resp,
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetMusicDirectory(r *http.Request) *spec.Response {
|
||||
id, err := parsing.GetIntParam(r, "id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
childrenObj := []*spec.TrackChild{}
|
||||
folder := &model.Album{}
|
||||
c.DB.First(folder, id)
|
||||
//
|
||||
// start looking for child childFolders in the current dir
|
||||
var childFolders []*model.Album
|
||||
c.DB.
|
||||
Where("parent_id = ?", id).
|
||||
Find(&childFolders)
|
||||
for _, c := range childFolders {
|
||||
childrenObj = append(childrenObj, spec.NewTCAlbumByFolder(c))
|
||||
}
|
||||
//
|
||||
// start looking for child childTracks in the current dir
|
||||
var childTracks []*model.Track
|
||||
c.DB.
|
||||
Where("album_id = ?", id).
|
||||
Preload("Album").
|
||||
Order("filename").
|
||||
Find(&childTracks)
|
||||
for _, c := range childTracks {
|
||||
toAppend := spec.NewTCTrackByFolder(c, folder)
|
||||
if parsing.GetStrParam(r, "c") == "Jamstash" {
|
||||
// jamstash thinks it can't play flacs
|
||||
toAppend.ContentType = "audio/mpeg"
|
||||
toAppend.Suffix = "mp3"
|
||||
}
|
||||
childrenObj = append(childrenObj, toAppend)
|
||||
}
|
||||
//
|
||||
// respond section
|
||||
sub := spec.NewResponse()
|
||||
sub.Directory = spec.NewDirectoryByFolder(folder, childrenObj)
|
||||
return sub
|
||||
}
|
||||
|
||||
// changes to this function should be reflected in in _by_tags.go's
|
||||
// getAlbumListTwo() function
|
||||
func (c *Controller) ServeGetAlbumList(r *http.Request) *spec.Response {
|
||||
listType := parsing.GetStrParam(r, "type")
|
||||
if listType == "" {
|
||||
return spec.NewError(10, "please provide a `type` parameter")
|
||||
}
|
||||
q := c.DB.DB
|
||||
switch listType {
|
||||
case "alphabeticalByArtist":
|
||||
q = q.Joins(`
|
||||
JOIN albums AS parent_albums
|
||||
ON albums.parent_id = parent_albums.id`)
|
||||
q = q.Order("parent_albums.right_path")
|
||||
case "alphabeticalByName":
|
||||
q = q.Order("right_path")
|
||||
case "frequent":
|
||||
user := r.Context().Value(key.User).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.count DESC")
|
||||
case "newest":
|
||||
q = q.Order("modified_at DESC")
|
||||
case "random":
|
||||
q = q.Order(gorm.Expr("random()"))
|
||||
case "recent":
|
||||
user := r.Context().Value(key.User).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.time DESC")
|
||||
default:
|
||||
return spec.NewError(10, "unknown value `%s` for parameter 'type'", listType)
|
||||
}
|
||||
var folders []*model.Album
|
||||
q.
|
||||
Where("albums.tag_artist_id IS NOT NULL").
|
||||
Offset(parsing.GetIntParamOr(r, "offset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "size", 10)).
|
||||
Preload("Parent").
|
||||
Find(&folders)
|
||||
sub := spec.NewResponse()
|
||||
sub.Albums = &spec.Albums{
|
||||
List: make([]*spec.Album, len(folders)),
|
||||
}
|
||||
for i, folder := range folders {
|
||||
sub.Albums.List[i] = spec.NewAlbumByFolder(folder)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeSearchTwo(r *http.Request) *spec.Response {
|
||||
query := parsing.GetStrParam(r, "query")
|
||||
if query == "" {
|
||||
return spec.NewError(10, "please provide a `query` parameter")
|
||||
}
|
||||
query = fmt.Sprintf("%%%s%%",
|
||||
strings.TrimSuffix(query, "*"))
|
||||
results := &spec.SearchResultTwo{}
|
||||
//
|
||||
// search "artists"
|
||||
var artists []*model.Album
|
||||
c.DB.
|
||||
Where(`
|
||||
parent_id = 1
|
||||
AND (right_path LIKE ? OR
|
||||
right_path_u_dec LIKE ?)
|
||||
`, query, query).
|
||||
Offset(parsing.GetIntParamOr(r, "artistOffset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "artistCount", 20)).
|
||||
Find(&artists)
|
||||
for _, a := range artists {
|
||||
results.Artists = append(results.Artists,
|
||||
spec.NewDirectoryByFolder(a, nil))
|
||||
}
|
||||
//
|
||||
// search "albums"
|
||||
var albums []*model.Album
|
||||
c.DB.
|
||||
Where(`
|
||||
tag_artist_id IS NOT NULL
|
||||
AND (right_path LIKE ? OR
|
||||
right_path_u_dec LIKE ?)
|
||||
`, query, query).
|
||||
Offset(parsing.GetIntParamOr(r, "albumOffset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "albumCount", 20)).
|
||||
Find(&albums)
|
||||
for _, a := range albums {
|
||||
results.Albums = append(results.Albums, spec.NewTCAlbumByFolder(a))
|
||||
}
|
||||
//
|
||||
// search tracks
|
||||
var tracks []*model.Track
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Where(`
|
||||
filename LIKE ? OR
|
||||
filename_u_dec LIKE ?
|
||||
`, query, query).
|
||||
Offset(parsing.GetIntParamOr(r, "songOffset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "songCount", 20)).
|
||||
Find(&tracks)
|
||||
for _, t := range tracks {
|
||||
results.Tracks = append(results.Tracks,
|
||||
spec.NewTCTrackByFolder(t, t.Album))
|
||||
}
|
||||
//
|
||||
sub := spec.NewResponse()
|
||||
sub.SearchResultTwo = results
|
||||
return sub
|
||||
}
|
||||
38
server/ctrlsubsonic/handlers_by_folder_test.go
Normal file
38
server/ctrlsubsonic/handlers_by_folder_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
)
|
||||
|
||||
func TestGetIndexes(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeGetIndexes, []*queryCase{
|
||||
{url.Values{}, "no_args", false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMusicDirectory(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeGetMusicDirectory, []*queryCase{
|
||||
{url.Values{"id": []string{"2"}}, "without_tracks", false},
|
||||
{url.Values{"id": []string{"3"}}, "with_tracks", false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAlbumList(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeGetAlbumList, []*queryCase{
|
||||
{url.Values{"type": []string{"alphabeticalByArtist"}}, "alpha_artist", false},
|
||||
{url.Values{"type": []string{"alphabeticalByName"}}, "alpha_name", false},
|
||||
{url.Values{"type": []string{"newest"}}, "newest", false},
|
||||
{url.Values{"type": []string{"random"}, "size": []string{"15"}}, "random", true},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchTwo(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeSearchTwo, []*queryCase{
|
||||
{url.Values{"query": []string{"13"}}, "q_13", false},
|
||||
{url.Values{"query": []string{"ani"}}, "q_ani", false},
|
||||
{url.Values{"query": []string{"cert"}}, "q_cert", false},
|
||||
})
|
||||
}
|
||||
215
server/ctrlsubsonic/handlers_by_tags.go
Normal file
215
server/ctrlsubsonic/handlers_by_tags.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"senan.xyz/g/gonic/model"
|
||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||
"senan.xyz/g/gonic/server/key"
|
||||
"senan.xyz/g/gonic/server/parsing"
|
||||
)
|
||||
|
||||
func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
|
||||
var artists []*model.Artist
|
||||
c.DB.
|
||||
Select("*, count(sub.id) as album_count").
|
||||
Joins(`
|
||||
LEFT JOIN albums sub
|
||||
ON artists.id = sub.tag_artist_id
|
||||
`).
|
||||
Group("artists.id").
|
||||
Find(&artists)
|
||||
// [a-z#] -> 27
|
||||
indexMap := make(map[string]*spec.Index, 27)
|
||||
resp := make([]*spec.Index, 0, 27)
|
||||
for _, artist := range artists {
|
||||
i := lowerUDecOrHash(artist.IndexName())
|
||||
index, ok := indexMap[i]
|
||||
if !ok {
|
||||
index = &spec.Index{
|
||||
Name: i,
|
||||
Artists: []*spec.Artist{},
|
||||
}
|
||||
indexMap[i] = index
|
||||
resp = append(resp, index)
|
||||
}
|
||||
index.Artists = append(index.Artists,
|
||||
spec.NewArtistByTags(artist))
|
||||
}
|
||||
sort.Slice(resp, func(i, j int) bool {
|
||||
return resp[i].Name < resp[j].Name
|
||||
})
|
||||
sub := spec.NewResponse()
|
||||
sub.Artists = &spec.Artists{
|
||||
List: resp,
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetArtist(r *http.Request) *spec.Response {
|
||||
id, err := parsing.GetIntParam(r, "id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
artist := &model.Artist{}
|
||||
c.DB.
|
||||
Preload("Albums").
|
||||
First(artist, id)
|
||||
sub := spec.NewResponse()
|
||||
sub.Artist = spec.NewArtistByTags(artist)
|
||||
sub.Artist.Albums = make([]*spec.Album, len(artist.Albums))
|
||||
for i, album := range artist.Albums {
|
||||
sub.Artist.Albums[i] = spec.NewAlbumByTags(album, artist)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetAlbum(r *http.Request) *spec.Response {
|
||||
id, err := parsing.GetIntParam(r, "id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
album := &model.Album{}
|
||||
err = c.DB.
|
||||
Preload("TagArtist").
|
||||
Preload("Tracks", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("tracks.tag_disc_number, tracks.tag_track_number")
|
||||
}).
|
||||
First(album, id).
|
||||
Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return spec.NewError(10, "couldn't find an album with that id")
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
sub.Album = spec.NewAlbumByTags(album, album.TagArtist)
|
||||
sub.Album.Tracks = make([]*spec.TrackChild, len(album.Tracks))
|
||||
for i, track := range album.Tracks {
|
||||
sub.Album.Tracks[i] = spec.NewTrackByTags(track, album)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// changes to this function should be reflected in in _by_folder.go's
|
||||
// getAlbumList() function
|
||||
func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
||||
listType := parsing.GetStrParam(r, "type")
|
||||
if listType == "" {
|
||||
return spec.NewError(10, "please provide a `type` parameter")
|
||||
}
|
||||
q := c.DB.DB
|
||||
switch listType {
|
||||
case "alphabeticalByArtist":
|
||||
q = q.Joins(`
|
||||
JOIN artists
|
||||
ON albums.tag_artist_id = artists.id`)
|
||||
q = q.Order("artists.name")
|
||||
case "alphabeticalByName":
|
||||
q = q.Order("tag_title")
|
||||
case "byYear":
|
||||
q = q.Where(
|
||||
"tag_year BETWEEN ? AND ?",
|
||||
parsing.GetIntParamOr(r, "fromYear", 1800),
|
||||
parsing.GetIntParamOr(r, "toYear", 2200))
|
||||
q = q.Order("tag_year")
|
||||
case "frequent":
|
||||
user := r.Context().Value(key.User).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.count DESC")
|
||||
case "newest":
|
||||
q = q.Order("modified_at DESC")
|
||||
case "random":
|
||||
q = q.Order(gorm.Expr("random()"))
|
||||
case "recent":
|
||||
user := r.Context().Value(key.User).(*model.User)
|
||||
q = q.Joins(`
|
||||
JOIN plays
|
||||
ON albums.id = plays.album_id AND plays.user_id = ?`,
|
||||
user.ID)
|
||||
q = q.Order("plays.time DESC")
|
||||
default:
|
||||
return spec.NewError(10, "unknown value `%s` for parameter 'type'", listType)
|
||||
}
|
||||
var albums []*model.Album
|
||||
q.
|
||||
Where("albums.tag_artist_id IS NOT NULL").
|
||||
Offset(parsing.GetIntParamOr(r, "offset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "size", 10)).
|
||||
Preload("TagArtist").
|
||||
Find(&albums)
|
||||
sub := spec.NewResponse()
|
||||
sub.AlbumsTwo = &spec.Albums{
|
||||
List: make([]*spec.Album, len(albums)),
|
||||
}
|
||||
for i, album := range albums {
|
||||
sub.AlbumsTwo.List[i] = spec.NewAlbumByTags(album, album.TagArtist)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeSearchThree(r *http.Request) *spec.Response {
|
||||
query := parsing.GetStrParam(r, "query")
|
||||
if query == "" {
|
||||
return spec.NewError(10, "please provide a `query` parameter")
|
||||
}
|
||||
query = fmt.Sprintf("%%%s%%",
|
||||
strings.TrimSuffix(query, "*"))
|
||||
results := &spec.SearchResultThree{}
|
||||
//
|
||||
// search "artists"
|
||||
var artists []*model.Artist
|
||||
c.DB.
|
||||
Where(`
|
||||
name LIKE ? OR
|
||||
name_u_dec LIKE ?
|
||||
`, query, query).
|
||||
Offset(parsing.GetIntParamOr(r, "artistOffset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "artistCount", 20)).
|
||||
Find(&artists)
|
||||
for _, a := range artists {
|
||||
results.Artists = append(results.Artists,
|
||||
spec.NewArtistByTags(a))
|
||||
}
|
||||
//
|
||||
// search "albums"
|
||||
var albums []*model.Album
|
||||
c.DB.
|
||||
Preload("TagArtist").
|
||||
Where(`
|
||||
tag_title LIKE ? OR
|
||||
tag_title_u_dec LIKE ?
|
||||
`, query, query).
|
||||
Offset(parsing.GetIntParamOr(r, "albumOffset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "albumCount", 20)).
|
||||
Find(&albums)
|
||||
for _, a := range albums {
|
||||
results.Albums = append(results.Albums,
|
||||
spec.NewAlbumByTags(a, a.TagArtist))
|
||||
}
|
||||
//
|
||||
// search tracks
|
||||
var tracks []*model.Track
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Where(`
|
||||
tag_title LIKE ? OR
|
||||
tag_title_u_dec LIKE ?
|
||||
`, query, query).
|
||||
Offset(parsing.GetIntParamOr(r, "songOffset", 0)).
|
||||
Limit(parsing.GetIntParamOr(r, "songCount", 20)).
|
||||
Find(&tracks)
|
||||
for _, t := range tracks {
|
||||
results.Tracks = append(results.Tracks,
|
||||
spec.NewTrackByTags(t, t.Album))
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
sub.SearchResultThree = results
|
||||
return sub
|
||||
}
|
||||
44
server/ctrlsubsonic/handlers_by_tags_test.go
Normal file
44
server/ctrlsubsonic/handlers_by_tags_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetArtists(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeGetArtists, []*queryCase{
|
||||
{url.Values{}, "no_args", false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetArtist(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeGetArtist, []*queryCase{
|
||||
{url.Values{"id": []string{"1"}}, "id_one", false},
|
||||
{url.Values{"id": []string{"2"}}, "id_two", false},
|
||||
{url.Values{"id": []string{"3"}}, "id_three", false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAlbum(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeGetAlbum, []*queryCase{
|
||||
{url.Values{"id": []string{"2"}}, "without_cover", false},
|
||||
{url.Values{"id": []string{"3"}}, "with_cover", false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAlbumListTwo(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeGetAlbumListTwo, []*queryCase{
|
||||
{url.Values{"type": []string{"alphabeticalByArtist"}}, "alpha_artist", false},
|
||||
{url.Values{"type": []string{"alphabeticalByName"}}, "alpha_name", false},
|
||||
{url.Values{"type": []string{"newest"}}, "newest", false},
|
||||
{url.Values{"type": []string{"random"}, "size": []string{"15"}}, "random", true},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchThree(t *testing.T) {
|
||||
runQueryCases(t, testController.ServeSearchThree, []*queryCase{
|
||||
{url.Values{"query": []string{"13"}}, "q_13", false},
|
||||
{url.Values{"query": []string{"ani"}}, "q_ani", false},
|
||||
{url.Values{"query": []string{"cert"}}, "q_cert", false},
|
||||
})
|
||||
}
|
||||
107
server/ctrlsubsonic/handlers_common.go
Normal file
107
server/ctrlsubsonic/handlers_common.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"senan.xyz/g/gonic/model"
|
||||
"senan.xyz/g/gonic/scanner"
|
||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||
"senan.xyz/g/gonic/server/key"
|
||||
"senan.xyz/g/gonic/server/lastfm"
|
||||
"senan.xyz/g/gonic/server/parsing"
|
||||
)
|
||||
|
||||
func lowerUDecOrHash(in string) string {
|
||||
lower := unicode.ToLower(rune(in[0]))
|
||||
if !unicode.IsLetter(lower) {
|
||||
return "#"
|
||||
}
|
||||
return string(lower)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetLicence(r *http.Request) *spec.Response {
|
||||
sub := spec.NewResponse()
|
||||
sub.Licence = &spec.Licence{
|
||||
Valid: true,
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServePing(r *http.Request) *spec.Response {
|
||||
return spec.NewResponse()
|
||||
}
|
||||
|
||||
func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
|
||||
id, err := parsing.GetIntParam(r, "id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
// fetch user to get lastfm session
|
||||
user := r.Context().Value(key.User).(*model.User)
|
||||
if user.LastFMSession == "" {
|
||||
return spec.NewError(0, "you don't have a last.fm session")
|
||||
}
|
||||
// fetch track for getting info to send to last.fm function
|
||||
track := &model.Track{}
|
||||
c.DB.
|
||||
Preload("Album").
|
||||
Preload("Artist").
|
||||
First(track, id)
|
||||
// scrobble with above info
|
||||
err = lastfm.Scrobble(
|
||||
c.DB.GetSetting("lastfm_api_key"),
|
||||
c.DB.GetSetting("lastfm_secret"),
|
||||
user.LastFMSession,
|
||||
track,
|
||||
// clients will provide time in miliseconds, so use that or
|
||||
// instead convert UnixNano to miliseconds
|
||||
parsing.GetIntParamOr(r, "time", int(time.Now().UnixNano()/1e6)),
|
||||
parsing.GetStrParamOr(r, "submission", "true") != "false",
|
||||
)
|
||||
if err != nil {
|
||||
return spec.NewError(0, "error when submitting: %v", err)
|
||||
}
|
||||
return spec.NewResponse()
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetMusicFolders(r *http.Request) *spec.Response {
|
||||
folders := &spec.MusicFolders{}
|
||||
folders.List = []*spec.MusicFolder{
|
||||
{ID: 1, Name: "music"},
|
||||
}
|
||||
sub := spec.NewResponse()
|
||||
sub.MusicFolders = folders
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeStartScan(r *http.Request) *spec.Response {
|
||||
go func() {
|
||||
err := scanner.
|
||||
New(c.DB, c.MusicPath).
|
||||
Start()
|
||||
if err != nil {
|
||||
log.Printf("error while scanning: %v\n", err)
|
||||
}
|
||||
}()
|
||||
return c.ServeGetScanStatus(r)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetScanStatus(r *http.Request) *spec.Response {
|
||||
var trackCount int
|
||||
c.DB.
|
||||
Model(model.Track{}).
|
||||
Count(&trackCount)
|
||||
sub := spec.NewResponse()
|
||||
sub.ScanStatus = &spec.ScanStatus{
|
||||
Scanning: scanner.IsScanning(),
|
||||
Count: trackCount,
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeNotFound(r *http.Request) *spec.Response {
|
||||
return spec.NewError(70, "view not found")
|
||||
}
|
||||
88
server/ctrlsubsonic/handlers_raw.go
Normal file
88
server/ctrlsubsonic/handlers_raw.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"senan.xyz/g/gonic/model"
|
||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||
"senan.xyz/g/gonic/server/key"
|
||||
"senan.xyz/g/gonic/server/parsing"
|
||||
)
|
||||
|
||||
// "raw" handlers are ones that don't always return a spec response.
|
||||
// it could be a file, stream, etc. so you must either
|
||||
// a) write to response writer
|
||||
// b) return a non-nil spec.Response
|
||||
// _but not both_
|
||||
|
||||
func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||
id, err := parsing.GetIntParam(r, "id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
folder := &model.Album{}
|
||||
err = c.DB.
|
||||
Select("id, left_path, right_path, cover").
|
||||
First(folder, id).
|
||||
Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return spec.NewError(10, "could not find a cover with that id")
|
||||
}
|
||||
if folder.Cover == "" {
|
||||
return spec.NewError(10, "no cover found for that folder")
|
||||
}
|
||||
absPath := path.Join(
|
||||
c.MusicPath,
|
||||
folder.LeftPath,
|
||||
folder.RightPath,
|
||||
folder.Cover,
|
||||
)
|
||||
http.ServeFile(w, r, absPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||
id, err := parsing.GetIntParam(r, "id")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `id` parameter")
|
||||
}
|
||||
track := &model.Track{}
|
||||
err = c.DB.
|
||||
Preload("Album").
|
||||
First(track, id).
|
||||
Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return spec.NewError(70, "media with id `%d` was not found", id)
|
||||
}
|
||||
absPath := path.Join(
|
||||
c.MusicPath,
|
||||
track.Album.LeftPath,
|
||||
track.Album.RightPath,
|
||||
track.Filename,
|
||||
)
|
||||
file, err := os.Open(absPath)
|
||||
if err != nil {
|
||||
return spec.NewError(0, "error while streaming media: %v", err)
|
||||
}
|
||||
stat, _ := file.Stat()
|
||||
http.ServeContent(w, r, absPath, stat.ModTime(), file)
|
||||
//
|
||||
// after we've served the file, mark the album as played
|
||||
user := r.Context().Value(key.User).(*model.User)
|
||||
play := model.Play{
|
||||
AlbumID: track.Album.ID,
|
||||
UserID: user.ID,
|
||||
}
|
||||
c.DB.
|
||||
Where(play).
|
||||
First(&play)
|
||||
play.Time = time.Now() // for getAlbumList?type=recent
|
||||
play.Count++ // for getAlbumList?type=frequent
|
||||
c.DB.Save(&play)
|
||||
return nil
|
||||
}
|
||||
80
server/ctrlsubsonic/middleware.go
Normal file
80
server/ctrlsubsonic/middleware.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
||||
"senan.xyz/g/gonic/server/key"
|
||||
"senan.xyz/g/gonic/server/parsing"
|
||||
)
|
||||
|
||||
var requiredParameters = []string{
|
||||
"u", "v", "c",
|
||||
}
|
||||
|
||||
func checkHasAllParams(params url.Values) error {
|
||||
for _, req := range requiredParameters {
|
||||
param := params.Get(req)
|
||||
if param != "" {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("please provide a `%s` parameter", req)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCredsToken(password, token, salt string) bool {
|
||||
toHash := fmt.Sprintf("%s%s", password, salt)
|
||||
hash := md5.Sum([]byte(toHash))
|
||||
expToken := hex.EncodeToString(hash[:])
|
||||
return token == expToken
|
||||
}
|
||||
|
||||
func checkCredsBasic(password, given string) bool {
|
||||
if len(given) >= 4 && given[:4] == "enc:" {
|
||||
bytes, _ := hex.DecodeString(given[4:])
|
||||
given = string(bytes)
|
||||
}
|
||||
return password == given
|
||||
}
|
||||
|
||||
func (c *Controller) WithValidSubsonicArgs(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := checkHasAllParams(r.URL.Query()); err != nil {
|
||||
writeResp(w, r, spec.NewError(10, err.Error()))
|
||||
return
|
||||
}
|
||||
username := parsing.GetStrParam(r, "u")
|
||||
password := parsing.GetStrParam(r, "p")
|
||||
token := parsing.GetStrParam(r, "t")
|
||||
salt := parsing.GetStrParam(r, "s")
|
||||
passwordAuth := token == "" && salt == ""
|
||||
tokenAuth := password == ""
|
||||
if tokenAuth == passwordAuth {
|
||||
writeResp(w, r, spec.NewError(10, "please provide `t` and `s`, or just `p`"))
|
||||
return
|
||||
}
|
||||
user := c.DB.GetUserFromName(username)
|
||||
if user == nil {
|
||||
writeResp(w, r, spec.NewError(40, "invalid username `%s`", username))
|
||||
return
|
||||
}
|
||||
var credsOk bool
|
||||
if tokenAuth {
|
||||
credsOk = checkCredsToken(user.Password, token, salt)
|
||||
} else {
|
||||
credsOk = checkCredsBasic(user.Password, password)
|
||||
}
|
||||
if !credsOk {
|
||||
writeResp(w, r, spec.NewError(40, "invalid password"))
|
||||
return
|
||||
}
|
||||
withUser := context.WithValue(r.Context(), key.User, user)
|
||||
next.ServeHTTP(w, r.WithContext(withUser))
|
||||
})
|
||||
}
|
||||
76
server/ctrlsubsonic/spec/construct_by_folder.go
Normal file
76
server/ctrlsubsonic/spec/construct_by_folder.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"senan.xyz/g/gonic/model"
|
||||
)
|
||||
|
||||
func NewAlbumByFolder(f *model.Album) *Album {
|
||||
return &Album{
|
||||
Artist: f.Parent.RightPath,
|
||||
CoverID: f.ID,
|
||||
ID: f.ID,
|
||||
IsDir: true,
|
||||
ParentID: f.ParentID,
|
||||
Title: f.RightPath,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTCAlbumByFolder(f *model.Album) *TrackChild {
|
||||
trCh := &TrackChild{
|
||||
ID: f.ID,
|
||||
IsDir: true,
|
||||
Title: f.RightPath,
|
||||
ParentID: f.ParentID,
|
||||
}
|
||||
if f.Cover != "" {
|
||||
trCh.CoverID = f.ID
|
||||
}
|
||||
return trCh
|
||||
}
|
||||
|
||||
func NewTCTrackByFolder(t *model.Track, parent *model.Album) *TrackChild {
|
||||
trCh := &TrackChild{
|
||||
ID: t.ID,
|
||||
Album: t.Album.RightPath,
|
||||
ContentType: t.MIME(),
|
||||
Suffix: t.Ext(),
|
||||
Size: t.Size,
|
||||
Artist: t.TagTrackArtist,
|
||||
Title: t.TagTitle,
|
||||
TrackNumber: t.TagTrackNumber,
|
||||
DiscNumber: t.TagDiscNumber,
|
||||
Path: path.Join(
|
||||
parent.LeftPath,
|
||||
parent.RightPath,
|
||||
t.Filename,
|
||||
),
|
||||
ParentID: parent.ID,
|
||||
Duration: t.Length,
|
||||
Bitrate: t.Bitrate,
|
||||
IsDir: false,
|
||||
Type: "music",
|
||||
}
|
||||
if parent.Cover != "" {
|
||||
trCh.CoverID = parent.ID
|
||||
}
|
||||
return trCh
|
||||
}
|
||||
|
||||
func NewArtistByFolder(f *model.Album) *Artist {
|
||||
return &Artist{
|
||||
ID: f.ID,
|
||||
Name: f.RightPath,
|
||||
AlbumCount: f.ChildCount,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDirectoryByFolder(f *model.Album, children []*TrackChild) *Directory {
|
||||
return &Directory{
|
||||
ID: f.ID,
|
||||
Parent: f.ParentID,
|
||||
Name: f.RightPath,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
61
server/ctrlsubsonic/spec/construct_by_tags.go
Normal file
61
server/ctrlsubsonic/spec/construct_by_tags.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"senan.xyz/g/gonic/model"
|
||||
)
|
||||
|
||||
func NewAlbumByTags(a *model.Album, artist *model.Artist) *Album {
|
||||
ret := &Album{
|
||||
Created: a.ModifiedAt,
|
||||
ID: a.ID,
|
||||
Name: a.TagTitle,
|
||||
}
|
||||
if a.Cover != "" {
|
||||
ret.CoverID = a.ID
|
||||
}
|
||||
if artist != nil {
|
||||
ret.Artist = artist.Name
|
||||
ret.ArtistID = artist.ID
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func NewTrackByTags(t *model.Track, album *model.Album) *TrackChild {
|
||||
ret := &TrackChild{
|
||||
ID: t.ID,
|
||||
ContentType: t.MIME(),
|
||||
Suffix: t.Ext(),
|
||||
ParentID: t.AlbumID,
|
||||
CreatedAt: t.CreatedAt,
|
||||
Size: t.Size,
|
||||
Title: t.TagTitle,
|
||||
Artist: t.TagTrackArtist,
|
||||
TrackNumber: t.TagTrackNumber,
|
||||
DiscNumber: t.TagDiscNumber,
|
||||
Path: path.Join(
|
||||
album.LeftPath,
|
||||
album.RightPath,
|
||||
t.Filename,
|
||||
),
|
||||
Album: album.TagTitle,
|
||||
AlbumID: album.ID,
|
||||
ArtistID: album.TagArtist.ID,
|
||||
Duration: t.Length,
|
||||
Bitrate: t.Bitrate,
|
||||
Type: "music",
|
||||
}
|
||||
if album.Cover != "" {
|
||||
ret.CoverID = album.ID
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func NewArtistByTags(a *model.Artist) *Artist {
|
||||
return &Artist{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
AlbumCount: a.AlbumCount,
|
||||
}
|
||||
}
|
||||
179
server/ctrlsubsonic/spec/spec.go
Normal file
179
server/ctrlsubsonic/spec/spec.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
apiVersion = "1.9.0"
|
||||
xmlns = "http://subsonic.org/restapi"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Status string `xml:"status,attr" json:"status"`
|
||||
Version string `xml:"version,attr" json:"version"`
|
||||
XMLNS string `xml:"xmlns,attr" json:"-"`
|
||||
Error *Error `xml:"error" json:"error,omitempty"`
|
||||
Albums *Albums `xml:"albumList" json:"albumList,omitempty"`
|
||||
AlbumsTwo *Albums `xml:"albumList2" json:"albumList2,omitempty"`
|
||||
Album *Album `xml:"album" json:"album,omitempty"`
|
||||
Track *TrackChild `xml:"song" json:"song,omitempty"`
|
||||
Indexes *Indexes `xml:"indexes" json:"indexes,omitempty"`
|
||||
Artists *Artists `xml:"artists" json:"artists,omitempty"`
|
||||
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
||||
Directory *Directory `xml:"directory" json:"directory,omitempty"`
|
||||
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
|
||||
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
|
||||
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
|
||||
Licence *Licence `xml:"license" json:"license,omitempty"`
|
||||
SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"`
|
||||
SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"`
|
||||
}
|
||||
|
||||
func NewResponse() *Response {
|
||||
return &Response{
|
||||
Status: "ok",
|
||||
XMLNS: xmlns,
|
||||
Version: apiVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// spec errors:
|
||||
// 0 a generic error
|
||||
// 10 required parameter is missing
|
||||
// 20 incompatible subsonic rest protocol version. client must upgrade
|
||||
// 30 incompatible subsonic rest protocol version. server must upgrade
|
||||
// 40 wrong username or password
|
||||
// 41 token authentication not supported for ldap users
|
||||
// 50 user is not authorized for the given operation
|
||||
// 60 the trial period for the subsonic server is over
|
||||
// 70 the requested data was not found
|
||||
|
||||
type Error struct {
|
||||
Code int `xml:"code,attr" json:"code"`
|
||||
Message string `xml:"message,attr" json:"message"`
|
||||
}
|
||||
|
||||
func NewError(code int, message string, a ...interface{}) *Response {
|
||||
return &Response{
|
||||
Status: "failed",
|
||||
XMLNS: xmlns,
|
||||
Version: apiVersion,
|
||||
Error: &Error{
|
||||
Code: code,
|
||||
Message: fmt.Sprintf(message, a...),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Albums struct {
|
||||
List []*Album `xml:"album" json:"album,omitempty"`
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
// common
|
||||
ID int `xml:"id,attr,omitempty" json:"id"`
|
||||
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
ArtistID int `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
// browsing by folder (getAlbumList)
|
||||
Title string `xml:"title,attr,omitempty" json:"title,omitempty"`
|
||||
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
||||
ParentID int `xml:"parent,attr,omitempty" json:"parent,omitempty"`
|
||||
IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"`
|
||||
// browsing by tags (getAlbumList2)
|
||||
Name string `xml:"name,attr,omitempty" json:"name,omitempty"`
|
||||
TrackCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
|
||||
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
|
||||
Created time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
|
||||
Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"`
|
||||
}
|
||||
|
||||
type RandomTracks struct {
|
||||
Tracks []*TrackChild `xml:"song" json:"song"`
|
||||
}
|
||||
|
||||
type TrackChild struct {
|
||||
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
||||
AlbumID int `xml:"albumId,attr,omitempty" json:"albumId,omitempty"`
|
||||
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
|
||||
ArtistID int `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
|
||||
Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate,omitempty"`
|
||||
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
|
||||
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
|
||||
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
|
||||
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
|
||||
ID int `xml:"id,attr,omitempty" json:"id,omitempty"`
|
||||
IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"`
|
||||
IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo,omitempty"`
|
||||
ParentID int `xml:"parent,attr,omitempty" json:"parent,omitempty"`
|
||||
Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
|
||||
Size int `xml:"size,attr,omitempty" json:"size,omitempty"`
|
||||
Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
|
||||
Title string `xml:"title,attr,omitempty" json:"title,omitempty"`
|
||||
TrackNumber int `xml:"track,attr,omitempty" json:"track,omitempty"`
|
||||
DiscNumber int `xml:"discNumber,attr,omitempty" json:"discNumber,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty" json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type Artists struct {
|
||||
List []*Index `xml:"index,omitempty" json:"index,omitempty"`
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"`
|
||||
AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
|
||||
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
|
||||
}
|
||||
|
||||
type Indexes struct {
|
||||
LastModified int `xml:"lastModified,attr,omitempty" json:"lastModified"`
|
||||
Index []*Index `xml:"index,omitempty" json:"index"`
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
Artists []*Artist `xml:"artist,omitempty" json:"artist"`
|
||||
}
|
||||
|
||||
type Directory struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id"`
|
||||
Parent int `xml:"parent,attr,omitempty" json:"parent"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"`
|
||||
Children []*TrackChild `xml:"child,omitempty" json:"child,omitempty"`
|
||||
}
|
||||
|
||||
type MusicFolders struct {
|
||||
List []*MusicFolder `xml:"musicFolder,omitempty" json:"musicFolder,omitempty"`
|
||||
}
|
||||
|
||||
type MusicFolder struct {
|
||||
ID int `xml:"id,attr,omitempty" json:"id,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type Licence struct {
|
||||
Valid bool `xml:"valid,attr,omitempty" json:"valid,omitempty"`
|
||||
}
|
||||
|
||||
type ScanStatus struct {
|
||||
Scanning bool `xml:"scanning,attr" json:"scanning"`
|
||||
Count int `xml:"count,attr,omitempty" json:"count,omitempty"`
|
||||
}
|
||||
|
||||
type SearchResultTwo struct {
|
||||
Artists []*Directory `xml:"artist,omitempty" json:"artist,omitempty"`
|
||||
Albums []*TrackChild `xml:"album,omitempty" json:"album,omitempty"`
|
||||
Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"`
|
||||
}
|
||||
|
||||
type SearchResultThree struct {
|
||||
Artists []*Artist `xml:"artist,omitempty" json:"artist,omitempty"`
|
||||
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
|
||||
Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"`
|
||||
}
|
||||
BIN
server/ctrlsubsonic/testdata/db
vendored
Normal file
BIN
server/ctrlsubsonic/testdata/db
vendored
Normal file
Binary file not shown.
100
server/ctrlsubsonic/testdata/test_get_album_list_alpha_artist
vendored
Normal file
100
server/ctrlsubsonic/testdata/test_get_album_list_alpha_artist
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList": {
|
||||
"album": [
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1967) Easter Nowhere",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1994) The Graveyard and the Ballroom",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"coverArt": 6,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1981) To EachOTHER.",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artist": "Captain Beefheart",
|
||||
"title": "(1970) Lick My Decals Off, Bitch",
|
||||
"parent": 20,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
||||
"title": "(1983) Snake Charmer",
|
||||
"parent": 2,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1980) Jane From Occupied Europe",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1979) A Trip to Marineville",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artist": "Ten Years After",
|
||||
"title": "(1967) Ten Years After",
|
||||
"parent": 18,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artist": "There",
|
||||
"title": "(2010) Anika",
|
||||
"parent": 12,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
100
server/ctrlsubsonic/testdata/test_get_album_list_alpha_name
vendored
Normal file
100
server/ctrlsubsonic/testdata/test_get_album_list_alpha_name
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList": {
|
||||
"album": [
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1967) Easter Nowhere",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artist": "Ten Years After",
|
||||
"title": "(1967) Ten Years After",
|
||||
"parent": 18,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artist": "Captain Beefheart",
|
||||
"title": "(1970) Lick My Decals Off, Bitch",
|
||||
"parent": 20,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1979) A Trip to Marineville",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1980) Jane From Occupied Europe",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"coverArt": 6,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1981) To EachOTHER.",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
||||
"title": "(1983) Snake Charmer",
|
||||
"parent": 2,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1994) The Graveyard and the Ballroom",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artist": "There",
|
||||
"title": "(2010) Anika",
|
||||
"parent": 12,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
100
server/ctrlsubsonic/testdata/test_get_album_list_newest
vendored
Normal file
100
server/ctrlsubsonic/testdata/test_get_album_list_newest
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList": {
|
||||
"album": [
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1967) Easter Nowhere",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artist": "Captain Beefheart",
|
||||
"title": "(1970) Lick My Decals Off, Bitch",
|
||||
"parent": 20,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1994) The Graveyard and the Ballroom",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"coverArt": 6,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1981) To EachOTHER.",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artist": "There",
|
||||
"title": "(2010) Anika",
|
||||
"parent": 12,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
||||
"title": "(1983) Snake Charmer",
|
||||
"parent": 2,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1980) Jane From Occupied Europe",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1979) A Trip to Marineville",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artist": "Ten Years After",
|
||||
"title": "(1967) Ten Years After",
|
||||
"parent": 18,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
100
server/ctrlsubsonic/testdata/test_get_album_list_random
vendored
Normal file
100
server/ctrlsubsonic/testdata/test_get_album_list_random
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList": {
|
||||
"album": [
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1979) A Trip to Marineville",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1994) The Graveyard and the Ballroom",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1967) Easter Nowhere",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artist": "Ten Years After",
|
||||
"title": "(1967) Ten Years After",
|
||||
"parent": 18,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artist": "Jah Wobble, The Edge, Holger Czukay",
|
||||
"title": "(1983) Snake Charmer",
|
||||
"parent": 2,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artist": "Swell Maps",
|
||||
"title": "(1980) Jane From Occupied Europe",
|
||||
"parent": 15,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artist": "Captain Beefheart",
|
||||
"title": "(1970) Lick My Decals Off, Bitch",
|
||||
"parent": 20,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artist": "There",
|
||||
"title": "(2010) Anika",
|
||||
"parent": 12,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"coverArt": 6,
|
||||
"artist": "A Certain Ratio",
|
||||
"title": "(1981) To EachOTHER.",
|
||||
"parent": 4,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artist": "13th Floor Lowervators",
|
||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"parent": 7,
|
||||
"isDir": true,
|
||||
"created": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_artist
vendored
Normal file
89
server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_artist
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList2": {
|
||||
"album": [
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "Easter Everywhere",
|
||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "The Graveyard and the Ballroom",
|
||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "To Each...",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artistId": 4,
|
||||
"artist": "Anikas",
|
||||
"name": "Anika",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artistId": 7,
|
||||
"artist": "Captain Beefheart & His Magic Band",
|
||||
"name": "Lick My Decals Off, Baby",
|
||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artistId": 1,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"name": "Snake Charmer",
|
||||
"created": "2019-05-16T22:10:52+01:00"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "Jane From Occupied Europe",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "A Trip to Marineville",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artistId": 6,
|
||||
"artist": "Ten Years After",
|
||||
"name": "Ten Years After",
|
||||
"created": "2019-04-30T16:48:30+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_name
vendored
Normal file
89
server/ctrlsubsonic/testdata/test_get_album_list_two_alpha_name
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList2": {
|
||||
"album": [
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "A Trip to Marineville",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artistId": 4,
|
||||
"artist": "Anikas",
|
||||
"name": "Anika",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "Easter Everywhere",
|
||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "Jane From Occupied Europe",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artistId": 7,
|
||||
"artist": "Captain Beefheart & His Magic Band",
|
||||
"name": "Lick My Decals Off, Baby",
|
||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artistId": 1,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"name": "Snake Charmer",
|
||||
"created": "2019-05-16T22:10:52+01:00"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artistId": 6,
|
||||
"artist": "Ten Years After",
|
||||
"name": "Ten Years After",
|
||||
"created": "2019-04-30T16:48:30+01:00"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "The Graveyard and the Ballroom",
|
||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "To Each...",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
server/ctrlsubsonic/testdata/test_get_album_list_two_newest
vendored
Normal file
89
server/ctrlsubsonic/testdata/test_get_album_list_two_newest
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList2": {
|
||||
"album": [
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "Easter Everywhere",
|
||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artistId": 7,
|
||||
"artist": "Captain Beefheart & His Magic Band",
|
||||
"name": "Lick My Decals Off, Baby",
|
||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "The Graveyard and the Ballroom",
|
||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "To Each...",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artistId": 4,
|
||||
"artist": "Anikas",
|
||||
"name": "Anika",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artistId": 1,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"name": "Snake Charmer",
|
||||
"created": "2019-05-16T22:10:52+01:00"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "Jane From Occupied Europe",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "A Trip to Marineville",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artistId": 6,
|
||||
"artist": "Ten Years After",
|
||||
"name": "Ten Years After",
|
||||
"created": "2019-04-30T16:48:30+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
89
server/ctrlsubsonic/testdata/test_get_album_list_two_random
vendored
Normal file
89
server/ctrlsubsonic/testdata/test_get_album_list_two_random
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"albumList2": {
|
||||
"album": [
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artistId": 1,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"name": "Snake Charmer",
|
||||
"created": "2019-05-16T22:10:52+01:00"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artistId": 4,
|
||||
"artist": "Anikas",
|
||||
"name": "Anika",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "Easter Everywhere",
|
||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"coverArt": 19,
|
||||
"artistId": 6,
|
||||
"artist": "Ten Years After",
|
||||
"name": "Ten Years After",
|
||||
"created": "2019-04-30T16:48:30+01:00"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"coverArt": 17,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "A Trip to Marineville",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "The Graveyard and the Ballroom",
|
||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "To Each...",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"coverArt": 16,
|
||||
"artistId": 5,
|
||||
"artist": "Swell Maps",
|
||||
"name": "Jane From Occupied Europe",
|
||||
"created": "2019-04-30T16:48:48+01:00"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"coverArt": 21,
|
||||
"artistId": 7,
|
||||
"artist": "Captain Beefheart & His Magic Band",
|
||||
"name": "Lick My Decals Off, Baby",
|
||||
"created": "2019-06-10T19:26:30.944742894+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
116
server/ctrlsubsonic/testdata/test_get_album_with_cover
vendored
Normal file
116
server/ctrlsubsonic/testdata/test_get_album_with_cover
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"album": {
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artistId": 1,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"name": "Snake Charmer",
|
||||
"created": "2019-05-16T22:10:52+01:00",
|
||||
"song": [
|
||||
{
|
||||
"album": "Snake Charmer",
|
||||
"albumId": 3,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"artistId": 1,
|
||||
"bitRate": 882,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "2019-07-08T21:49:40.978045401+01:00",
|
||||
"duration": 372,
|
||||
"id": 1,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/01.05 Snake Charmer.flac",
|
||||
"size": 41274185,
|
||||
"suffix": "flac",
|
||||
"title": "Snake Charmer",
|
||||
"track": 1,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "Snake Charmer",
|
||||
"albumId": 3,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"artistId": 1,
|
||||
"bitRate": 814,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "2019-07-08T21:49:40.981605306+01:00",
|
||||
"duration": 523,
|
||||
"id": 3,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/02.05 Hold On to Your Dreams.flac",
|
||||
"size": 53447545,
|
||||
"suffix": "flac",
|
||||
"title": "Hold On to Your Dreams",
|
||||
"track": 2,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "Snake Charmer",
|
||||
"albumId": 3,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"artistId": 1,
|
||||
"bitRate": 745,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "2019-07-08T21:49:40.979981084+01:00",
|
||||
"duration": 331,
|
||||
"id": 2,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/03.05 It Was a Camel.flac",
|
||||
"size": 31080508,
|
||||
"suffix": "flac",
|
||||
"title": "It Was a Camel",
|
||||
"track": 3,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "Snake Charmer",
|
||||
"albumId": 3,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"artistId": 1,
|
||||
"bitRate": 976,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "2019-07-08T21:49:40.984853203+01:00",
|
||||
"duration": 227,
|
||||
"id": 5,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/04.05 Sleazy.flac",
|
||||
"size": 27938750,
|
||||
"suffix": "flac",
|
||||
"title": "Sleazy",
|
||||
"track": 4,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "Snake Charmer",
|
||||
"albumId": 3,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"artistId": 1,
|
||||
"bitRate": 884,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "2019-07-08T21:49:40.983301328+01:00",
|
||||
"duration": 418,
|
||||
"id": 4,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/05.05 Snake Charmer (reprise).flac",
|
||||
"size": 46427922,
|
||||
"suffix": "flac",
|
||||
"title": "Snake Charmer (reprise)",
|
||||
"track": 5,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
10
server/ctrlsubsonic/testdata/test_get_album_without_cover
vendored
Normal file
10
server/ctrlsubsonic/testdata/test_get_album_without_cover
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"album": {
|
||||
"id": 2,
|
||||
"created": "2019-05-16T22:10:21+01:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
server/ctrlsubsonic/testdata/test_get_artist_id_one
vendored
Normal file
20
server/ctrlsubsonic/testdata/test_get_artist_id_one
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"artist": {
|
||||
"id": 1,
|
||||
"name": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"album": [
|
||||
{
|
||||
"id": 3,
|
||||
"coverArt": 3,
|
||||
"artistId": 1,
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"name": "Snake Charmer",
|
||||
"created": "2019-05-16T22:10:52+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
28
server/ctrlsubsonic/testdata/test_get_artist_id_three
vendored
Normal file
28
server/ctrlsubsonic/testdata/test_get_artist_id_three
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"artist": {
|
||||
"id": 3,
|
||||
"name": "13th Floor Elevators",
|
||||
"album": [
|
||||
{
|
||||
"id": 8,
|
||||
"coverArt": 8,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "Easter Everywhere",
|
||||
"created": "2019-06-13T12:57:28.850090338+01:00"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
server/ctrlsubsonic/testdata/test_get_artist_id_two
vendored
Normal file
27
server/ctrlsubsonic/testdata/test_get_artist_id_two
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"artist": {
|
||||
"id": 2,
|
||||
"name": "A Certain Ratio",
|
||||
"album": [
|
||||
{
|
||||
"id": 5,
|
||||
"coverArt": 5,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "The Graveyard and the Ballroom",
|
||||
"created": "2019-06-05T17:46:37.675917974+01:00"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"artistId": 2,
|
||||
"artist": "A Certain Ratio",
|
||||
"name": "To Each...",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
75
server/ctrlsubsonic/testdata/test_get_artists_no_args
vendored
Normal file
75
server/ctrlsubsonic/testdata/test_get_artists_no_args
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"artists": {
|
||||
"index": [
|
||||
{
|
||||
"name": "#",
|
||||
"artist": [
|
||||
{
|
||||
"id": 3,
|
||||
"name": "13th Floor Elevators",
|
||||
"albumCount": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "A Certain Ratio",
|
||||
"albumCount": 2
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Anikas",
|
||||
"albumCount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "c",
|
||||
"artist": [
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Captain Beefheart & His Magic Band",
|
||||
"albumCount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "j",
|
||||
"artist": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"albumCount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "s",
|
||||
"artist": [
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Swell Maps",
|
||||
"albumCount": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "t",
|
||||
"artist": [
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Ten Years After",
|
||||
"albumCount": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
76
server/ctrlsubsonic/testdata/test_get_indexes_no_args
vendored
Normal file
76
server/ctrlsubsonic/testdata/test_get_indexes_no_args
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"indexes": {
|
||||
"lastModified": 0,
|
||||
"index": [
|
||||
{
|
||||
"name": "#",
|
||||
"artist": [
|
||||
{
|
||||
"id": 7,
|
||||
"name": "13th Floor Lowervators",
|
||||
"albumCount": 2
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "___Anika",
|
||||
"albumCount": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "a",
|
||||
"artist": [
|
||||
{
|
||||
"id": 4,
|
||||
"name": "A Certain Ratio",
|
||||
"albumCount": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "c",
|
||||
"artist": [
|
||||
{
|
||||
"id": 20,
|
||||
"name": "Captain Beefheart",
|
||||
"albumCount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "j",
|
||||
"artist": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Jah Wobble, The Edge, Holger Czukay",
|
||||
"albumCount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "s",
|
||||
"artist": [
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Swell Maps",
|
||||
"albumCount": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "t",
|
||||
"artist": [
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Ten Years After",
|
||||
"albumCount": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
103
server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks
vendored
Normal file
103
server/ctrlsubsonic/testdata/test_get_music_directory_with_tracks
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"directory": {
|
||||
"id": 3,
|
||||
"parent": 2,
|
||||
"name": "(1983) Snake Charmer",
|
||||
"child": [
|
||||
{
|
||||
"album": "(1983) Snake Charmer",
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"bitRate": 882,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 372,
|
||||
"id": 1,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/01.05 Snake Charmer.flac",
|
||||
"size": 41274185,
|
||||
"suffix": "flac",
|
||||
"title": "Snake Charmer",
|
||||
"track": 1,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1983) Snake Charmer",
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"bitRate": 814,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 523,
|
||||
"id": 3,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/02.05 Hold On to Your Dreams.flac",
|
||||
"size": 53447545,
|
||||
"suffix": "flac",
|
||||
"title": "Hold On to Your Dreams",
|
||||
"track": 2,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1983) Snake Charmer",
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"bitRate": 745,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 331,
|
||||
"id": 2,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/03.05 It Was a Camel.flac",
|
||||
"size": 31080508,
|
||||
"suffix": "flac",
|
||||
"title": "It Was a Camel",
|
||||
"track": 3,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1983) Snake Charmer",
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"bitRate": 976,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 227,
|
||||
"id": 5,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/04.05 Sleazy.flac",
|
||||
"size": 27938750,
|
||||
"suffix": "flac",
|
||||
"title": "Sleazy",
|
||||
"track": 4,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1983) Snake Charmer",
|
||||
"artist": "Jah Wobble, The Edge & Holger Czukay",
|
||||
"bitRate": 884,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 3,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 418,
|
||||
"id": 4,
|
||||
"parent": 3,
|
||||
"path": "Jah Wobble, The Edge, Holger Czukay/(1983) Snake Charmer/05.05 Snake Charmer (reprise).flac",
|
||||
"size": 46427922,
|
||||
"suffix": "flac",
|
||||
"title": "Snake Charmer (reprise)",
|
||||
"track": 5,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
server/ctrlsubsonic/testdata/test_get_music_directory_without_tracks
vendored
Normal file
21
server/ctrlsubsonic/testdata/test_get_music_directory_without_tracks
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"directory": {
|
||||
"id": 2,
|
||||
"parent": 1,
|
||||
"name": "Jah Wobble, The Edge, Holger Czukay",
|
||||
"child": [
|
||||
{
|
||||
"coverArt": 3,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"id": 3,
|
||||
"isDir": true,
|
||||
"parent": 2,
|
||||
"title": "(1983) Snake Charmer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
24
server/ctrlsubsonic/testdata/test_search_three_q_13
vendored
Normal file
24
server/ctrlsubsonic/testdata/test_search_three_q_13
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"searchResult3": {
|
||||
"artist": [
|
||||
{
|
||||
"id": 3,
|
||||
"name": "13th Floor Elevators"
|
||||
}
|
||||
],
|
||||
"album": [
|
||||
{
|
||||
"id": 9,
|
||||
"coverArt": 9,
|
||||
"artistId": 3,
|
||||
"artist": "13th Floor Elevators",
|
||||
"name": "The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"created": "2019-06-13T12:57:24.306717554+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
24
server/ctrlsubsonic/testdata/test_search_three_q_ani
vendored
Normal file
24
server/ctrlsubsonic/testdata/test_search_three_q_ani
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"searchResult3": {
|
||||
"artist": [
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Anikas"
|
||||
}
|
||||
],
|
||||
"album": [
|
||||
{
|
||||
"id": 13,
|
||||
"coverArt": 13,
|
||||
"artistId": 4,
|
||||
"artist": "Anikas",
|
||||
"name": "Anika",
|
||||
"created": "2019-05-23T15:12:02.921473302+01:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
14
server/ctrlsubsonic/testdata/test_search_three_q_cert
vendored
Normal file
14
server/ctrlsubsonic/testdata/test_search_three_q_cert
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"searchResult3": {
|
||||
"artist": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "A Certain Ratio"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
135
server/ctrlsubsonic/testdata/test_search_two_q_13
vendored
Normal file
135
server/ctrlsubsonic/testdata/test_search_two_q_13
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"searchResult2": {
|
||||
"artist": [
|
||||
{
|
||||
"id": 7,
|
||||
"parent": 1,
|
||||
"name": "13th Floor Lowervators"
|
||||
}
|
||||
],
|
||||
"album": [
|
||||
{
|
||||
"coverArt": 9,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"id": 9,
|
||||
"isDir": true,
|
||||
"parent": 7,
|
||||
"title": "(1966) The Psychedelic Sounds of the 13th Floor Elevators"
|
||||
}
|
||||
],
|
||||
"song": [
|
||||
{
|
||||
"album": "(1994) The Graveyard and the Ballroom",
|
||||
"artist": "A Certain Ratio",
|
||||
"bitRate": 894,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 5,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 332,
|
||||
"id": 6,
|
||||
"parent": 5,
|
||||
"path": "A Certain Ratio/(1994) The Graveyard and the Ballroom/13.14 Flight.flac",
|
||||
"size": 37302417,
|
||||
"suffix": "flac",
|
||||
"title": "Flight",
|
||||
"track": 13,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1966) The Psychedelic Sounds of the 13th Floor Elevators",
|
||||
"artist": "13th Floor Elevators",
|
||||
"bitRate": 244,
|
||||
"contentType": "audio/mpeg",
|
||||
"coverArt": 9,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 154,
|
||||
"id": 40,
|
||||
"parent": 9,
|
||||
"path": "13th Floor Lowervators/(1966) The Psychedelic Sounds of the 13th Floor Elevators/13.21 Before You Accuse Me.mp3",
|
||||
"size": 4722688,
|
||||
"suffix": "mp3",
|
||||
"title": "Before You Accuse Me",
|
||||
"track": 13,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1980) Jane From Occupied Europe",
|
||||
"artist": "Swell Maps",
|
||||
"bitRate": 1204,
|
||||
"contentType": "audio/x-flac",
|
||||
"coverArt": 16,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 220,
|
||||
"id": 76,
|
||||
"parent": 16,
|
||||
"path": "Swell Maps/(1980) Jane From Occupied Europe/13.16 Blenheim Shots.flac",
|
||||
"size": 33140852,
|
||||
"suffix": "flac",
|
||||
"title": "Blenheim Shots",
|
||||
"track": 13,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1979) A Trip to Marineville",
|
||||
"artist": "Swell Maps",
|
||||
"bitRate": 295,
|
||||
"contentType": "audio/mpeg",
|
||||
"coverArt": 17,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 463,
|
||||
"id": 93,
|
||||
"parent": 17,
|
||||
"path": "Swell Maps/(1979) A Trip to Marineville/d01 13.14 Adventuring Into Basketry.mp3",
|
||||
"size": 17119309,
|
||||
"suffix": "mp3",
|
||||
"title": "Adventuring Into Basketry",
|
||||
"track": 13,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1967) Ten Years After",
|
||||
"artist": "Ten Years After",
|
||||
"bitRate": 192,
|
||||
"contentType": "audio/ogg",
|
||||
"coverArt": 19,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 433,
|
||||
"id": 107,
|
||||
"parent": 19,
|
||||
"path": "Ten Years After/(1967) Ten Years After/13.15 Spider in My Web.ogg",
|
||||
"size": 10400948,
|
||||
"suffix": "ogg",
|
||||
"title": "Spider in My Web",
|
||||
"track": 13,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
},
|
||||
{
|
||||
"album": "(1970) Lick My Decals Off, Bitch",
|
||||
"artist": "Captain Beefheart & His Magic Band",
|
||||
"bitRate": 160,
|
||||
"contentType": "audio/mpeg",
|
||||
"coverArt": 21,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"duration": 152,
|
||||
"id": 129,
|
||||
"parent": 21,
|
||||
"path": "Captain Beefheart/(1970) Lick My Decals Off, Bitch/13.15 Space-Age Couple.mp3",
|
||||
"size": 3054515,
|
||||
"suffix": "mp3",
|
||||
"title": "Space-Age Couple",
|
||||
"track": 13,
|
||||
"discNumber": 1,
|
||||
"type": "music"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
25
server/ctrlsubsonic/testdata/test_search_two_q_ani
vendored
Normal file
25
server/ctrlsubsonic/testdata/test_search_two_q_ani
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"searchResult2": {
|
||||
"artist": [
|
||||
{
|
||||
"id": 10,
|
||||
"parent": 1,
|
||||
"name": "___Anika"
|
||||
}
|
||||
],
|
||||
"album": [
|
||||
{
|
||||
"coverArt": 13,
|
||||
"created": "0001-01-01T00:00:00Z",
|
||||
"id": 13,
|
||||
"isDir": true,
|
||||
"parent": 12,
|
||||
"title": "(2010) Anika"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
15
server/ctrlsubsonic/testdata/test_search_two_q_cert
vendored
Normal file
15
server/ctrlsubsonic/testdata/test_search_two_q_cert
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"subsonic-response": {
|
||||
"status": "ok",
|
||||
"version": "1.9.0",
|
||||
"searchResult2": {
|
||||
"artist": [
|
||||
{
|
||||
"id": 4,
|
||||
"parent": 1,
|
||||
"name": "A Certain Ratio"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user