Files
supersonic/backend/mediaprovider/helpers/iterators.go
T
Michael Manganiello a220243279 misc: Decouple Grid view from Albums
The Grid view and related components were highly coupled to Albums,
including the filter button that is currently used only for album
filtering.

As part of the migration of Artists to Grid view, this is the smallest
possible change to convert many of the existing code to
generics/interfaces that will allow different media to be displayed and
filtered by using Grid views.

The main changes in this diff are:

* Introduction of generics for Media types (`M`) and Filter options
  (`F`), which can be later extended to other entities besides Albums.
* Complete decoupling of `GridViewPage` and related components from
  Albums (making these pages also generic).
* Refactoring of Subsonic/Jellyfin specific code to understand these new
  generics when dealing with filtering logic.
2024-03-11 00:44:20 -03:00

201 lines
5.4 KiB
Go

package helpers
import (
"log"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
)
type baseIter[M, F any] struct {
filter mediaprovider.MediaFilter[M, F]
prefetchCB func(*M)
serverPos int
fetcher func(offset, limit int) ([]*M, error)
prefetched []*M
prefetchedPos int
done bool
}
type AlbumFetchFn func(offset, limit int) ([]*mediaprovider.Album, error)
func NewAlbumIterator(fetchFn AlbumFetchFn, filter mediaprovider.AlbumFilter, cb func(string)) mediaprovider.AlbumIterator {
return &baseIter[mediaprovider.Album, mediaprovider.AlbumFilterOptions]{
prefetchCB: func(a *mediaprovider.Album) { cb(a.CoverArtID) },
filter: filter,
fetcher: fetchFn,
}
}
type ArtistFetchFn func(offset, limit int) ([]*mediaprovider.Artist, error)
func NewArtistIterator(fetchFn ArtistFetchFn) mediaprovider.ArtistIterator {
return &baseIter[mediaprovider.Artist, nilFilterOptions]{
fetcher: fetchFn,
filter: nilFilter[mediaprovider.Artist]{},
}
}
type TrackFetchFn func(offset, limit int) ([]*mediaprovider.Track, error)
func NewTrackIterator(fetchFn TrackFetchFn, cb func(string)) mediaprovider.TrackIterator {
return &baseIter[mediaprovider.Track, nilFilterOptions]{
prefetchCB: func(a *mediaprovider.Track) { cb(a.CoverArtID) },
filter: nilFilter[mediaprovider.Track]{},
fetcher: fetchFn,
}
}
func (r *baseIter[M, F]) Next() *M {
if r.done {
return nil
}
if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) {
a := r.prefetched[r.prefetchedPos]
r.prefetchedPos++
return a
}
r.prefetched = nil
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())
items = nil
}
if len(items) == 0 {
r.done = true
return nil
}
r.serverPos += len(items)
if !r.filter.IsNil() {
items = sharedutil.FilterSlice(items, r.filter.Matches)
}
r.prefetched = items
if len(items) > 0 {
break
}
}
r.prefetchedPos = 1
if r.prefetchCB != nil {
for _, item := range r.prefetched {
go r.prefetchCB(item)
}
}
return r.prefetched[0]
}
type randomAlbumIter 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 NewRandomAlbumIter(deterministicFetcher, randomFetcher AlbumFetchFn, filter mediaprovider.AlbumFilter, prefetchCoverCB func(string)) *randomAlbumIter {
return &randomAlbumIter{
filter: filter,
prefetchCB: prefetchCoverCB,
deterministicFetcher: deterministicFetcher,
ramdomFetcher: randomFetcher,
albumIDSet: make(map[string]bool),
}
}
func (r *randomAlbumIter) 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 && r.filter.Matches(album) {
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 r.filter.Matches(album) {
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
}
type nilFilterOptions struct{}
type nilFilter[M any] struct{}
func (n nilFilter[M]) IsNil() bool { return true }
func (n nilFilter[M]) Matches(*M) bool { return true }
func (n nilFilter[M]) Options() nilFilterOptions { return nilFilterOptions{} }
func (n nilFilter[M]) SetOptions(o nilFilterOptions) {}