update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
This commit is contained in:
156
server/subsonic/searching.go
Normal file
156
server/subsonic/searching.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package subsonic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/sanitize"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type searchParams struct {
|
||||
query string
|
||||
artistCount int
|
||||
artistOffset int
|
||||
albumCount int
|
||||
albumOffset int
|
||||
songCount int
|
||||
songOffset int
|
||||
}
|
||||
|
||||
func (api *Router) getSearchParams(r *http.Request) (*searchParams, error) {
|
||||
p := req.Params(r)
|
||||
sp := &searchParams{}
|
||||
sp.query = p.StringOr("query", `""`)
|
||||
sp.artistCount = p.IntOr("artistCount", 20)
|
||||
sp.artistOffset = p.IntOr("artistOffset", 0)
|
||||
sp.albumCount = p.IntOr("albumCount", 20)
|
||||
sp.albumOffset = p.IntOr("albumOffset", 0)
|
||||
sp.songCount = p.IntOr("songCount", 20)
|
||||
sp.songOffset = p.IntOr("songOffset", 0)
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
type searchFunc[T any] func(q string, offset int, size int, options ...model.QueryOptions) (T, error)
|
||||
|
||||
func callSearch[T any](ctx context.Context, s searchFunc[T], q string, offset, size int, result *T, options ...model.QueryOptions) func() error {
|
||||
return func() error {
|
||||
if size == 0 {
|
||||
return nil
|
||||
}
|
||||
typ := strings.TrimPrefix(reflect.TypeOf(*result).String(), "model.")
|
||||
var err error
|
||||
start := time.Now()
|
||||
*result, err = s(q, offset, size, options...)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error searching "+typ, "query", q, "elapsed", time.Since(start), err)
|
||||
} else {
|
||||
log.Trace(ctx, "Search for "+typ+" completed", "query", q, "elapsed", time.Since(start))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (api *Router) searchAll(ctx context.Context, sp *searchParams, musicFolderIds []int) (mediaFiles model.MediaFiles, albums model.Albums, artists model.Artists) {
|
||||
start := time.Now()
|
||||
q := sanitize.Accents(strings.ToLower(strings.TrimSuffix(sp.query, "*")))
|
||||
|
||||
// Create query options for library filtering
|
||||
var options []model.QueryOptions
|
||||
var artistOptions []model.QueryOptions
|
||||
if len(musicFolderIds) > 0 {
|
||||
// For MediaFiles and Albums, use direct library_id filter
|
||||
options = append(options, model.QueryOptions{
|
||||
Filters: Eq{"library_id": musicFolderIds},
|
||||
})
|
||||
// For Artists, use the repository's built-in library filtering mechanism
|
||||
// which properly handles the library_artist table joins
|
||||
// TODO Revisit library filtering in sql_base_repository.go
|
||||
artistOptions = append(artistOptions, model.QueryOptions{
|
||||
Filters: Eq{"library_artist.library_id": musicFolderIds},
|
||||
})
|
||||
}
|
||||
|
||||
// Run searches in parallel
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.Go(callSearch(ctx, api.ds.MediaFile(ctx).Search, q, sp.songOffset, sp.songCount, &mediaFiles, options...))
|
||||
g.Go(callSearch(ctx, api.ds.Album(ctx).Search, q, sp.albumOffset, sp.albumCount, &albums, options...))
|
||||
g.Go(callSearch(ctx, api.ds.Artist(ctx).Search, q, sp.artistOffset, sp.artistCount, &artists, artistOptions...))
|
||||
err := g.Wait()
|
||||
if err == nil {
|
||||
log.Debug(ctx, fmt.Sprintf("Search resulted in %d songs, %d albums and %d artists",
|
||||
len(mediaFiles), len(albums), len(artists)), "query", sp.query, "elapsedTime", time.Since(start))
|
||||
} else {
|
||||
log.Warn(ctx, "Search was interrupted", "query", sp.query, "elapsedTime", time.Since(start), err)
|
||||
}
|
||||
return mediaFiles, albums, artists
|
||||
}
|
||||
|
||||
func (api *Router) Search2(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
sp, err := api.getSearchParams(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get optional library IDs from musicFolderId parameter
|
||||
musicFolderIds, err := selectedMusicFolderIds(r, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mfs, als, as := api.searchAll(ctx, sp, musicFolderIds)
|
||||
|
||||
response := newResponse()
|
||||
searchResult2 := &responses.SearchResult2{}
|
||||
searchResult2.Artist = slice.Map(as, func(artist model.Artist) responses.Artist {
|
||||
a := responses.Artist{
|
||||
Id: artist.ID,
|
||||
Name: artist.Name,
|
||||
UserRating: int32(artist.Rating),
|
||||
CoverArt: artist.CoverArtID().String(),
|
||||
ArtistImageUrl: public.ImageURL(r, artist.CoverArtID(), 600),
|
||||
}
|
||||
if artist.Starred {
|
||||
a.Starred = artist.StarredAt
|
||||
}
|
||||
return a
|
||||
})
|
||||
searchResult2.Album = slice.MapWithArg(als, ctx, childFromAlbum)
|
||||
searchResult2.Song = slice.MapWithArg(mfs, ctx, childFromMediaFile)
|
||||
response.SearchResult2 = searchResult2
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (api *Router) Search3(r *http.Request) (*responses.Subsonic, error) {
|
||||
ctx := r.Context()
|
||||
sp, err := api.getSearchParams(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get optional library IDs from musicFolderId parameter
|
||||
musicFolderIds, err := selectedMusicFolderIds(r, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mfs, als, as := api.searchAll(ctx, sp, musicFolderIds)
|
||||
|
||||
response := newResponse()
|
||||
searchResult3 := &responses.SearchResult3{}
|
||||
searchResult3.Artist = slice.MapWithArg(as, r, toArtistID3)
|
||||
searchResult3.Album = slice.MapWithArg(als, ctx, buildAlbumID3)
|
||||
searchResult3.Song = slice.MapWithArg(mfs, ctx, childFromMediaFile)
|
||||
response.SearchResult3 = searchResult3
|
||||
return response, nil
|
||||
}
|
||||
Reference in New Issue
Block a user