From 2cb002dde019cbdf9749c21e4eb809ee2ff99e03 Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Mon, 4 Mar 2024 01:15:10 -0300 Subject: [PATCH] misc: Implement Iterator approach for artists First step towards improving sorting for Artists. Use the same iterator approach as albums and tracks. --- backend/mediaprovider/helpers/iterators.go | 15 +++- backend/mediaprovider/jellyfin/iterators.go | 34 ++++++++ .../jellyfin/jellyfinmediaprovider.go | 8 -- backend/mediaprovider/mediaprovider.go | 8 +- .../mediaprovider/subsonic/artistiterator.go | 79 +++++++++++++++++++ .../subsonic/subsonicmediaprovider.go | 14 ---- ui/browsing/artistspage.go | 12 ++- 7 files changed, 140 insertions(+), 30 deletions(-) create mode 100644 backend/mediaprovider/subsonic/artistiterator.go diff --git a/backend/mediaprovider/helpers/iterators.go b/backend/mediaprovider/helpers/iterators.go index 5898813..86c8d95 100644 --- a/backend/mediaprovider/helpers/iterators.go +++ b/backend/mediaprovider/helpers/iterators.go @@ -32,6 +32,15 @@ func NewAlbumIterator(fetchFn AlbumFetchFn, filter mediaprovider.AlbumFilter, cb } } +type ArtistFetchFn func(offset, limit int) ([]*mediaprovider.Artist, error) + +func NewArtistIterator(fetchFn ArtistFetchFn) mediaprovider.ArtistIterator { + return &baseIter[mediaprovider.Artist]{ + fetcher: fetchFn, + filter: nilFilter[mediaprovider.Artist]{}, + } +} + type TrackFetchFn func(offset, limit int) ([]*mediaprovider.Track, error) func NewTrackIterator(fetchFn TrackFetchFn, cb func(string)) mediaprovider.TrackIterator { @@ -52,7 +61,7 @@ func (r *baseIter[T]) Next() *T { return a } r.prefetched = nil - for { // keep fetching until we are done or have mathcing results + for { // keep fetching until we are done or have matching results items, err := r.fetcher(r.serverPos, 20) if err != nil { log.Printf("error fetching items: %s", err.Error()) @@ -73,8 +82,8 @@ func (r *baseIter[T]) Next() *T { } r.prefetchedPos = 1 if r.prefetchCB != nil { - for _, album := range r.prefetched { - go r.prefetchCB(album) + for _, item := range r.prefetched { + go r.prefetchCB(item) } } return r.prefetched[0] diff --git a/backend/mediaprovider/jellyfin/iterators.go b/backend/mediaprovider/jellyfin/iterators.go index e7f299c..311a9eb 100644 --- a/backend/mediaprovider/jellyfin/iterators.go +++ b/backend/mediaprovider/jellyfin/iterators.go @@ -16,6 +16,8 @@ const ( AlbumSortArtistAZ string = "Artist (A-Z)" AlbumSortYearAscending string = "Year (ascending)" AlbumSortYearDescending string = "Year (descending)" + + ArtistSortNameAZ string = "Name (A-Z)" ) func (j *jellyfinMediaProvider) AlbumSortOrders() []string { @@ -29,6 +31,12 @@ func (j *jellyfinMediaProvider) AlbumSortOrders() []string { } } +func (j *jellyfinMediaProvider) ArtistSortOrders() []string { + return []string{ + ArtistSortNameAZ, + } +} + func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { var jfSort jellyfin.Sort switch sortOrder { @@ -116,6 +124,32 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider. return helpers.NewTrackIterator(fetcher, j.prefetchCoverCB) } +func (j *jellyfinMediaProvider) IterateArtists(sortOrder string) mediaprovider.ArtistIterator { + var jfSort jellyfin.Sort + + if sortOrder == "" { + sortOrder = ArtistSortNameAZ // default + } + switch sortOrder { + case ArtistSortNameAZ: + jfSort.Field = jellyfin.SortByName + jfSort.Mode = jellyfin.SortAsc + } + + fetcher := func(offs, limit int) ([]*mediaprovider.Artist, error) { + ar, err := j.client.GetAlbumArtists(jellyfin.QueryOpts{ + Sort: jfSort, + Paging: jellyfin.Paging{StartIndex: offs, Limit: limit}, + }) + if err != nil { + return nil, err + } + return sharedutil.MapSlice(ar, toArtist), nil + } + + return helpers.NewArtistIterator(fetcher) +} + // Creates the Jellyfin filter to implement the given mediaprovider filter, // and zeros out the now-unneeded fields in the mediaprovider filter. func jfFilterFromFilter(filter *mediaprovider.AlbumFilter) jellyfin.Filter { diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index dd862d9..c4b40f6 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -159,14 +159,6 @@ func (j *jellyfinMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A }, nil } -func (j *jellyfinMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) { - ar, err := j.client.GetAlbumArtists(jellyfin.QueryOpts{}) - if err != nil { - return nil, err - } - return sharedutil.MapSlice(ar, toArtist), nil -} - func (j *jellyfinMediaProvider) GetTrack(trackID string) (*mediaprovider.Track, error) { tr, err := j.client.GetSong(trackID) if err != nil { diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index f7fe0b1..eca6fd6 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -42,6 +42,10 @@ func (f AlbumFilter) Matches(album *Album) bool { return genresMatch(f.Genres, album.Genres) } +type ArtistIterator interface { + Next() *Artist +} + type AlbumIterator interface { Next() *Album } @@ -103,7 +107,9 @@ type MediaProvider interface { GetSimilarTracks(artistID string, count int) ([]*Track, error) - GetArtists() ([]*Artist, error) + ArtistSortOrders() []string + + IterateArtists(sortOrder string) ArtistIterator GetGenres() ([]*Genre, error) diff --git a/backend/mediaprovider/subsonic/artistiterator.go b/backend/mediaprovider/subsonic/artistiterator.go new file mode 100644 index 0000000..97281e4 --- /dev/null +++ b/backend/mediaprovider/subsonic/artistiterator.go @@ -0,0 +1,79 @@ +package subsonic + +import ( + "cmp" + "log" + "slices" + "strings" + + "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/backend/mediaprovider/helpers" + "github.com/dweymouth/supersonic/sharedutil" +) + +const ( + ArtistSortNameAZ string = "Name (A-Z)" +) + +func (s *subsonicMediaProvider) ArtistSortOrders() []string { + return []string{ + ArtistSortNameAZ, + } +} + +func (s *subsonicMediaProvider) IterateArtists(sortOrder string) mediaprovider.ArtistIterator { + if sortOrder == "" { + sortOrder = ArtistSortNameAZ // default + } + switch sortOrder { + case ArtistSortNameAZ: + return s.baseArtistIterFromSimpleSortOrder( + func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 { + slices.SortFunc(artists, func(a, b *subsonic.ArtistID3) int { + return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) + }) + return artists + }, + ) + default: + log.Printf("Undefined artist sort order: %s", sortOrder) + return nil + } +} + +func (s *subsonicMediaProvider) baseArtistIterFromSimpleSortOrder(sortFn func([]*subsonic.ArtistID3) []*subsonic.ArtistID3) mediaprovider.ArtistIterator { + return helpers.NewArtistIterator(s.artistFetchFnFromStandardSort(sortFn)) +} + +func (s *subsonicMediaProvider) artistFetchFnFromStandardSort(sortFn func([]*subsonic.ArtistID3) []*subsonic.ArtistID3) helpers.ArtistFetchFn { + return makeArtistFetchFn(func(offset, limit int) ([]*subsonic.ArtistID3, error) { + // When the iterator asks for a second page of results, return nil, as Subsonic does not support pagination for artists. + if offset > 0 { + return nil, nil + } + + idxs, err := s.client.GetArtists(map[string]string{}) + if err != nil { + return nil, err + } + var artists []*subsonic.ArtistID3 + for _, idx := range idxs.Index { + for _, ar := range idx.Artist { + artists = append(artists, ar) + } + } + artists = sortFn(artists) + return artists, nil + }) +} + +func makeArtistFetchFn(subsonicFetchFn func(offset, limit int) ([]*subsonic.ArtistID3, error)) helpers.ArtistFetchFn { + return func(offset, limit int) ([]*mediaprovider.Artist, error) { + ar, err := subsonicFetchFn(offset, limit) + if err != nil { + return nil, err + } + return sharedutil.MapSlice(ar, toArtistFromID3), nil + } +} diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 5c858eb..ae64b94 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -130,20 +130,6 @@ func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A }, nil } -func (s *subsonicMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) { - idxs, err := s.client.GetArtists(map[string]string{}) - if err != nil { - return nil, err - } - var artists []*mediaprovider.Artist - for _, idx := range idxs.Index { - for _, ar := range idx.Artist { - artists = append(artists, toArtistFromID3(ar)) - } - } - return artists, nil -} - func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) { params := map[string]string{} if size > 0 { diff --git a/ui/browsing/artistspage.go b/ui/browsing/artistspage.go index 40d610f..b34de3b 100644 --- a/ui/browsing/artistspage.go +++ b/ui/browsing/artistspage.go @@ -2,7 +2,6 @@ package browsing import ( "fmt" - "log" "strings" "fyne.io/fyne/v2" @@ -111,9 +110,14 @@ func (a *ArtistsPage) Reload() { } func (a *ArtistsPage) load() { - artists, err := a.mp.GetArtists() - if err != nil { - log.Printf("error loading artists: %v", err.Error()) + iter := a.mp.IterateArtists("") + var artists []*mediaprovider.Artist + for { + artist := iter.Next() + if artist == nil { + break + } + artists = append(artists, artist) } a.artists = artists a.onSearched(a.searcher.Entry.Text, true)