diff --git a/backend/mediaprovider/helpers/iterators.go b/backend/mediaprovider/helpers/iterators.go index 03ddc1f..a7e9778 100644 --- a/backend/mediaprovider/helpers/iterators.go +++ b/backend/mediaprovider/helpers/iterators.go @@ -195,6 +195,8 @@ func (n nilFilter[M]) IsNil() bool { return true } func (n nilFilter[M]) Matches(*M) bool { return true } +func (n nilFilter[M]) Clone() mediaprovider.MediaFilter[M, nilFilterOptions] { return n } + func (n nilFilter[M]) Options() nilFilterOptions { return nilFilterOptions{} } -func (n nilFilter[M]) SetOptions(o nilFilterOptions) {} +func (n nilFilter[M]) SetOptions(options nilFilterOptions) {} diff --git a/backend/mediaprovider/jellyfin/iterators.go b/backend/mediaprovider/jellyfin/iterators.go index 6a630d5..c6c58a9 100644 --- a/backend/mediaprovider/jellyfin/iterators.go +++ b/backend/mediaprovider/jellyfin/iterators.go @@ -58,7 +58,7 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov jfSort.Field = jellyfin.SortByYear jfSort.Mode = jellyfin.SortDesc } - jfFilt := jfFilterFromFilter(filter) + jfFilt, modifiedFilter := jfFilterFromFilter(filter) fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) { al, err := j.client.GetAlbums(jellyfin.QueryOpts{ @@ -84,9 +84,9 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov } return sharedutil.MapSlice(al, toAlbum), nil } - return helpers.NewRandomAlbumIter(determFetcher, fetcher, filter, j.prefetchCoverCB) + return helpers.NewRandomAlbumIter(determFetcher, fetcher, modifiedFilter, j.prefetchCoverCB) } - return helpers.NewAlbumIterator(fetcher, filter, j.prefetchCoverCB) + return helpers.NewAlbumIterator(fetcher, modifiedFilter, j.prefetchCoverCB) } func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { @@ -151,15 +151,20 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string) mediaprovider.A } // 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 { +// and returns a modified mediaprovider filter, with now-unneeded fields zeroed out. +func jfFilterFromFilter(filter mediaprovider.AlbumFilter) (jellyfin.Filter, mediaprovider.AlbumFilter) { var jfFilt jellyfin.Filter - filterOptions := filter.Options() + // Clone the original filter to not modify its options. + // Set filters must be maintained in the original filter, as they are used for the UI. + // Modified filter options are used to ignore further filtering that was already handled by the + // Jellyfin API. + modifiedFilter := filter.Clone() + filterOptions := modifiedFilter.Options() if filterOptions.ExcludeUnfavorited { jfFilt.Favorite = true - filterOptions.ExcludeUnfavorited = false // Jellyfin will handle this filter + filterOptions.ExcludeUnfavorited = false } if filterOptions.MinYear > 0 && filterOptions.MaxYear > 0 { jfFilt.YearRange = [2]int{filterOptions.MinYear, filterOptions.MaxYear} @@ -174,7 +179,6 @@ func jfFilterFromFilter(filter mediaprovider.AlbumFilter) jellyfin.Filter { jfFilt.Genres = filterOptions.Genres filterOptions.Genres = nil - filter.SetOptions(filterOptions) - - return jfFilt + modifiedFilter.SetOptions(filterOptions) + return jfFilt, modifiedFilter } diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 5627956..2bdcccb 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -18,6 +18,7 @@ type TrackIterator = MediaIterator[Track] type MediaFilter[M, F any] interface { Options() F SetOptions(F) + Clone() MediaFilter[M, F] IsNil() bool Matches(*M) bool } @@ -33,6 +34,19 @@ type AlbumFilterOptions struct { ExcludeUnfavorited bool // mut. exc. with ExcludeFavorited } +// Clone returns a deep copy of the filter options +func (o AlbumFilterOptions) Clone() AlbumFilterOptions { + genres := make([]string, len(o.Genres)) + copy(genres, o.Genres) + return AlbumFilterOptions{ + MinYear: o.MinYear, + MaxYear: o.MaxYear, + Genres: genres, + ExcludeFavorited: o.ExcludeFavorited, + ExcludeUnfavorited: o.ExcludeUnfavorited, + } +} + type albumFilter struct { options AlbumFilterOptions } @@ -45,8 +59,13 @@ func (a albumFilter) Options() AlbumFilterOptions { return a.options } -func (a *albumFilter) SetOptions(o AlbumFilterOptions) { - a.options = o +func (a *albumFilter) SetOptions(options AlbumFilterOptions) { + a.options = options +} + +// Clone returns a deep copy of the filter +func (a albumFilter) Clone() AlbumFilter { + return NewAlbumFilter(a.options.Clone()) } // Returns true if the filter is the nil filter - i.e. matches everything diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go index b16b731..5637460 100644 --- a/backend/mediaprovider/subsonic/albumiterator.go +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -68,18 +68,22 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov // but servers do internally match against all the genres the album is categorized with. // So we must not additionally filter by genre to avoid excluding results where // the single genre returned by Subsonic isn't the one we're iterating on. - filterOptions.Genres = nil + modifiedFilter := filter.Clone() + modifiedOptions := modifiedFilter.Options() + modifiedOptions.Genres = nil + modifiedFilter.SetOptions(modifiedOptions) fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) { return s.client.GetAlbumList2("byGenre", map[string]string{"genre": genre, "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}) } - filter.SetOptions(filterOptions) - return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB) + return helpers.NewAlbumIterator(makeFetchFn(fetchFn), modifiedFilter, s.prefetchCoverCB) } if sortOrder == "" && filterOptions.ExcludeUnfavorited { - filterOptions.ExcludeUnfavorited = false // we're already filtering by this - filter.SetOptions(filterOptions) - return s.baseIterFromSimpleSortOrder("starred", filter) + modifiedFilter := filter.Clone() + modifiedOptions := modifiedFilter.Options() + modifiedOptions.ExcludeUnfavorited = false // we're already filtering by this + modifiedFilter.SetOptions(modifiedOptions) + return s.baseIterFromSimpleSortOrder("starred", modifiedFilter) } if sortOrder == "" { sortOrder = AlbumSortRecentlyAdded // default