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
+283
View File
@@ -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
}