Merge pull request #366 from adamantike/feat/add-sorting-options-for-artists

feat: Add sorting options for Artists
This commit is contained in:
Drew Weymouth
2024-04-14 10:20:36 -07:00
committed by GitHub
4 changed files with 168 additions and 50 deletions
@@ -0,0 +1,126 @@
package jellyfin
import (
"slices"
"github.com/dweymouth/go-jellyfin"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/mediaprovider/helpers"
"github.com/dweymouth/supersonic/sharedutil"
)
const (
ArtistSortAlbumCount string = "Album Count"
ArtistSortNameAZ string = "Name (A-Z)"
ArtistSortRandom string = "Random"
)
func (j *jellyfinMediaProvider) ArtistSortOrders() []string {
return []string{
ArtistSortAlbumCount,
ArtistSortNameAZ,
ArtistSortRandom,
}
}
func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
var jfSort jellyfin.Sort
var disablePagination bool
var sortFn func([]*jellyfin.Artist) []*jellyfin.Artist
if sortOrder == "" {
sortOrder = ArtistSortNameAZ // default
}
switch sortOrder {
case ArtistSortAlbumCount:
// Pagination needs to be disabled, to retrieve all results in a single request, and correctly sort them.
disablePagination = true
sortFn = func(artists []*jellyfin.Artist) []*jellyfin.Artist {
slices.SortStableFunc(artists, func(a, b *jellyfin.Artist) int {
return b.AlbumCount - a.AlbumCount
})
return artists
}
case ArtistSortNameAZ:
jfSort.Field = jellyfin.SortByName
jfSort.Mode = jellyfin.SortAsc
case ArtistSortRandom:
jfSort.Field = jellyfin.SortByRandom
}
fetcher := makeArtistFetchFn(
func(offs, limit int) ([]*jellyfin.Artist, error) {
if disablePagination && offs > 0 {
return nil, nil
}
var paging jellyfin.Paging
if !disablePagination {
paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
}
return j.client.GetAlbumArtists(jellyfin.QueryOpts{
Sort: jfSort,
Paging: paging,
})
},
sortFn,
)
return helpers.NewArtistIterator(fetcher, filter, j.prefetchCoverCB)
}
func (j *jellyfinMediaProvider) SearchArtists(searchQuery string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
// TODO: Jellyfin API is not returning search results for artists.
// Uncomment the following code once the issue is resolved.
// Related issue: https://github.com/jellyfin/jellyfin/issues/8222
// fetcher := makeArtistFetchFn(
// func(offs, limit int) ([]*jellyfin.Artist, error) {
// log.Printf("Searching for artists: %s", searchQuery)
// sr, err := j.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{StartIndex: offs, Limit: limit})
// if err != nil {
// return nil, err
// }
// log.Printf("Found %d artists", len(sr.Artists))
// for _, a := range sr.Artists {
// log.Printf("Artist: %s", a.Name)
// }
// return sr.Artists, nil
// },
// nil,
// )
// return helpers.NewArtistIterator(fetcher, filter, j.prefetchCoverCB)
modifiedFilter := filter.Clone()
modifiedOptions := modifiedFilter.Options()
modifiedOptions.SearchQuery = searchQuery
modifiedFilter.SetOptions(modifiedOptions)
fetcher := makeArtistFetchFn(
func(offs, limit int) ([]*jellyfin.Artist, error) {
return j.client.GetAlbumArtists(jellyfin.QueryOpts{
Sort: jellyfin.Sort{
Field: jellyfin.SortByName,
Mode: jellyfin.SortAsc,
},
Paging: jellyfin.Paging{StartIndex: offs, Limit: limit},
})
},
nil,
)
return helpers.NewArtistIterator(fetcher, modifiedFilter, j.prefetchCoverCB)
}
func makeArtistFetchFn(
fetchFn func(offset, limit int) ([]*jellyfin.Artist, error),
sortFn func([]*jellyfin.Artist) []*jellyfin.Artist,
) helpers.ArtistFetchFn {
return func(offset, limit int) ([]*mediaprovider.Artist, error) {
ar, err := fetchFn(offset, limit)
if err != nil {
return nil, err
}
if sortFn != nil {
ar = sortFn(ar)
}
return sharedutil.MapSlice(ar, toArtist), nil
}
}
@@ -16,8 +16,6 @@ const (
AlbumSortArtistAZ string = "Artist (A-Z)" AlbumSortArtistAZ string = "Artist (A-Z)"
AlbumSortYearAscending string = "Year (ascending)" AlbumSortYearAscending string = "Year (ascending)"
AlbumSortYearDescending string = "Year (descending)" AlbumSortYearDescending string = "Year (descending)"
ArtistSortNameAZ string = "Name (A-Z)"
) )
func (j *jellyfinMediaProvider) AlbumSortOrders() []string { func (j *jellyfinMediaProvider) AlbumSortOrders() []string {
@@ -31,12 +29,6 @@ func (j *jellyfinMediaProvider) AlbumSortOrders() []string {
} }
} }
func (j *jellyfinMediaProvider) ArtistSortOrders() []string {
return []string{
ArtistSortNameAZ,
}
}
func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
var jfSort jellyfin.Sort var jfSort jellyfin.Sort
switch sortOrder { switch sortOrder {
@@ -124,43 +116,6 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
return helpers.NewTrackIterator(fetcher, j.prefetchCoverCB) return helpers.NewTrackIterator(fetcher, j.prefetchCoverCB)
} }
func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) 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, filter, j.prefetchCoverCB)
}
func (j *jellyfinMediaProvider) SearchArtists(searchQuery string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
fetcher := func(offs, limit int) ([]*mediaprovider.Artist, error) {
sr, err := j.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{StartIndex: offs, Limit: limit})
if err != nil {
return nil, err
}
return sharedutil.MapSlice(sr.Artists, toArtist), nil
}
return helpers.NewArtistIterator(fetcher, filter, j.prefetchCoverCB)
}
// Creates the Jellyfin filter to implement the given mediaprovider filter, // Creates the Jellyfin filter to implement the given mediaprovider filter,
// and returns a modified mediaprovider filter, with now-unneeded fields zeroed out. // and returns a modified mediaprovider filter, with now-unneeded fields zeroed out.
func jfFilterFromFilter(filter mediaprovider.AlbumFilter) (jellyfin.Filter, mediaprovider.AlbumFilter) { func jfFilterFromFilter(filter mediaprovider.AlbumFilter) (jellyfin.Filter, mediaprovider.AlbumFilter) {
+15 -3
View File
@@ -5,6 +5,8 @@ import (
"io" "io"
"net/url" "net/url"
"strings" "strings"
"github.com/deluan/sanitize"
) )
type MediaIterator[M any] interface { type MediaIterator[M any] interface {
@@ -96,11 +98,15 @@ func (f albumFilter) Matches(album *Album) bool {
type ArtistFilter = MediaFilter[Artist, ArtistFilterOptions] type ArtistFilter = MediaFilter[Artist, ArtistFilterOptions]
type ArtistFilterOptions struct{} type ArtistFilterOptions struct {
SearchQuery string
}
// Clone returns a deep copy of the filter options // Clone returns a deep copy of the filter options
func (o ArtistFilterOptions) Clone() ArtistFilterOptions { func (o ArtistFilterOptions) Clone() ArtistFilterOptions {
return ArtistFilterOptions{} return ArtistFilterOptions{
SearchQuery: o.SearchQuery,
}
} }
type artistFilter struct { type artistFilter struct {
@@ -126,14 +132,20 @@ func (a artistFilter) Clone() ArtistFilter {
// Returns true if the filter is the nil filter - i.e. matches everything // Returns true if the filter is the nil filter - i.e. matches everything
func (a artistFilter) IsNil() bool { func (a artistFilter) IsNil() bool {
return true return a.options.SearchQuery == ""
} }
func (f artistFilter) Matches(artist *Artist) bool { func (f artistFilter) Matches(artist *Artist) bool {
if artist == nil { if artist == nil {
return false return false
} }
if f.options.SearchQuery != "" && !strings.Contains(
sanitize.Accents(strings.ToLower(artist.Name)),
sanitize.Accents(strings.ToLower(f.options.SearchQuery)),
) {
return false return false
}
return true
} }
type RatingFavoriteParameters struct { type RatingFavoriteParameters struct {
@@ -2,6 +2,7 @@ package subsonic
import ( import (
"log" "log"
"math/rand"
"slices" "slices"
"golang.org/x/text/collate" "golang.org/x/text/collate"
@@ -14,12 +15,16 @@ import (
) )
const ( const (
ArtistSortAlbumCount string = "Album Count"
ArtistSortNameAZ string = "Name (A-Z)" ArtistSortNameAZ string = "Name (A-Z)"
ArtistSortRandom string = "Random"
) )
func (s *subsonicMediaProvider) ArtistSortOrders() []string { func (s *subsonicMediaProvider) ArtistSortOrders() []string {
return []string{ return []string{
ArtistSortAlbumCount,
ArtistSortNameAZ, ArtistSortNameAZ,
ArtistSortRandom,
} }
} }
@@ -35,6 +40,16 @@ func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediapro
sortOrder = ArtistSortNameAZ // default sortOrder = ArtistSortNameAZ // default
} }
switch sortOrder { switch sortOrder {
case ArtistSortAlbumCount:
return s.baseArtistIterFromSimpleSortOrder(
func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 {
slices.SortStableFunc(artists, func(a, b *subsonic.ArtistID3) int {
return b.AlbumCount - a.AlbumCount
})
return artists
},
filter,
)
case ArtistSortNameAZ: case ArtistSortNameAZ:
return s.baseArtistIterFromSimpleSortOrder( return s.baseArtistIterFromSimpleSortOrder(
func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 { func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 {
@@ -46,6 +61,16 @@ func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediapro
}, },
filter, filter,
) )
case ArtistSortRandom:
return s.baseArtistIterFromSimpleSortOrder(
func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 {
newArtists := make([]*subsonic.ArtistID3, len(artists))
copy(newArtists, artists)
rand.Shuffle(len(newArtists), func(i, j int) { newArtists[i], newArtists[j] = newArtists[j], newArtists[i] })
return newArtists
},
filter,
)
default: default:
log.Printf("Undefined artist sort order: %s", sortOrder) log.Printf("Undefined artist sort order: %s", sortOrder)
return nil return nil