Include radio stations in Quick Search
This commit is contained in:
@@ -270,6 +270,7 @@ type LyricsProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RadioProvider interface {
|
type RadioProvider interface {
|
||||||
|
GetRadioStation(id string) (*RadioStation, error)
|
||||||
GetRadioStations() ([]*RadioStation, error)
|
GetRadioStations() ([]*RadioStation, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ const (
|
|||||||
ContentTypePlaylist
|
ContentTypePlaylist
|
||||||
ContentTypeTrack
|
ContentTypeTrack
|
||||||
ContentTypeGenre
|
ContentTypeGenre
|
||||||
|
ContentTypeRadioStation
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c ContentType) String() string {
|
func (c ContentType) String() string {
|
||||||
@@ -229,6 +230,8 @@ func (c ContentType) String() string {
|
|||||||
return "Playlist"
|
return "Playlist"
|
||||||
case ContentTypeGenre:
|
case ContentTypeGenre:
|
||||||
return "Genre"
|
return "Genre"
|
||||||
|
case ContentTypeRadioStation:
|
||||||
|
return "Radio station"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
@@ -243,8 +246,9 @@ type SearchResult struct {
|
|||||||
// for Album / Playlist: track count
|
// for Album / Playlist: track count
|
||||||
// Artist / Genre: album count
|
// Artist / Genre: album count
|
||||||
// Track: length (seconds)
|
// Track: length (seconds)
|
||||||
|
// for Radio: none
|
||||||
Size int
|
Size int
|
||||||
|
|
||||||
// Unset for ContentTypes Artist, Playlist, and Genre
|
// Unset for ContentTypes Artist, Playlist, Genre, and RadioStation
|
||||||
ArtistName string
|
ArtistName string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func (s *subsonicMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
var result *subsonic.SearchResult3
|
var result *subsonic.SearchResult3
|
||||||
var playlists []*subsonic.Playlist
|
var playlists []*subsonic.Playlist
|
||||||
var genres []*subsonic.Genre
|
var genres []*subsonic.Genre
|
||||||
|
var radios []*mediaprovider.RadioStation
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -60,12 +61,23 @@ func (s *subsonicMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
r, e := s.GetRadioStations()
|
||||||
|
if e == nil {
|
||||||
|
radios = sharedutil.FilterSlice(r, func(r *mediaprovider.RadioStation) bool {
|
||||||
|
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(r.Name)), queryLowerWords)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
results := mergeResults(result, playlists, genres)
|
results := mergeResults(result, playlists, genres, radios)
|
||||||
helpers.RankSearchResults(results, querySanitized, queryLowerWords)
|
helpers.RankSearchResults(results, querySanitized, queryLowerWords)
|
||||||
if len(results) > maxResults {
|
if len(results) > maxResults {
|
||||||
results = results[:maxResults]
|
results = results[:maxResults]
|
||||||
@@ -77,6 +89,7 @@ func mergeResults(
|
|||||||
searchResult *subsonic.SearchResult3,
|
searchResult *subsonic.SearchResult3,
|
||||||
matchingPlaylists []*subsonic.Playlist,
|
matchingPlaylists []*subsonic.Playlist,
|
||||||
matchingGenres []*subsonic.Genre,
|
matchingGenres []*subsonic.Genre,
|
||||||
|
matchingRadios []*mediaprovider.RadioStation,
|
||||||
) []*mediaprovider.SearchResult {
|
) []*mediaprovider.SearchResult {
|
||||||
var results []*mediaprovider.SearchResult
|
var results []*mediaprovider.SearchResult
|
||||||
|
|
||||||
@@ -131,6 +144,14 @@ func mergeResults(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, r := range matchingRadios {
|
||||||
|
results = append(results, &mediaprovider.SearchResult{
|
||||||
|
Type: mediaprovider.ContentTypeRadioStation,
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/sharedutil"
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cacheValidDurationSeconds = 60
|
const (
|
||||||
|
playlistCacheValidDurationSeconds = 60
|
||||||
|
cacheValidDurationSeconds = 120 // genres and radios aren't expected to change as much
|
||||||
|
)
|
||||||
|
|
||||||
type subsonicMediaProvider struct {
|
type subsonicMediaProvider struct {
|
||||||
client *subsonic.Client
|
client *subsonic.Client
|
||||||
@@ -29,6 +32,9 @@ type subsonicMediaProvider struct {
|
|||||||
|
|
||||||
playlistsCached []*mediaprovider.Playlist
|
playlistsCached []*mediaprovider.Playlist
|
||||||
playlistsCachedAt int64 // unix
|
playlistsCachedAt int64 // unix
|
||||||
|
|
||||||
|
radiosCached []*mediaprovider.RadioStation
|
||||||
|
radiosCachedAt int64 // unix
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
||||||
@@ -194,7 +200,7 @@ 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 {
|
if s.playlistsCached != nil && time.Now().Unix()-s.playlistsCachedAt < playlistCacheValidDurationSeconds {
|
||||||
return s.playlistsCached, nil
|
return s.playlistsCached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,11 +442,15 @@ func (s *subsonicMediaProvider) GetPlayQueue() (*mediaprovider.SavedPlayQueue, e
|
|||||||
var _ mediaprovider.RadioProvider = (*subsonicMediaProvider)(nil)
|
var _ mediaprovider.RadioProvider = (*subsonicMediaProvider)(nil)
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetRadioStations() ([]*mediaprovider.RadioStation, error) {
|
func (s *subsonicMediaProvider) GetRadioStations() ([]*mediaprovider.RadioStation, error) {
|
||||||
|
if s.radiosCached != nil && time.Now().Unix()-s.radiosCachedAt < cacheValidDurationSeconds {
|
||||||
|
return s.radiosCached, nil
|
||||||
|
}
|
||||||
|
|
||||||
rs, err := s.client.GetInternetRadioStations()
|
rs, err := s.client.GetInternetRadioStations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return sharedutil.MapSlice(rs, func(rs *subsonic.InternetRadioStation) *mediaprovider.RadioStation {
|
s.radiosCached = sharedutil.MapSlice(rs, func(rs *subsonic.InternetRadioStation) *mediaprovider.RadioStation {
|
||||||
return &mediaprovider.RadioStation{
|
return &mediaprovider.RadioStation{
|
||||||
// TODO - subsonic library is missing ID in its radiostation object. add it
|
// TODO - subsonic library is missing ID in its radiostation object. add it
|
||||||
ID: "radio-" + strings.ReplaceAll(rs.Name, " ", ""),
|
ID: "radio-" + strings.ReplaceAll(rs.Name, " ", ""),
|
||||||
@@ -448,7 +458,23 @@ func (s *subsonicMediaProvider) GetRadioStations() ([]*mediaprovider.RadioStatio
|
|||||||
HomePageURL: rs.HomePageUrl,
|
HomePageURL: rs.HomePageUrl,
|
||||||
StreamURL: rs.StreamUrl,
|
StreamURL: rs.StreamUrl,
|
||||||
}
|
}
|
||||||
}), nil
|
})
|
||||||
|
s.radiosCachedAt = time.Now().Unix()
|
||||||
|
return s.radiosCached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetRadioStation(id string) (*mediaprovider.RadioStation, error) {
|
||||||
|
rs, err := s.GetRadioStations()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
index := slices.IndexFunc(rs, func(r *mediaprovider.RadioStation) bool {
|
||||||
|
return r.ID == id
|
||||||
|
})
|
||||||
|
if index < 0 {
|
||||||
|
return nil, errors.New("radio station not found")
|
||||||
|
}
|
||||||
|
return rs[index], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
||||||
|
|||||||
@@ -234,6 +234,11 @@ func (p *PlaybackManager) LoadRadioStation(station *mediaprovider.RadioStation,
|
|||||||
p.engine.LoadRadioStation(station, queueMode)
|
p.engine.LoadRadioStation(station, queueMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) PlayRadioStation(station *mediaprovider.RadioStation) error {
|
||||||
|
p.LoadRadioStation(station, Replace)
|
||||||
|
return p.PlayFromBeginning()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Track, error)) {
|
func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Track, error)) {
|
||||||
if songs, err := fetchFn(); err != nil {
|
if songs, err := fetchFn(); err != nil {
|
||||||
log.Printf("error fetching tracks: %s", err.Error())
|
log.Printf("error fetching tracks: %s", err.Error())
|
||||||
|
|||||||
@@ -99,8 +99,7 @@ func (a *RadiosPage) load(searchOnLoad bool, scrollPos float32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *RadiosPage) onPlay(station *mediaprovider.RadioStation) {
|
func (a *RadiosPage) onPlay(station *mediaprovider.RadioStation) {
|
||||||
a.pm.LoadRadioStation(station, backend.Replace)
|
a.pm.PlayRadioStation(station)
|
||||||
a.pm.PlayFromBeginning()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *RadiosPage) onQueue(station *mediaprovider.RadioStation, next bool) {
|
func (a *RadiosPage) onQueue(station *mediaprovider.RadioStation, next bool) {
|
||||||
|
|||||||
@@ -591,6 +591,12 @@ func (c *Controller) ShowQuickSearch() {
|
|||||||
c.NavigateTo(PlaylistRoute(id))
|
c.NavigateTo(PlaylistRoute(id))
|
||||||
case mediaprovider.ContentTypeGenre:
|
case mediaprovider.ContentTypeGenre:
|
||||||
c.NavigateTo(GenreRoute(id))
|
c.NavigateTo(GenreRoute(id))
|
||||||
|
case mediaprovider.ContentTypeRadioStation:
|
||||||
|
if rp, ok := c.App.ServerManager.Server.(mediaprovider.RadioProvider); ok {
|
||||||
|
if radio, err := rp.GetRadioStation(id); err == nil {
|
||||||
|
go c.App.PlaybackManager.PlayRadioStation(radio)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
c.ClosePopUpOnEscape(pop)
|
c.ClosePopUpOnEscape(pop)
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ func placeholderIconForContentType(c mediaprovider.ContentType) fyne.Resource {
|
|||||||
return myTheme.GenreIcon
|
return myTheme.GenreIcon
|
||||||
case mediaprovider.ContentTypePlaylist:
|
case mediaprovider.ContentTypePlaylist:
|
||||||
return myTheme.PlaylistIcon
|
return myTheme.PlaylistIcon
|
||||||
|
case mediaprovider.ContentTypeRadioStation:
|
||||||
|
return myTheme.RadioIcon
|
||||||
default:
|
default:
|
||||||
return theme.WarningIcon() // unreached
|
return theme.WarningIcon() // unreached
|
||||||
}
|
}
|
||||||
@@ -284,6 +286,8 @@ func (s *searchResult) Update(result *mediaprovider.SearchResult) {
|
|||||||
secondaryText = result.ArtistName
|
secondaryText = result.ArtistName
|
||||||
case mediaprovider.ContentTypePlaylist:
|
case mediaprovider.ContentTypePlaylist:
|
||||||
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("track", result.Size))
|
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("track", result.Size))
|
||||||
|
case mediaprovider.ContentTypeRadioStation:
|
||||||
|
fallthrough // result.Size is always 0 for radios
|
||||||
case mediaprovider.ContentTypeGenre:
|
case mediaprovider.ContentTypeGenre:
|
||||||
if result.Size > 0 {
|
if result.Size > 0 {
|
||||||
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size))
|
secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size))
|
||||||
|
|||||||
Reference in New Issue
Block a user