Merge pull request #366 from adamantike/feat/add-sorting-options-for-artists
feat: Add sorting options for Artists
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
return false
|
if f.options.SearchQuery != "" && !strings.Contains(
|
||||||
|
sanitize.Accents(strings.ToLower(artist.Name)),
|
||||||
|
sanitize.Accents(strings.ToLower(f.options.SearchQuery)),
|
||||||
|
) {
|
||||||
|
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 (
|
||||||
ArtistSortNameAZ string = "Name (A-Z)"
|
ArtistSortAlbumCount string = "Album Count"
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user