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 }