Subsonic: album/song lists (getAlbumList/2, getRandomSongs, getSongsByGenre, getNowPlaying, getStarred/2) and search (search, search2, search3)

This commit is contained in:
2026-08-23 00:08:06 -05:00
parent f40a011c46
commit a297454065
6 changed files with 742 additions and 0 deletions
+177
View File
@@ -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
}