178 lines
5.1 KiB
Go
178 lines
5.1 KiB
Go
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
|
|
}
|