From c8255517f655e925d40303223a2f2798c2d45fbb Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 11 Nov 2023 18:37:27 -0800 Subject: [PATCH] extract random iter helper to be reused with Jellyfin --- .../mediaprovider/helpers/albumiterator.go | 143 ++++++++++++++++++ backend/mediaprovider/mediaprovider.go | 6 + backend/mediaprovider/server.go | 7 - .../mediaprovider/subsonic/albumiterator.go | 91 ++--------- 4 files changed, 163 insertions(+), 84 deletions(-) create mode 100644 backend/mediaprovider/helpers/albumiterator.go delete mode 100644 backend/mediaprovider/server.go diff --git a/backend/mediaprovider/helpers/albumiterator.go b/backend/mediaprovider/helpers/albumiterator.go new file mode 100644 index 0000000..f0f3eea --- /dev/null +++ b/backend/mediaprovider/helpers/albumiterator.go @@ -0,0 +1,143 @@ +package helpers + +import ( + "log" + "strings" + + "github.com/dweymouth/supersonic/backend/mediaprovider" +) + +type AlbumFetchFn func(offset, limit int) ([]*mediaprovider.Album, error) + +type randomIter struct { + filter mediaprovider.AlbumFilter + prefetchCB func(coverArtID string) + albumIDSet map[string]bool + prefetched []*mediaprovider.Album + prefetchedPos int + // Random iter works in two phases - phase 1 by requesting random + // albums from the server. Since the Subsonic API provides no way + // of paginating a single random sort, we may get albums back twice. + // We use albumIDSet to keep track of which albums have already been returned. + // Once we start getting back too many already-returned albums, + // switch to requesting more albums from a deterministic sort order. + deterministicFetcher AlbumFetchFn + ramdomFetcher AlbumFetchFn + phaseTwo bool + offset int + done bool +} + +func NewRandomIter(deterministicFetcher, randomFetcher AlbumFetchFn, filter mediaprovider.AlbumFilter, prefetchCoverCB func(string)) *randomIter { + return &randomIter{ + filter: filter, + prefetchCB: prefetchCoverCB, + deterministicFetcher: deterministicFetcher, + ramdomFetcher: randomFetcher, + albumIDSet: make(map[string]bool), + } +} + +func (r *randomIter) Next() *mediaprovider.Album { + if r.done { + return nil + } + + // repeat fetch task until we have matching results + // or we reach the end (handled via short circuit return) + for len(r.prefetched) == 0 { + if r.phaseTwo { + // fetch albums from deterministic order + albums, err := r.deterministicFetcher(r.offset, 25) + if err != nil { + log.Printf("error fetching albums: %s", err.Error()) + albums = nil + } + if len(albums) == 0 { + r.done = true + r.albumIDSet = nil + return nil + } + r.offset += len(albums) + for _, album := range albums { + if _, ok := r.albumIDSet[album.ID]; !ok && filterMatches(r.filter, album, false) { + r.prefetched = append(r.prefetched, album) + if r.prefetchCB != nil { + go r.prefetchCB(album.CoverArtID) + } + r.albumIDSet[album.ID] = true + } + } + } else { + albums, err := r.ramdomFetcher(0 /*offset - doesn't matter for random*/, 25) + if err != nil { + log.Println(err) + r.done = true + r.albumIDSet = nil + return nil + } + var hitCount int + for _, album := range albums { + if _, ok := r.albumIDSet[album.ID]; !ok { + // still need to keep track even if album is not matched + // by the filter because we need to know when to move to phase two + hitCount++ + r.albumIDSet[album.ID] = true + if filterMatches(r.filter, album, false) { + r.prefetched = append(r.prefetched, album) + if r.prefetchCB != nil { + go r.prefetchCB(album.CoverArtID) + } + } + } + } + if successRatio := float64(hitCount) / float64(25); successRatio < 0.3 { + r.phaseTwo = true + } + } + } + + // return from prefetched results + if len(r.prefetched) > 0 { + a := r.prefetched[r.prefetchedPos] + r.prefetchedPos++ + if r.prefetchedPos == len(r.prefetched) { + r.prefetched = nil + r.prefetchedPos = 0 + } + + return a + } + + return nil +} + +func filterMatches(f mediaprovider.AlbumFilter, album *mediaprovider.Album, ignoreGenre bool) bool { + if album == nil { + return false + } + if f.ExcludeFavorited && album.Favorite { + return false + } + if f.ExcludeUnfavorited && !album.Favorite { + return false + } + if y := album.Year; y < f.MinYear || (f.MaxYear > 0 && y > f.MaxYear) { + return false + } + if ignoreGenre || len(f.Genres) == 0 { + return true + } + return genresMatch(f.Genres, album.Genres) +} + +func genresMatch(filterGenres, albumGenres []string) bool { + for _, g1 := range filterGenres { + for _, g2 := range albumGenres { + if strings.EqualFold(g1, g2) { + return true + } + } + } + return false +} diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 837afb4..82a55dc 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -34,6 +34,12 @@ type Favorites struct { Tracks []*Track } +type Server interface { + Ping() bool + Login(username, password string) error + MediaProvider() MediaProvider +} + type MediaProvider interface { SetPrefetchCoverCallback(cb func(coverArtID string)) diff --git a/backend/mediaprovider/server.go b/backend/mediaprovider/server.go deleted file mode 100644 index 94e7218..0000000 --- a/backend/mediaprovider/server.go +++ /dev/null @@ -1,7 +0,0 @@ -package mediaprovider - -type Server interface { - Ping() bool - Login(username, password string) error - MediaProvider() MediaProvider -} diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go index df67587..a2bfee8 100644 --- a/backend/mediaprovider/subsonic/albumiterator.go +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -7,6 +7,7 @@ import ( "github.com/dweymouth/go-subsonic/subsonic" "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/backend/mediaprovider/helpers" "github.com/dweymouth/supersonic/sharedutil" ) @@ -276,85 +277,21 @@ type randomIter struct { done bool } -func (s *subsonicMediaProvider) newRandomIter(filter mediaprovider.AlbumFilter, cb func(string)) *randomIter { - return &randomIter{ - filter: filter, - prefetchCB: cb, - s: s.client, - albumIDSet: make(map[string]bool), - } -} - -func (r *randomIter) Next() *mediaprovider.Album { - if r.done { - return nil - } - - // repeat fetch task until we have matching results - // or we reach the end (handled via short circuit return) - for len(r.prefetched) == 0 { - if r.phaseTwo { - // fetch albums from deterministic order - albums, err := r.s.GetAlbumList2("newest", map[string]string{"size": "25", "offset": strconv.Itoa(r.offset)}) +func (s *subsonicMediaProvider) newRandomIter(filter mediaprovider.AlbumFilter, cb func(string)) mediaprovider.AlbumIterator { + return helpers.NewRandomIter( + func(offset, limit int) ([]*mediaprovider.Album, error) { + al, err := s.client.GetAlbumList2("newest", map[string]string{"size": strconv.Itoa(limit), "offset": strconv.Itoa(offset)}) if err != nil { - log.Printf("error fetching albums: %s", err.Error()) - albums = nil + return nil, err } - if len(albums) == 0 { - r.done = true - r.albumIDSet = nil - return nil - } - r.offset += len(albums) - for _, album := range albums { - if _, ok := r.albumIDSet[album.ID]; !ok && filterMatches(r.filter, album, false) { - r.prefetched = append(r.prefetched, album) - if r.prefetchCB != nil { - go r.prefetchCB(album.CoverArt) - } - r.albumIDSet[album.ID] = true - } - } - } else { - albums, err := r.s.GetAlbumList2("random", map[string]string{"size": "25"}) + return sharedutil.MapSlice(al, toAlbum), nil + }, + func(_, limit int) ([]*mediaprovider.Album, error) { + al, err := s.client.GetAlbumList2("random", map[string]string{"size": strconv.Itoa(limit)}) if err != nil { - log.Println(err) - r.done = true - r.albumIDSet = nil - return nil + return nil, err } - var hitCount int - for _, album := range albums { - if _, ok := r.albumIDSet[album.ID]; !ok { - // still need to keep track even if album is not matched - // by the filter because we need to know when to move to phase two - hitCount++ - r.albumIDSet[album.ID] = true - if filterMatches(r.filter, album, false) { - r.prefetched = append(r.prefetched, album) - if r.prefetchCB != nil { - go r.prefetchCB(album.CoverArt) - } - } - } - } - if successRatio := float64(hitCount) / float64(25); successRatio < 0.3 { - r.phaseTwo = true - } - } - } - - // return from prefetched results - if len(r.prefetched) > 0 { - a := r.prefetched[r.prefetchedPos] - r.prefetchedPos++ - if r.prefetchedPos == len(r.prefetched) { - r.prefetched = nil - r.prefetchedPos = 0 - } - - return toAlbum(a) - } - - return nil + return sharedutil.MapSlice(al, toAlbum), nil + }, + filter, s.prefetchCoverCB) }