Include radio stations in Quick Search

This commit is contained in:
Drew Weymouth
2024-07-01 08:04:10 -07:00
parent 5d13e7bf96
commit 59d979ba80
8 changed files with 74 additions and 8 deletions
+1
View File
@@ -270,6 +270,7 @@ type LyricsProvider interface {
} }
type RadioProvider interface { type RadioProvider interface {
GetRadioStation(id string) (*RadioStation, error)
GetRadioStations() ([]*RadioStation, error) GetRadioStations() ([]*RadioStation, error)
} }
+5 -1
View File
@@ -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
} }
+22 -1
View File
@@ -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 {
+5
View File
@@ -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())
+1 -2
View File
@@ -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) {
+6
View File
@@ -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)
+4
View File
@@ -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))