add SearchAll function to MediaProvider
This commit is contained in:
@@ -57,6 +57,8 @@ type MediaProvider interface {
|
|||||||
|
|
||||||
SearchAlbums(searchQuery string, filter AlbumFilter) AlbumIterator
|
SearchAlbums(searchQuery string, filter AlbumFilter) AlbumIterator
|
||||||
|
|
||||||
|
SearchAll(searchQuery string, maxResults int) ([]*SearchResult, error)
|
||||||
|
|
||||||
GetRandomTracks(genre string, count int) ([]*Track, error)
|
GetRandomTracks(genre string, count int) ([]*Track, error)
|
||||||
|
|
||||||
GetSimilarTracks(artistID string, count int) ([]*Track, error)
|
GetSimilarTracks(artistID string, count int) ([]*Track, error)
|
||||||
|
|||||||
@@ -87,3 +87,45 @@ type PlaylistWithTracks struct {
|
|||||||
Playlist
|
Playlist
|
||||||
Tracks []*Track
|
Tracks []*Track
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContentType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContentTypeAlbum ContentType = iota
|
||||||
|
ContentTypeArtist
|
||||||
|
ContentTypeTrack
|
||||||
|
ContentTypePlaylist
|
||||||
|
ContentTypeGenre
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c ContentType) String() string {
|
||||||
|
switch c {
|
||||||
|
case ContentTypeAlbum:
|
||||||
|
return "Album"
|
||||||
|
case ContentTypeArtist:
|
||||||
|
return "Artist"
|
||||||
|
case ContentTypeTrack:
|
||||||
|
return "Track"
|
||||||
|
case ContentTypePlaylist:
|
||||||
|
return "Playlist"
|
||||||
|
case ContentTypeGenre:
|
||||||
|
return "Genre"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResult struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
CoverID string
|
||||||
|
Type ContentType
|
||||||
|
|
||||||
|
// for Album / Playlist: track count
|
||||||
|
// Artist / Genre: album count
|
||||||
|
// Track: length (seconds)
|
||||||
|
Size int
|
||||||
|
|
||||||
|
// Unset for ContentTypes Artist, Playlist, and Genre
|
||||||
|
ArtistName string
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package subsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) SearchAll(searchQuery string, maxResults int) ([]*mediaprovider.SearchResult, error) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var err error // only set by Search3
|
||||||
|
var result *subsonic.SearchResult3
|
||||||
|
var playlists []*subsonic.Playlist
|
||||||
|
var genres []*subsonic.Genre
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
count := strconv.Itoa(maxResults / 3)
|
||||||
|
res, e := s.client.Search3(searchQuery, map[string]string{
|
||||||
|
"artistCount": count,
|
||||||
|
"albumCount": count,
|
||||||
|
"songCount": count,
|
||||||
|
})
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
} else {
|
||||||
|
result = res
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
queryLowerWords := strings.Fields(strings.ToLower(searchQuery))
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
p, e := s.client.GetPlaylists(nil)
|
||||||
|
if e != nil {
|
||||||
|
playlists = sharedutil.FilterSlice(p, func(p *subsonic.Playlist) bool {
|
||||||
|
return allTermsMatch(strings.ToLower(p.Name), queryLowerWords)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
g, e := s.client.GetGenres()
|
||||||
|
if e != nil {
|
||||||
|
genres = sharedutil.FilterSlice(g, func(g *subsonic.Genre) bool {
|
||||||
|
return allTermsMatch(strings.ToLower(g.Name), queryLowerWords)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := mergeResults(result, playlists, genres)
|
||||||
|
//rankResults(results, queryLowerWords) // TODO
|
||||||
|
if len(results) > maxResults {
|
||||||
|
results = results[:maxResults]
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// name and terms should be pre-converted to the same case
|
||||||
|
func allTermsMatch(name string, terms []string) bool {
|
||||||
|
for _, t := range terms {
|
||||||
|
if !strings.Contains(name, t) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeResults(
|
||||||
|
searchResult *subsonic.SearchResult3,
|
||||||
|
matchingPlaylists []*subsonic.Playlist,
|
||||||
|
matchingGenres []*subsonic.Genre,
|
||||||
|
) []*mediaprovider.SearchResult {
|
||||||
|
var results []*mediaprovider.SearchResult
|
||||||
|
|
||||||
|
for _, al := range searchResult.Album {
|
||||||
|
results = append(results, &mediaprovider.SearchResult{
|
||||||
|
Type: mediaprovider.ContentTypeAlbum,
|
||||||
|
Name: al.Name,
|
||||||
|
ArtistName: getNameString(al.Artist, al.Artists),
|
||||||
|
Size: al.SongCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ar := range searchResult.Artist {
|
||||||
|
results = append(results, &mediaprovider.SearchResult{
|
||||||
|
Type: mediaprovider.ContentTypeArtist,
|
||||||
|
Name: ar.Name,
|
||||||
|
Size: ar.AlbumCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tr := range searchResult.Song {
|
||||||
|
results = append(results, &mediaprovider.SearchResult{
|
||||||
|
Type: mediaprovider.ContentTypeTrack,
|
||||||
|
Name: tr.Title,
|
||||||
|
ArtistName: getNameString(tr.Artist, tr.Artists),
|
||||||
|
Size: tr.Duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func rankResults(results []*mediaprovider.SearchResult, queryTerms []string) {
|
||||||
|
// TODO
|
||||||
|
sort.Slice(results, func(a, b int) bool {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// select Subsonic single-valued name or join OpenSubsonic multi-valued names
|
||||||
|
func getNameString(singleName string, idNames []subsonic.IDName) string {
|
||||||
|
if len(idNames) == 0 {
|
||||||
|
return singleName
|
||||||
|
}
|
||||||
|
names := sharedutil.MapSlice(idNames, func(a subsonic.IDName) string {
|
||||||
|
return a.Name
|
||||||
|
})
|
||||||
|
return strings.Join(names, ", ")
|
||||||
|
}
|
||||||
@@ -14,9 +14,17 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/sharedutil"
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cacheValidDurationSeconds = 60
|
||||||
|
|
||||||
type subsonicMediaProvider struct {
|
type subsonicMediaProvider struct {
|
||||||
client *subsonic.Client
|
client *subsonic.Client
|
||||||
prefetchCoverCB func(coverArtID string)
|
prefetchCoverCB func(coverArtID string)
|
||||||
|
|
||||||
|
genresCached []*mediaprovider.Genre
|
||||||
|
genresCachedAt int64 // unix
|
||||||
|
|
||||||
|
playlistsCached []*mediaprovider.Playlist
|
||||||
|
playlistsCachedAt int64 // unix
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
||||||
@@ -139,17 +147,23 @@ func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
|
func (s *subsonicMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
|
||||||
|
if s.genresCached != nil && time.Now().Unix()-s.genresCachedAt < cacheValidDurationSeconds {
|
||||||
|
return s.genresCached, nil
|
||||||
|
}
|
||||||
|
|
||||||
g, err := s.client.GetGenres()
|
g, err := s.client.GetGenres()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre {
|
s.genresCached = sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre {
|
||||||
return &mediaprovider.Genre{
|
return &mediaprovider.Genre{
|
||||||
Name: g.Name,
|
Name: g.Name,
|
||||||
AlbumCount: g.AlbumCount,
|
AlbumCount: g.AlbumCount,
|
||||||
TrackCount: g.SongCount,
|
TrackCount: g.SongCount,
|
||||||
}
|
}
|
||||||
}), nil
|
})
|
||||||
|
s.genresCachedAt = time.Now().Unix()
|
||||||
|
return s.genresCached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.PlaylistWithTracks, error) {
|
func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.PlaylistWithTracks, error) {
|
||||||
@@ -165,11 +179,17 @@ func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.P
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) {
|
func (s *subsonicMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) {
|
||||||
|
if s.playlistsCached != nil && time.Now().Unix()-s.playlistsCachedAt < cacheValidDurationSeconds {
|
||||||
|
return s.playlistsCached, nil
|
||||||
|
}
|
||||||
|
|
||||||
pl, err := s.client.GetPlaylists(map[string]string{})
|
pl, err := s.client.GetPlaylists(map[string]string{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return sharedutil.MapSlice(pl, toPlaylist), nil
|
s.playlistsCached = sharedutil.MapSlice(pl, toPlaylist)
|
||||||
|
s.playlistsCachedAt = time.Now().Unix()
|
||||||
|
return s.playlistsCached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]*mediaprovider.Track, error) {
|
func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]*mediaprovider.Track, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user