Merge branch 'main' into feature/lyrics
This commit is contained in:
@@ -32,6 +32,15 @@ func NewAlbumIterator(fetchFn AlbumFetchFn, filter mediaprovider.AlbumFilter, cb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArtistFetchFn func(offset, limit int) ([]*mediaprovider.Artist, error)
|
||||||
|
|
||||||
|
func NewArtistIterator(fetchFn ArtistFetchFn) mediaprovider.ArtistIterator {
|
||||||
|
return &baseIter[mediaprovider.Artist]{
|
||||||
|
fetcher: fetchFn,
|
||||||
|
filter: nilFilter[mediaprovider.Artist]{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type TrackFetchFn func(offset, limit int) ([]*mediaprovider.Track, error)
|
type TrackFetchFn func(offset, limit int) ([]*mediaprovider.Track, error)
|
||||||
|
|
||||||
func NewTrackIterator(fetchFn TrackFetchFn, cb func(string)) mediaprovider.TrackIterator {
|
func NewTrackIterator(fetchFn TrackFetchFn, cb func(string)) mediaprovider.TrackIterator {
|
||||||
@@ -52,7 +61,7 @@ func (r *baseIter[T]) Next() *T {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
r.prefetched = nil
|
r.prefetched = nil
|
||||||
for { // keep fetching until we are done or have mathcing results
|
for { // keep fetching until we are done or have matching results
|
||||||
items, err := r.fetcher(r.serverPos, 20)
|
items, err := r.fetcher(r.serverPos, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error fetching items: %s", err.Error())
|
log.Printf("error fetching items: %s", err.Error())
|
||||||
@@ -73,8 +82,8 @@ func (r *baseIter[T]) Next() *T {
|
|||||||
}
|
}
|
||||||
r.prefetchedPos = 1
|
r.prefetchedPos = 1
|
||||||
if r.prefetchCB != nil {
|
if r.prefetchCB != nil {
|
||||||
for _, album := range r.prefetched {
|
for _, item := range r.prefetched {
|
||||||
go r.prefetchCB(album)
|
go r.prefetchCB(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r.prefetched[0]
|
return r.prefetched[0]
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ 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 {
|
||||||
@@ -29,6 +31,12 @@ 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 {
|
||||||
@@ -116,6 +124,32 @@ 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) 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)
|
||||||
|
}
|
||||||
|
|
||||||
// Creates the Jellyfin filter to implement the given mediaprovider filter,
|
// Creates the Jellyfin filter to implement the given mediaprovider filter,
|
||||||
// and zeros out the now-unneeded fields in the mediaprovider filter.
|
// and zeros out the now-unneeded fields in the mediaprovider filter.
|
||||||
func jfFilterFromFilter(filter *mediaprovider.AlbumFilter) jellyfin.Filter {
|
func jfFilterFromFilter(filter *mediaprovider.AlbumFilter) jellyfin.Filter {
|
||||||
|
|||||||
@@ -159,14 +159,6 @@ func (j *jellyfinMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jellyfinMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) {
|
|
||||||
ar, err := j.client.GetAlbumArtists(jellyfin.QueryOpts{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sharedutil.MapSlice(ar, toArtist), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jellyfinMediaProvider) GetTrack(trackID string) (*mediaprovider.Track, error) {
|
func (j *jellyfinMediaProvider) GetTrack(trackID string) (*mediaprovider.Track, error) {
|
||||||
tr, err := j.client.GetSong(trackID)
|
tr, err := j.client.GetSong(trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ func (f AlbumFilter) Matches(album *Album) bool {
|
|||||||
return genresMatch(f.Genres, album.Genres)
|
return genresMatch(f.Genres, album.Genres)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArtistIterator interface {
|
||||||
|
Next() *Artist
|
||||||
|
}
|
||||||
|
|
||||||
type AlbumIterator interface {
|
type AlbumIterator interface {
|
||||||
Next() *Album
|
Next() *Album
|
||||||
}
|
}
|
||||||
@@ -103,7 +107,9 @@ type MediaProvider interface {
|
|||||||
|
|
||||||
GetSimilarTracks(artistID string, count int) ([]*Track, error)
|
GetSimilarTracks(artistID string, count int) ([]*Track, error)
|
||||||
|
|
||||||
GetArtists() ([]*Artist, error)
|
ArtistSortOrders() []string
|
||||||
|
|
||||||
|
IterateArtists(sortOrder string) ArtistIterator
|
||||||
|
|
||||||
GetGenres() ([]*Genre, error)
|
GetGenres() ([]*Genre, error)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package subsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"golang.org/x/text/collate"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider/helpers"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ArtistSortNameAZ string = "Name (A-Z)"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) ArtistSortOrders() []string {
|
||||||
|
return []string{
|
||||||
|
ArtistSortNameAZ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) IterateArtists(sortOrder string) mediaprovider.ArtistIterator {
|
||||||
|
if sortOrder == "" {
|
||||||
|
sortOrder = ArtistSortNameAZ // default
|
||||||
|
}
|
||||||
|
switch sortOrder {
|
||||||
|
case ArtistSortNameAZ:
|
||||||
|
return s.baseArtistIterFromSimpleSortOrder(
|
||||||
|
func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 {
|
||||||
|
c := collate.New(language.English, collate.Loose)
|
||||||
|
slices.SortFunc(artists, func(a, b *subsonic.ArtistID3) int {
|
||||||
|
return c.CompareString(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
return artists
|
||||||
|
},
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
log.Printf("Undefined artist sort order: %s", sortOrder)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) baseArtistIterFromSimpleSortOrder(sortFn func([]*subsonic.ArtistID3) []*subsonic.ArtistID3) mediaprovider.ArtistIterator {
|
||||||
|
return helpers.NewArtistIterator(s.artistFetchFnFromStandardSort(sortFn))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) artistFetchFnFromStandardSort(sortFn func([]*subsonic.ArtistID3) []*subsonic.ArtistID3) helpers.ArtistFetchFn {
|
||||||
|
return makeArtistFetchFn(func(offset, limit int) ([]*subsonic.ArtistID3, error) {
|
||||||
|
// When the iterator asks for a second page of results, return nil, as Subsonic does not support pagination for artists.
|
||||||
|
if offset > 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idxs, err := s.client.GetArtists(map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var artists []*subsonic.ArtistID3
|
||||||
|
for _, idx := range idxs.Index {
|
||||||
|
for _, ar := range idx.Artist {
|
||||||
|
artists = append(artists, ar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artists = sortFn(artists)
|
||||||
|
return artists, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeArtistFetchFn(subsonicFetchFn func(offset, limit int) ([]*subsonic.ArtistID3, error)) helpers.ArtistFetchFn {
|
||||||
|
return func(offset, limit int) ([]*mediaprovider.Artist, error) {
|
||||||
|
ar, err := subsonicFetchFn(offset, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.MapSlice(ar, toArtistFromID3), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,20 +131,6 @@ func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) {
|
|
||||||
idxs, err := s.client.GetArtists(map[string]string{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var artists []*mediaprovider.Artist
|
|
||||||
for _, idx := range idxs.Index {
|
|
||||||
for _, ar := range idx.Artist {
|
|
||||||
artists = append(artists, toArtistFromID3(ar))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return artists, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) {
|
func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) {
|
||||||
params := map[string]string{}
|
params := map[string]string{}
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/quarckster/go-mpris-server v1.0.3
|
github.com/quarckster/go-mpris-server v1.0.3
|
||||||
github.com/zalando/go-keyring v0.2.1
|
github.com/zalando/go-keyring v0.2.1
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.17.0
|
||||||
|
golang.org/x/text v0.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -42,7 +43,6 @@ require (
|
|||||||
golang.org/x/image v0.14.0 // indirect
|
golang.org/x/image v0.14.0 // indirect
|
||||||
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
|
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
|
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package browsing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
@@ -111,9 +110,14 @@ func (a *ArtistsPage) Reload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsPage) load() {
|
func (a *ArtistsPage) load() {
|
||||||
artists, err := a.mp.GetArtists()
|
iter := a.mp.IterateArtists("")
|
||||||
if err != nil {
|
var artists []*mediaprovider.Artist
|
||||||
log.Printf("error loading artists: %v", err.Error())
|
for {
|
||||||
|
artist := iter.Next()
|
||||||
|
if artist == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
artists = append(artists, artist)
|
||||||
}
|
}
|
||||||
a.artists = artists
|
a.artists = artists
|
||||||
a.onSearched(a.searcher.Entry.Text, true)
|
a.onSearched(a.searcher.Entry.Text, true)
|
||||||
|
|||||||
Reference in New Issue
Block a user