Subsonic: album/song lists (getAlbumList/2, getRandomSongs, getSongsByGenre, getNowPlaying, getStarred/2) and search (search, search2, search3)
This commit is contained in:
@@ -14,6 +14,10 @@ import (
|
||||
// keeps XML and JSON output (and every endpoint that happens to return
|
||||
// the same entity type) semantically identical.
|
||||
|
||||
func secondsSince(t time.Time) float64 {
|
||||
return time.Since(t).Seconds()
|
||||
}
|
||||
|
||||
func isoTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
package subsonic
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("getAlbumList", handleGetAlbumList)
|
||||
register("getAlbumList2", handleGetAlbumList2)
|
||||
register("getRandomSongs", handleGetRandomSongs)
|
||||
register("getSongsByGenre", handleGetSongsByGenre)
|
||||
register("getNowPlaying", handleGetNowPlaying)
|
||||
register("getStarred", handleGetStarred)
|
||||
register("getStarred2", handleGetStarred2)
|
||||
}
|
||||
|
||||
// resolveAlbumListType fetches albums for one of the official
|
||||
// getAlbumList/getAlbumList2 "type" values. Every branch is a real
|
||||
// query — nothing here is a fabricated or hardcoded result.
|
||||
func resolveAlbumListType(rc *Context) ([]*database.Album, *apiError) {
|
||||
db := rc.Server.DB
|
||||
listType, aerr := rc.Params.Required("type")
|
||||
if aerr != nil {
|
||||
return nil, aerr
|
||||
}
|
||||
size := rc.Params.Int("size", 10)
|
||||
if size > 500 {
|
||||
size = 500
|
||||
}
|
||||
offset := rc.Params.Int("offset", 0)
|
||||
|
||||
var albums []*database.Album
|
||||
var err error
|
||||
switch listType {
|
||||
case "random":
|
||||
albums, err = database.RandomAlbums(db, size)
|
||||
case "newest":
|
||||
albums, err = database.RecentlyAddedAlbums(db, size, offset)
|
||||
case "highest":
|
||||
albums, err = database.AlbumsByHighestRating(db, size, offset)
|
||||
case "frequent":
|
||||
albums, err = database.AlbumsByPlayCount(db, size, offset)
|
||||
case "recent":
|
||||
albums, err = database.AlbumsByRecentlyPlayed(db, rc.User.ID, size, offset)
|
||||
case "alphabeticalByName":
|
||||
albums, err = database.AlbumsAlphabetical(db, false, size, offset)
|
||||
case "alphabeticalByArtist":
|
||||
albums, err = database.AlbumsAlphabetical(db, true, size, offset)
|
||||
case "starred":
|
||||
albums, err = starredAlbums(db, rc.User.ID, size, offset)
|
||||
case "byYear":
|
||||
fromYear, ferr := rc.Params.RequiredInt("fromYear")
|
||||
if ferr != nil {
|
||||
return nil, ferr
|
||||
}
|
||||
toYear, terr := rc.Params.RequiredInt("toYear")
|
||||
if terr != nil {
|
||||
return nil, terr
|
||||
}
|
||||
albums, err = database.AlbumsByYearRange(db, fromYear, toYear, size, offset)
|
||||
case "byGenre":
|
||||
genre, gerr := rc.Params.Required("genre")
|
||||
if gerr != nil {
|
||||
return nil, gerr
|
||||
}
|
||||
albums, err = database.AlbumsByGenre(db, genre, size, offset)
|
||||
default:
|
||||
return nil, newError(ErrGeneric, "Unknown album list type: "+listType)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, newError(ErrGeneric, err.Error())
|
||||
}
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
func starredAlbums(db *sql.DB, userID string, limit, offset int) ([]*database.Album, error) {
|
||||
ids, err := database.ListStarredIDs(db, userID, "album")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if offset >= len(ids) {
|
||||
return nil, nil
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(ids) {
|
||||
end = len(ids)
|
||||
}
|
||||
var out []*database.Album
|
||||
for _, id := range ids[offset:end] {
|
||||
if a, err := database.GetAlbum(db, id); err == nil {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func handleGetAlbumList(rc *Context) *apiError {
|
||||
albums, aerr := resolveAlbumListType(rc)
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
out := &AlbumList{}
|
||||
for _, a := range albums {
|
||||
out.Album = append(out.Album, childFromAlbum(rc.Server.DB, a, rc.User.ID))
|
||||
}
|
||||
rc.Response = newResponse()
|
||||
rc.Response.AlbumList = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetAlbumList2(rc *Context) *apiError {
|
||||
albums, aerr := resolveAlbumListType(rc)
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
out := &AlbumList2{}
|
||||
for _, a := range albums {
|
||||
out.Album = append(out.Album, albumToID3(rc.Server.DB, a, rc.User.ID))
|
||||
}
|
||||
rc.Response = newResponse()
|
||||
rc.Response.AlbumList2 = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetRandomSongs(rc *Context) *apiError {
|
||||
db := rc.Server.DB
|
||||
size := rc.Params.Int("size", 10)
|
||||
if size > 500 {
|
||||
size = 500
|
||||
}
|
||||
genre := rc.Params.Get("genre")
|
||||
fromYear := rc.Params.Int("fromYear", 0)
|
||||
toYear := rc.Params.Int("toYear", 0)
|
||||
|
||||
ids, err := database.RandomSongIDs(db, size, genre, fromYear, toYear)
|
||||
if err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
out := &Songs{}
|
||||
for _, id := range ids {
|
||||
song, serr := database.GetSong(db, id)
|
||||
if serr != nil {
|
||||
continue
|
||||
}
|
||||
mf, merr := database.GetMediaFile(db, song.MediaFileID)
|
||||
if merr != nil {
|
||||
continue
|
||||
}
|
||||
if canSee, cerr := UserCanSeeMediaFile(db, rc.User, mf); cerr != nil || !canSee {
|
||||
continue
|
||||
}
|
||||
out.Song = append(out.Song, childFromSong(db, song, mf, albumOfSong(db, song), rc.User.ID))
|
||||
}
|
||||
rc.Response = newResponse()
|
||||
rc.Response.RandomSongs = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetSongsByGenre(rc *Context) *apiError {
|
||||
genre, aerr := rc.Params.Required("genre")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
db := rc.Server.DB
|
||||
count := rc.Params.Int("count", 10)
|
||||
if count > 500 {
|
||||
count = 500
|
||||
}
|
||||
offset := rc.Params.Int("offset", 0)
|
||||
|
||||
songs, err := database.SongsByGenre(db, genre, count, offset)
|
||||
if err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
out := &Songs{}
|
||||
for _, s := range songs {
|
||||
mf, merr := database.GetMediaFile(db, s.MediaFileID)
|
||||
if merr != nil {
|
||||
continue
|
||||
}
|
||||
if canSee, cerr := UserCanSeeMediaFile(db, rc.User, mf); cerr != nil || !canSee {
|
||||
continue
|
||||
}
|
||||
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), rc.User.ID))
|
||||
}
|
||||
rc.Response = newResponse()
|
||||
rc.Response.SongsByGenre = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetNowPlaying(rc *Context) *apiError {
|
||||
db := rc.Server.DB
|
||||
entries, err := database.ListNowPlaying(db)
|
||||
if err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
out := &NowPlaying{}
|
||||
for _, e := range entries {
|
||||
song, serr := database.GetSong(db, e.SongID)
|
||||
if serr != nil {
|
||||
continue
|
||||
}
|
||||
mf, merr := database.GetMediaFile(db, song.MediaFileID)
|
||||
if merr != nil {
|
||||
continue
|
||||
}
|
||||
c := childFromSong(db, song, mf, albumOfSong(db, song), "")
|
||||
minutesAgo := int(secondsSince(e.StartedAt) / 60)
|
||||
out.Entry = append(out.Entry, NowPlayingEntry{
|
||||
Child: c,
|
||||
Username: e.Username,
|
||||
MinutesAgo: minutesAgo,
|
||||
PlayerID: 1,
|
||||
})
|
||||
}
|
||||
rc.Response = newResponse()
|
||||
rc.Response.NowPlaying = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetStarred(rc *Context) *apiError {
|
||||
db := rc.Server.DB
|
||||
userID := rc.User.ID
|
||||
out := &Starred{}
|
||||
|
||||
artistIDs, _ := database.ListStarredIDs(db, userID, "artist")
|
||||
for _, id := range artistIDs {
|
||||
if a, err := database.GetArtist(db, id); err == nil {
|
||||
out.Artist = append(out.Artist, Artist{ID: a.ID, Name: a.Name})
|
||||
}
|
||||
}
|
||||
albumIDs, _ := database.ListStarredIDs(db, userID, "album")
|
||||
for _, id := range albumIDs {
|
||||
if a, err := database.GetAlbum(db, id); err == nil {
|
||||
out.Album = append(out.Album, childFromAlbum(db, a, userID))
|
||||
}
|
||||
}
|
||||
songIDs, _ := database.ListStarredIDs(db, userID, "song")
|
||||
for _, id := range songIDs {
|
||||
if s, err := database.GetSong(db, id); err == nil {
|
||||
if mf, merr := database.GetMediaFile(db, s.MediaFileID); merr == nil {
|
||||
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), userID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rc.Response = newResponse()
|
||||
rc.Response.Starred = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetStarred2(rc *Context) *apiError {
|
||||
db := rc.Server.DB
|
||||
userID := rc.User.ID
|
||||
out := &Starred2{}
|
||||
|
||||
artistIDs, _ := database.ListStarredIDs(db, userID, "artist")
|
||||
for _, id := range artistIDs {
|
||||
if a, err := database.GetArtist(db, id); err == nil {
|
||||
out.Artist = append(out.Artist, artistToID3(db, a, userID))
|
||||
}
|
||||
}
|
||||
albumIDs, _ := database.ListStarredIDs(db, userID, "album")
|
||||
for _, id := range albumIDs {
|
||||
if a, err := database.GetAlbum(db, id); err == nil {
|
||||
out.Album = append(out.Album, albumToID3(db, a, userID))
|
||||
}
|
||||
}
|
||||
songIDs, _ := database.ListStarredIDs(db, userID, "song")
|
||||
for _, id := range songIDs {
|
||||
if s, err := database.GetSong(db, id); err == nil {
|
||||
if mf, merr := database.GetMediaFile(db, s.MediaFileID); merr == nil {
|
||||
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), userID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rc.Response = newResponse()
|
||||
rc.Response.Starred2 = out
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package subsonic
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
||||
)
|
||||
|
||||
func TestGetAlbumListTypes(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
for _, listType := range []string{"random", "newest", "highest", "frequent", "alphabeticalByName", "alphabeticalByArtist"} {
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("type", listType)
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/getAlbumList.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
if j["status"] != "ok" {
|
||||
t.Errorf("type=%s: status = %v, want ok (%v)", listType, j["status"], j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAlbumListByYear(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("type", "byYear")
|
||||
q.Set("fromYear", "2000")
|
||||
q.Set("toYear", "2030")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/getAlbumList2.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
list := j["albumList2"].(map[string]any)
|
||||
albums, ok := list["album"].([]any)
|
||||
if !ok || len(albums) == 0 {
|
||||
t.Fatalf("expected at least one album in byYear 2000-2030 range, got %v", list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAlbumListByYearMissingParam(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("type", "byYear")
|
||||
q.Set("fromYear", "2000")
|
||||
resp := getPath(t, srv.URL, "/rest/getAlbumList.view", q)
|
||||
x := decodeXML(t, resp)
|
||||
if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrMissingParameter {
|
||||
t.Fatalf("expected error 10 for missing toYear, got %+v", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAlbumListMissingType(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
|
||||
resp := getPath(t, srv.URL, "/rest/getAlbumList.view", baseParams("alice", "password123"))
|
||||
x := decodeXML(t, resp)
|
||||
if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrMissingParameter {
|
||||
t.Fatalf("expected error 10, got %+v", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRandomSongs(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("size", "1")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/getRandomSongs.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
songs := j["randomSongs"].(map[string]any)["song"].([]any)
|
||||
if len(songs) != 1 {
|
||||
t.Fatalf("randomSongs = %v, want 1", songs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSongsByGenre(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("genre", "Rock")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/getSongsByGenre.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
songs := j["songsByGenre"].(map[string]any)["song"].([]any)
|
||||
if len(songs) != 2 {
|
||||
t.Fatalf("songsByGenre = %v, want 2", songs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNowPlayingEmpty(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/getNowPlaying.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
if j["status"] != "ok" {
|
||||
t.Fatalf("status = %v", j["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStarredAndStarred2(t *testing.T) {
|
||||
s, db := newTestServer(t)
|
||||
u := createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
srv := httptest.NewServer(s.Router())
|
||||
defer srv.Close()
|
||||
|
||||
songs, err := database.ListSongs(db, 1, 0)
|
||||
if err != nil || len(songs) == 0 {
|
||||
t.Fatalf("ListSongs: %v %v", songs, err)
|
||||
}
|
||||
if err := database.Star(db, u.ID, "song", songs[0].ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/getStarred.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
starred := j["starred"].(map[string]any)
|
||||
if s, ok := starred["song"].([]any); !ok || len(s) != 1 {
|
||||
t.Fatalf("starred.song = %v, want 1", starred["song"])
|
||||
}
|
||||
|
||||
resp2 := getPath(t, srv.URL, "/rest/getStarred2.view", q)
|
||||
j2 := decodeJSON(t, resp2)
|
||||
starred2 := j2["starred2"].(map[string]any)
|
||||
if s, ok := starred2["song"].([]any); !ok || len(s) != 1 {
|
||||
t.Fatalf("starred2.song = %v, want 1", starred2["song"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package subsonic
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("search", handleSearch)
|
||||
register("search2", handleSearch2)
|
||||
register("search3", handleSearch3)
|
||||
}
|
||||
|
||||
type searchMatches struct {
|
||||
Artists []*database.Artist
|
||||
Albums []*database.Album
|
||||
Songs []*database.Song
|
||||
}
|
||||
|
||||
// runSearch performs a simple substring, case-insensitive match against
|
||||
// artist/album/song names, with independent offset/count for each
|
||||
// category as the protocol requires. VibeSonic's libraries are small
|
||||
// enough (self-hosted, not a streaming catalog) that a LIKE scan is
|
||||
// plenty fast; there's no need for SQLite FTS's added complexity here.
|
||||
func runSearch(db *sql.DB, query string, artistOffset, artistCount, albumOffset, albumCount, songOffset, songCount int) (searchMatches, error) {
|
||||
var m searchMatches
|
||||
if query == "" {
|
||||
return m, nil
|
||||
}
|
||||
like := "%" + query + "%"
|
||||
|
||||
artistRows, err := db.Query(`SELECT DISTINCT a.id FROM artists a JOIN album_artists aa ON aa.artist_id = a.id
|
||||
WHERE a.name LIKE ? COLLATE NOCASE ORDER BY a.name COLLATE NOCASE LIMIT ? OFFSET ?`, like, artistCount, artistOffset)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
for artistRows.Next() {
|
||||
var id string
|
||||
if err := artistRows.Scan(&id); err == nil {
|
||||
if a, err := database.GetArtist(db, id); err == nil {
|
||||
m.Artists = append(m.Artists, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
artistRows.Close()
|
||||
|
||||
albumRows, err := db.Query(`SELECT id FROM albums WHERE title LIKE ? COLLATE NOCASE ORDER BY title COLLATE NOCASE LIMIT ? OFFSET ?`, like, albumCount, albumOffset)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
for albumRows.Next() {
|
||||
var id string
|
||||
if err := albumRows.Scan(&id); err == nil {
|
||||
if a, err := database.GetAlbum(db, id); err == nil {
|
||||
m.Albums = append(m.Albums, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
albumRows.Close()
|
||||
|
||||
songRows, err := db.Query(`SELECT id FROM songs WHERE title LIKE ? COLLATE NOCASE ORDER BY title COLLATE NOCASE LIMIT ? OFFSET ?`, like, songCount, songOffset)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
for songRows.Next() {
|
||||
var id string
|
||||
if err := songRows.Scan(&id); err == nil {
|
||||
if s, err := database.GetSong(db, id); err == nil {
|
||||
m.Songs = append(m.Songs, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
songRows.Close()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// handleSearch implements the original, deprecated (since 1.4.0, but
|
||||
// still required) flat search: a single "any" query with one offset/
|
||||
// count, results as Child, regardless of type.
|
||||
func handleSearch(rc *Context) *apiError {
|
||||
any, aerr := rc.Params.Required("any")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
db := rc.Server.DB
|
||||
offset := rc.Params.Int("offset", 0)
|
||||
count := rc.Params.Int("count", 20)
|
||||
|
||||
m, err := runSearch(db, any, offset, count, offset, count, offset, count)
|
||||
if err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
out := &SearchResult{Offset: offset}
|
||||
for _, a := range m.Artists {
|
||||
out.Match = append(out.Match, Child{ID: a.ID, IsDir: true, Title: a.Name})
|
||||
}
|
||||
for _, a := range m.Albums {
|
||||
out.Match = append(out.Match, childFromAlbum(db, a, rc.User.ID))
|
||||
}
|
||||
for _, s := range m.Songs {
|
||||
mf, merr := database.GetMediaFile(db, s.MediaFileID)
|
||||
if merr != nil {
|
||||
continue
|
||||
}
|
||||
out.Match = append(out.Match, childFromSong(db, s, mf, albumOfSong(db, s), rc.User.ID))
|
||||
}
|
||||
out.TotalHits = len(out.Match)
|
||||
rc.Response = newResponse()
|
||||
rc.Response.SearchResult = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSearch2(rc *Context) *apiError {
|
||||
query, aerr := rc.Params.Required("query")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
db := rc.Server.DB
|
||||
m, err := runSearch(db, query,
|
||||
rc.Params.Int("artistOffset", 0), rc.Params.Int("artistCount", 20),
|
||||
rc.Params.Int("albumOffset", 0), rc.Params.Int("albumCount", 20),
|
||||
rc.Params.Int("songOffset", 0), rc.Params.Int("songCount", 20))
|
||||
if err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
out := &SearchResult2{}
|
||||
for _, a := range m.Artists {
|
||||
out.Artist = append(out.Artist, Artist{ID: a.ID, Name: a.Name})
|
||||
}
|
||||
for _, a := range m.Albums {
|
||||
out.Album = append(out.Album, childFromAlbum(db, a, rc.User.ID))
|
||||
}
|
||||
for _, s := range m.Songs {
|
||||
mf, merr := database.GetMediaFile(db, s.MediaFileID)
|
||||
if merr != nil {
|
||||
continue
|
||||
}
|
||||
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), rc.User.ID))
|
||||
}
|
||||
rc.Response = newResponse()
|
||||
rc.Response.SearchResult2 = out
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSearch3(rc *Context) *apiError {
|
||||
query, aerr := rc.Params.Required("query")
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
db := rc.Server.DB
|
||||
m, err := runSearch(db, query,
|
||||
rc.Params.Int("artistOffset", 0), rc.Params.Int("artistCount", 20),
|
||||
rc.Params.Int("albumOffset", 0), rc.Params.Int("albumCount", 20),
|
||||
rc.Params.Int("songOffset", 0), rc.Params.Int("songCount", 20))
|
||||
if err != nil {
|
||||
return newError(ErrGeneric, err.Error())
|
||||
}
|
||||
out := &SearchResult3{}
|
||||
for _, a := range m.Artists {
|
||||
out.Artist = append(out.Artist, artistToID3(db, a, rc.User.ID))
|
||||
}
|
||||
for _, a := range m.Albums {
|
||||
out.Album = append(out.Album, albumToID3(db, a, rc.User.ID))
|
||||
}
|
||||
for _, s := range m.Songs {
|
||||
mf, merr := database.GetMediaFile(db, s.MediaFileID)
|
||||
if merr != nil {
|
||||
continue
|
||||
}
|
||||
out.Song = append(out.Song, childFromSong(db, s, mf, albumOfSong(db, s), rc.User.ID))
|
||||
}
|
||||
rc.Response = newResponse()
|
||||
rc.Response.SearchResult3 = out
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package subsonic
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSearch3(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("query", "Second")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/search3.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
result := j["searchResult3"].(map[string]any)
|
||||
songs, ok := result["song"].([]any)
|
||||
if !ok || len(songs) != 1 {
|
||||
t.Fatalf("search3 songs = %v, want 1 match for 'Second'", result["song"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch2(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("query", "Test Artist")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/search2.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
result := j["searchResult2"].(map[string]any)
|
||||
artists, ok := result["artist"].([]any)
|
||||
if !ok || len(artists) != 1 {
|
||||
t.Fatalf("search2 artists = %v, want 1 match", result["artist"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchDeprecatedFlat(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("any", "Test Album")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/search.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
result := j["searchResult"].(map[string]any)
|
||||
if result["totalHits"] == nil {
|
||||
t.Fatalf("search: missing totalHits: %v", result)
|
||||
}
|
||||
matches, ok := result["match"].([]any)
|
||||
if !ok || len(matches) == 0 {
|
||||
t.Fatalf("search: matches = %v, want at least 1", result["match"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch3MissingQuery(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
|
||||
resp := getPath(t, srv.URL, "/rest/search3.view", baseParams("alice", "password123"))
|
||||
x := decodeXML(t, resp)
|
||||
if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrMissingParameter {
|
||||
t.Fatalf("expected error 10, got %+v", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch3NoResults(t *testing.T) {
|
||||
s, srv := newTestRecorderServer(t)
|
||||
createTestUser(t, s, "alice", "password123", false)
|
||||
seedLibrary(t, s)
|
||||
|
||||
q := baseParams("alice", "password123")
|
||||
q.Set("query", "ThisWillNeverMatchAnything")
|
||||
q.Set("f", "json")
|
||||
resp := getPath(t, srv.URL, "/rest/search3.view", q)
|
||||
j := decodeJSON(t, resp)
|
||||
if j["status"] != "ok" {
|
||||
t.Fatalf("status = %v, want ok even with no results", j["status"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user