Merge pull request #655 from dweymouth/feature/multi-library
Allow browsing a specific music library for servers that expose multiple libraries
This commit is contained in:
@@ -54,6 +54,7 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediapro
|
|||||||
return j.client.GetAlbumArtists(jellyfin.QueryOpts{
|
return j.client.GetAlbumArtists(jellyfin.QueryOpts{
|
||||||
Sort: jfSort,
|
Sort: jfSort,
|
||||||
Paging: paging,
|
Paging: paging,
|
||||||
|
Filter: jellyfin.Filter{ParentID: j.currentLibraryID},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
sortFn,
|
sortFn,
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
|
|||||||
jfSort.Mode = jellyfin.SortDesc
|
jfSort.Mode = jellyfin.SortDesc
|
||||||
}
|
}
|
||||||
jfFilt, modifiedFilter := jfFilterFromFilter(filter)
|
jfFilt, modifiedFilter := jfFilterFromFilter(filter)
|
||||||
|
if j.currentLibraryID != "" {
|
||||||
|
jfFilt.ParentID = j.currentLibraryID
|
||||||
|
}
|
||||||
|
|
||||||
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
|
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
|
||||||
al, err := j.client.GetAlbums(jellyfin.QueryOpts{
|
al, err := j.client.GetAlbums(jellyfin.QueryOpts{
|
||||||
@@ -74,7 +77,10 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
|
|||||||
|
|
||||||
func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
|
func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
|
||||||
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
|
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
|
||||||
sr, err := j.client.Search(searchQuery, jellyfin.TypeAlbum, jellyfin.Paging{StartIndex: offs, Limit: limit})
|
var opts jellyfin.QueryOpts
|
||||||
|
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
|
||||||
|
opts.Filter.ParentID = j.currentLibraryID
|
||||||
|
sr, err := j.client.Search(searchQuery, jellyfin.TypeAlbum, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -89,6 +95,9 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
|
|||||||
fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) {
|
fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) {
|
||||||
var opts jellyfin.QueryOpts
|
var opts jellyfin.QueryOpts
|
||||||
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
|
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
|
||||||
|
if j.currentLibraryID != "" {
|
||||||
|
opts.Filter.ParentID = j.currentLibraryID
|
||||||
|
}
|
||||||
s, err := j.client.GetSongs(opts)
|
s, err := j.client.GetSongs(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -97,7 +106,10 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) {
|
fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) {
|
||||||
sr, err := j.client.Search(searchQuery, jellyfin.TypeSong, jellyfin.Paging{StartIndex: offs, Limit: limit})
|
var opts jellyfin.QueryOpts
|
||||||
|
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
|
||||||
|
opts.Filter.ParentID = j.currentLibraryID
|
||||||
|
sr, err := j.client.Search(searchQuery, jellyfin.TypeSong, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ type jellyfinMediaProvider struct {
|
|||||||
client *jellyfin.Client
|
client *jellyfin.Client
|
||||||
prefetchCoverCB func(coverArtID string)
|
prefetchCoverCB func(coverArtID string)
|
||||||
|
|
||||||
|
currentLibraryID string
|
||||||
|
|
||||||
genresCached []*mediaprovider.Genre
|
genresCached []*mediaprovider.Genre
|
||||||
genresCachedAt int64 // unix
|
genresCachedAt int64 // unix
|
||||||
}
|
}
|
||||||
@@ -59,6 +61,22 @@ func (j *jellyfinMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID stri
|
|||||||
j.prefetchCoverCB = cb
|
j.prefetchCoverCB = cb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *jellyfinMediaProvider) GetLibraries() ([]mediaprovider.Library, error) {
|
||||||
|
v, err := j.client.GetUserViews()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.FilterMapSlice(v, func(v *jellyfin.BaseItem) (mediaprovider.Library, bool) {
|
||||||
|
return mediaprovider.Library{Name: v.Name, ID: v.ID}, v.CollectionType == string(jellyfin.CollectionTypeMusic)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jellyfinMediaProvider) SetLibrary(id string) error {
|
||||||
|
j.currentLibraryID = id
|
||||||
|
j.genresCached = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *jellyfinMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
func (j *jellyfinMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
||||||
return j.client.CreatePlaylist(name, trackIDs)
|
return j.client.CreatePlaylist(name, trackIDs)
|
||||||
}
|
}
|
||||||
@@ -178,6 +196,9 @@ func (j *jellyfinMediaProvider) GetTopTracks(artist mediaprovider.Artist, limit
|
|||||||
opts.Filter.ArtistID = artist.ID
|
opts.Filter.ArtistID = artist.ID
|
||||||
opts.Sort.Field = jellyfin.SortByCommunityRating
|
opts.Sort.Field = jellyfin.SortByCommunityRating
|
||||||
opts.Sort.Mode = jellyfin.SortDesc
|
opts.Sort.Mode = jellyfin.SortDesc
|
||||||
|
if j.currentLibraryID != "" {
|
||||||
|
opts.Filter.ParentID = j.currentLibraryID
|
||||||
|
}
|
||||||
tr, err := j.client.GetSongs(opts)
|
tr, err := j.client.GetSongs(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -193,6 +214,9 @@ func (j *jellyfinMediaProvider) GetRandomTracks(genreName string, limit int) ([]
|
|||||||
opts.Paging.Limit = limit
|
opts.Paging.Limit = limit
|
||||||
opts.Filter.Genres = []string{genreName}
|
opts.Filter.Genres = []string{genreName}
|
||||||
opts.Sort.Field = jellyfin.SortByRandom
|
opts.Sort.Field = jellyfin.SortByRandom
|
||||||
|
if j.currentLibraryID != "" {
|
||||||
|
opts.Filter.ParentID = j.currentLibraryID
|
||||||
|
}
|
||||||
tr, err := j.client.GetSongs(opts)
|
tr, err := j.client.GetSongs(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -212,15 +236,16 @@ func (j *jellyfinMediaProvider) GetCoverArt(id string, size int) (image.Image, e
|
|||||||
return j.client.GetItemImage(id, "Primary", size, 92)
|
return j.client.GetItemImage(id, "Primary", size, 92)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
|
func (j *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var favorites mediaprovider.Favorites
|
var favorites mediaprovider.Favorites
|
||||||
|
|
||||||
|
var opts jellyfin.QueryOpts
|
||||||
|
opts.Filter.Favorite = true
|
||||||
|
opts.Filter.ParentID = j.currentLibraryID
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
var opts jellyfin.QueryOpts
|
al, err := j.client.GetAlbums(opts)
|
||||||
opts.Filter.Favorite = true
|
|
||||||
al, err := s.client.GetAlbums(opts)
|
|
||||||
if err == nil && len(al) > 0 {
|
if err == nil && len(al) > 0 {
|
||||||
favorites.Albums = sharedutil.MapSlice(al, toAlbum)
|
favorites.Albums = sharedutil.MapSlice(al, toAlbum)
|
||||||
}
|
}
|
||||||
@@ -229,9 +254,7 @@ func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
|
|||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
var opts jellyfin.QueryOpts
|
ar, err := j.client.GetAlbumArtists(opts)
|
||||||
opts.Filter.Favorite = true
|
|
||||||
ar, err := s.client.GetAlbumArtists(opts)
|
|
||||||
if err == nil && len(ar) > 0 {
|
if err == nil && len(ar) > 0 {
|
||||||
favorites.Artists = sharedutil.MapSlice(ar, toArtist)
|
favorites.Artists = sharedutil.MapSlice(ar, toArtist)
|
||||||
}
|
}
|
||||||
@@ -240,9 +263,7 @@ func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
|
|||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
var opts jellyfin.QueryOpts
|
tr, err := j.client.GetSongs(opts)
|
||||||
opts.Filter.Favorite = true
|
|
||||||
tr, err := s.client.GetSongs(opts)
|
|
||||||
if err == nil && len(tr) > 0 {
|
if err == nil && len(tr) > 0 {
|
||||||
favorites.Tracks = sharedutil.MapSlice(tr, toTrack)
|
favorites.Tracks = sharedutil.MapSlice(tr, toTrack)
|
||||||
}
|
}
|
||||||
@@ -258,7 +279,7 @@ func (j *jellyfinMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
|
|||||||
return j.genresCached, nil
|
return j.genresCached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := j.client.GetGenres(jellyfin.Paging{})
|
g, err := j.client.GetGenres(jellyfin.Paging{}, j.currentLibraryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,21 +20,24 @@ func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
var genres []jellyfin.NameID
|
var genres []jellyfin.NameID
|
||||||
var playlists []*jellyfin.Playlist
|
var playlists []*jellyfin.Playlist
|
||||||
|
|
||||||
|
var opts jellyfin.QueryOpts
|
||||||
|
opts.Paging.Limit = limit
|
||||||
|
opts.Filter.ParentID = j.currentLibraryID
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
albumResult, _ := j.client.Search(searchQuery, jellyfin.TypeAlbum, jellyfin.Paging{Limit: limit})
|
albumResult, _ := j.client.Search(searchQuery, jellyfin.TypeAlbum, opts)
|
||||||
albums = albumResult.Albums
|
albums = albumResult.Albums
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
artistResult, _ := j.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{Limit: limit})
|
artistResult, _ := j.client.Search(searchQuery, jellyfin.TypeArtist, opts)
|
||||||
artists = artistResult.Artists
|
artists = artistResult.Artists
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
songResult, _ := j.client.Search(searchQuery, jellyfin.TypeSong, jellyfin.Paging{Limit: limit})
|
songResult, _ := j.client.Search(searchQuery, jellyfin.TypeSong, opts)
|
||||||
songs = songResult.Songs
|
songs = songResult.Songs
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
@@ -55,7 +58,7 @@ func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
g, e := j.client.GetGenres(jellyfin.Paging{})
|
g, e := j.client.GetGenres(jellyfin.Paging{}, "")
|
||||||
if e == nil {
|
if e == nil {
|
||||||
genres = sharedutil.FilterSlice(g, func(g jellyfin.NameID) bool {
|
genres = sharedutil.FilterSlice(g, func(g jellyfin.NameID) bool {
|
||||||
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(g.Name)), queryLowerWords)
|
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(g.Name)), queryLowerWords)
|
||||||
|
|||||||
@@ -197,6 +197,15 @@ type Server interface {
|
|||||||
type MediaProvider interface {
|
type MediaProvider interface {
|
||||||
SetPrefetchCoverCallback(cb func(coverArtID string))
|
SetPrefetchCoverCallback(cb func(coverArtID string))
|
||||||
|
|
||||||
|
// GetLibraries gets the list of top-level music libraries
|
||||||
|
// (musicFolders in Subsonic)
|
||||||
|
GetLibraries() ([]Library, error)
|
||||||
|
|
||||||
|
// SetLibrary sets the current library that all other
|
||||||
|
// MediaProvider API calls will filter to. Use empty string
|
||||||
|
// to reset to all libraries.
|
||||||
|
SetLibrary(id string) error
|
||||||
|
|
||||||
GetTrack(trackID string) (*Track, error)
|
GetTrack(trackID string) (*Track, error)
|
||||||
|
|
||||||
GetAlbum(albumID string) (*AlbumWithTracks, error)
|
GetAlbum(albumID string) (*AlbumWithTracks, error)
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ const (
|
|||||||
ReleaseTypeSpokenWord ReleaseType = 0x8000
|
ReleaseTypeSpokenWord ReleaseType = 0x8000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Library struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
type ItemDate struct {
|
type ItemDate struct {
|
||||||
Year *int
|
Year *int
|
||||||
Month *int
|
Month *int
|
||||||
|
|||||||
@@ -62,8 +62,11 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
|
|||||||
modifiedOptions.Genres = nil
|
modifiedOptions.Genres = nil
|
||||||
modifiedFilter.SetOptions(modifiedOptions)
|
modifiedFilter.SetOptions(modifiedOptions)
|
||||||
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
||||||
return s.client.GetAlbumList2("byGenre",
|
params := map[string]string{"genre": genre, "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}
|
||||||
map[string]string{"genre": genre, "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
|
if s.currentLibraryID != "" {
|
||||||
|
params["musicFolderId"] = s.currentLibraryID
|
||||||
|
}
|
||||||
|
return s.client.GetAlbumList2("byGenre", params)
|
||||||
}
|
}
|
||||||
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), modifiedFilter, s.prefetchCoverCB)
|
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), modifiedFilter, s.prefetchCoverCB)
|
||||||
}
|
}
|
||||||
@@ -92,14 +95,20 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
|
|||||||
return s.baseIterFromSimpleSortOrder("alphabeticalByArtist", filter)
|
return s.baseIterFromSimpleSortOrder("alphabeticalByArtist", filter)
|
||||||
case mediaprovider.AlbumSortYearAscending:
|
case mediaprovider.AlbumSortYearAscending:
|
||||||
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
||||||
return s.client.GetAlbumList2("byYear",
|
params := map[string]string{"fromYear": "0", "toYear": "3000", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}
|
||||||
map[string]string{"fromYear": "0", "toYear": "3000", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
|
if s.currentLibraryID != "" {
|
||||||
|
params["musicFolderId"] = s.currentLibraryID
|
||||||
|
}
|
||||||
|
return s.client.GetAlbumList2("byYear", params)
|
||||||
}
|
}
|
||||||
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB)
|
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB)
|
||||||
case mediaprovider.AlbumSortYearDescending:
|
case mediaprovider.AlbumSortYearDescending:
|
||||||
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
||||||
return s.client.GetAlbumList2("byYear",
|
params := map[string]string{"fromYear": "3000", "toYear": "0", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}
|
||||||
map[string]string{"fromYear": "3000", "toYear": "0", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
|
if s.currentLibraryID != "" {
|
||||||
|
params["musicFolderId"] = s.currentLibraryID
|
||||||
|
}
|
||||||
|
return s.client.GetAlbumList2("byYear", params)
|
||||||
}
|
}
|
||||||
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB)
|
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB)
|
||||||
default:
|
default:
|
||||||
@@ -126,8 +135,9 @@ type searchAlbumIter struct {
|
|||||||
func (s *subsonicMediaProvider) newSearchAlbumIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchAlbumIter {
|
func (s *subsonicMediaProvider) newSearchAlbumIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchAlbumIter {
|
||||||
return &searchAlbumIter{
|
return &searchAlbumIter{
|
||||||
searchIterBase: searchIterBase{
|
searchIterBase: searchIterBase{
|
||||||
query: query,
|
query: query,
|
||||||
s: s.client,
|
s: s.client,
|
||||||
|
musicFolderId: s.currentLibraryID,
|
||||||
},
|
},
|
||||||
prefetchCB: cb,
|
prefetchCB: cb,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
@@ -218,6 +228,9 @@ func (s *subsonicMediaProvider) newRandomIter(filter mediaprovider.AlbumFilter,
|
|||||||
"size": strconv.Itoa(limit),
|
"size": strconv.Itoa(limit),
|
||||||
"offset": strconv.Itoa(offset),
|
"offset": strconv.Itoa(offset),
|
||||||
}
|
}
|
||||||
|
if s.currentLibraryID != "" {
|
||||||
|
args["musicFolderId"] = s.currentLibraryID
|
||||||
|
}
|
||||||
return s.client.GetAlbumList2("random", args)
|
return s.client.GetAlbumList2("random", args)
|
||||||
}),
|
}),
|
||||||
filter, s.prefetchCoverCB)
|
filter, s.prefetchCoverCB)
|
||||||
@@ -229,7 +242,11 @@ func (s *subsonicMediaProvider) baseIterFromSimpleSortOrder(sort string, filter
|
|||||||
|
|
||||||
func (s *subsonicMediaProvider) fetchFnFromStandardSort(sort string) helpers.AlbumFetchFn {
|
func (s *subsonicMediaProvider) fetchFnFromStandardSort(sort string) helpers.AlbumFetchFn {
|
||||||
return makeFetchFn(func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
return makeFetchFn(func(offset, limit int) ([]*subsonic.AlbumID3, error) {
|
||||||
return s.client.GetAlbumList2(sort, map[string]string{"size": strconv.Itoa(limit), "offset": strconv.Itoa(offset)})
|
params := map[string]string{"size": strconv.Itoa(limit), "offset": strconv.Itoa(offset)}
|
||||||
|
if s.currentLibraryID != "" {
|
||||||
|
params["musicFolderId"] = s.currentLibraryID
|
||||||
|
}
|
||||||
|
return s.client.GetAlbumList2(sort, params)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,8 +95,9 @@ type searchArtistIter struct {
|
|||||||
func (s *subsonicMediaProvider) newSearchArtistIter(query string, filter mediaprovider.ArtistFilter, cb func(string)) *searchArtistIter {
|
func (s *subsonicMediaProvider) newSearchArtistIter(query string, filter mediaprovider.ArtistFilter, cb func(string)) *searchArtistIter {
|
||||||
return &searchArtistIter{
|
return &searchArtistIter{
|
||||||
searchIterBase: searchIterBase{
|
searchIterBase: searchIterBase{
|
||||||
query: query,
|
query: query,
|
||||||
s: s.client,
|
s: s.client,
|
||||||
|
musicFolderId: s.currentLibraryID,
|
||||||
},
|
},
|
||||||
prefetchCB: cb,
|
prefetchCB: cb,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
@@ -165,7 +166,11 @@ func (s *subsonicMediaProvider) artistFetchFnFromStandardSort(sortFn func([]*sub
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
idxs, err := s.client.GetArtists(map[string]string{})
|
var params map[string]string
|
||||||
|
if s.currentLibraryID != "" {
|
||||||
|
params = map[string]string{"musicFolderId": s.currentLibraryID}
|
||||||
|
}
|
||||||
|
idxs, err := s.client.GetArtists(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ func (s *subsonicMediaProvider) SearchAll(searchQuery string, maxResults int) ([
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
count := strconv.Itoa(maxResults / 3)
|
count := strconv.Itoa(maxResults / 3)
|
||||||
res, e := s.client.Search3(searchQuery, map[string]string{
|
params := map[string]string{
|
||||||
"artistCount": count,
|
"artistCount": count,
|
||||||
"albumCount": count,
|
"albumCount": count,
|
||||||
"songCount": count,
|
"songCount": count,
|
||||||
})
|
}
|
||||||
|
if s.currentLibraryID != "" {
|
||||||
|
params["musicFolderId"] = s.currentLibraryID
|
||||||
|
}
|
||||||
|
res, e := s.client.Search3(searchQuery, params)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
err = e
|
err = e
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type searchIterBase struct {
|
type searchIterBase struct {
|
||||||
query string
|
musicFolderId string
|
||||||
artistOffset int
|
query string
|
||||||
albumOffset int
|
artistOffset int
|
||||||
songOffset int
|
albumOffset int
|
||||||
s *subsonic.Client
|
songOffset int
|
||||||
|
s *subsonic.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
|
func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
|
||||||
@@ -21,6 +22,9 @@ func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
|
|||||||
"albumOffset": strconv.Itoa(s.albumOffset),
|
"albumOffset": strconv.Itoa(s.albumOffset),
|
||||||
"songOffset": strconv.Itoa(s.songOffset),
|
"songOffset": strconv.Itoa(s.songOffset),
|
||||||
}
|
}
|
||||||
|
if s.musicFolderId != "" {
|
||||||
|
searchOpts["musicFolderId"] = s.musicFolderId
|
||||||
|
}
|
||||||
results, err := s.s.Search3(s.query, searchOpts)
|
results, err := s.s.Search3(s.query, searchOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type subsonicMediaProvider struct {
|
type subsonicMediaProvider struct {
|
||||||
|
currentLibraryID string
|
||||||
|
|
||||||
client *subsonic.Client
|
client *subsonic.Client
|
||||||
prefetchCoverCB func(coverArtID string)
|
prefetchCoverCB func(coverArtID string)
|
||||||
|
|
||||||
@@ -45,6 +47,21 @@ func (s *subsonicMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID stri
|
|||||||
s.prefetchCoverCB = cb
|
s.prefetchCoverCB = cb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetLibraries() ([]mediaprovider.Library, error) {
|
||||||
|
folders, err := s.client.GetMusicFolders()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.MapSlice(folders, func(f *subsonic.MusicFolder) mediaprovider.Library {
|
||||||
|
return mediaprovider.Library{ID: f.ID, Name: f.Name}
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) SetLibrary(id string) error {
|
||||||
|
s.currentLibraryID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
||||||
s.playlistsCached = nil
|
s.playlistsCached = nil
|
||||||
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name})
|
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name})
|
||||||
@@ -157,7 +174,11 @@ func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
|
func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
|
||||||
fav, err := s.client.GetStarred2(map[string]string{})
|
var params map[string]string
|
||||||
|
if s.currentLibraryID != "" {
|
||||||
|
params = map[string]string{"musicFolderId": s.currentLibraryID}
|
||||||
|
}
|
||||||
|
fav, err := s.client.GetStarred2(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mediaprovider.Favorites{}, err
|
return mediaprovider.Favorites{}, err
|
||||||
}
|
}
|
||||||
@@ -219,6 +240,9 @@ func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]
|
|||||||
if genreName != "" {
|
if genreName != "" {
|
||||||
opts["genre"] = genreName
|
opts["genre"] = genreName
|
||||||
}
|
}
|
||||||
|
if s.currentLibraryID != "" {
|
||||||
|
opts["musicFolderId"] = s.currentLibraryID
|
||||||
|
}
|
||||||
tr, err := s.client.GetRandomSongs(opts)
|
tr, err := s.client.GetRandomSongs(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider.
|
|||||||
}
|
}
|
||||||
return &searchTracksIterator{
|
return &searchTracksIterator{
|
||||||
searchIterBase: searchIterBase{
|
searchIterBase: searchIterBase{
|
||||||
s: s.client,
|
s: s.client,
|
||||||
query: searchQuery,
|
query: searchQuery,
|
||||||
|
musicFolderId: s.currentLibraryID,
|
||||||
},
|
},
|
||||||
trackIDset: make(map[string]bool),
|
trackIDset: make(map[string]bool),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
||||||
github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c
|
github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c
|
||||||
github.com/dweymouth/fyne-tooltip v0.3.0
|
github.com/dweymouth/fyne-tooltip v0.3.0
|
||||||
github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0
|
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f
|
||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4 h1:Q3r94Ac
|
|||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
||||||
github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0 h1:9t1CR83uzn5Va4Nycwncmuw/NEAN64O4lf82VBPzdfE=
|
github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0 h1:9t1CR83uzn5Va4Nycwncmuw/NEAN64O4lf82VBPzdfE=
|
||||||
github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
||||||
|
github.com/dweymouth/go-jellyfin v0.0.0-20250715154414-d0a1630ee74f h1:7O7Cn17pwKHG7zPgNhxMabXJP1ZdmNVxCb4i8yJgVZo=
|
||||||
|
github.com/dweymouth/go-jellyfin v0.0.0-20250715154414-d0a1630ee74f/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
||||||
|
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f h1:QsKPwFpTHuYHEEuhvp4VBClkHh00bNNgQ/2Ij1bkk8M=
|
||||||
|
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@
|
|||||||
fyne bundle -package res -prefix Res appicon-256.png > bundled.go
|
fyne bundle -package res -prefix Res appicon-256.png > bundled.go
|
||||||
fyne bundle -append -prefix Res icons/coreui/playlist-add-next.svg >> bundled.go
|
fyne bundle -append -prefix Res icons/coreui/playlist-add-next.svg >> bundled.go
|
||||||
fyne bundle -append -prefix Res icons/freepik/playbutton.png >> bundled.go
|
fyne bundle -append -prefix Res icons/freepik/playbutton.png >> bundled.go
|
||||||
|
fyne bundle -append -prefix Res icons/majesticons/library.svg >> bundled.go
|
||||||
fyne bundle -append -prefix Res icons/publicdomain/cast.svg >> bundled.go
|
fyne bundle -append -prefix Res icons/publicdomain/cast.svg >> bundled.go
|
||||||
fyne bundle -append -prefix Res icons/publicdomain/disc.svg >> bundled.go
|
fyne bundle -append -prefix Res icons/publicdomain/disc.svg >> bundled.go
|
||||||
fyne bundle -append -prefix Res icons/publicdomain/headphones.svg >> bundled.go
|
fyne bundle -append -prefix Res icons/publicdomain/headphones.svg >> bundled.go
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
These icons are from the Majesticons Solid Interface Icons collection by halfmage on svgrepo.com and are available under the MIT license
|
||||||
|
|
||||||
|
https://www.svgrepo.com/page/licensing/#MIT
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#000000" fill-rule="evenodd" d="M11.514 3.126a1 1 0 0 1 .972 0l9 5A1 1 0 0 1 21 10v9a1 1 0 1 1 0 2H3a1 1 0 1 1 0-2v-9a1 1 0 0 1-.486-1.874l9-5zM9 13a1 1 0 1 0-2 0v3a1 1 0 1 0 2 0v-3zm4 0a1 1 0 1 0-2 0v3a1 1 0 1 0 2 0v-3zm4 0a1 1 0 1 0-2 0v3a1 1 0 1 0 2 0v-3z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 519 B |
@@ -15,6 +15,7 @@
|
|||||||
"Albums": "Albums",
|
"Albums": "Albums",
|
||||||
"albums": "albums",
|
"albums": "albums",
|
||||||
"All": "All",
|
"All": "All",
|
||||||
|
"All Libraries": "All Libraries",
|
||||||
"All Tracks": "All Tracks",
|
"All Tracks": "All Tracks",
|
||||||
"Allow multiple app instances": "Allow multiple app instances",
|
"Allow multiple app instances": "Allow multiple app instances",
|
||||||
"Alt. URL": "Alt. URL",
|
"Alt. URL": "Alt. URL",
|
||||||
@@ -182,6 +183,7 @@
|
|||||||
"Search page": "Search page",
|
"Search page": "Search page",
|
||||||
"Search playlists or new playlist name": "Search playlists or new playlist name",
|
"Search playlists or new playlist name": "Search playlists or new playlist name",
|
||||||
"sec": "sec",
|
"sec": "sec",
|
||||||
|
"Select Library": "Select Library",
|
||||||
"selected": "selected",
|
"selected": "selected",
|
||||||
"Send playback statistics to server": "Send playback statistics to server",
|
"Send playback statistics to server": "Send playback statistics to server",
|
||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
|
|||||||
@@ -153,6 +153,14 @@ func (b *BrowsingPane) AddSettingsSubmenu(label string, icon fyne.Resource, menu
|
|||||||
b.settingsMenu.Items = append(b.settingsMenu.Items, item)
|
b.settingsMenu.Items = append(b.settingsMenu.Items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BrowsingPane) SetSubmenuForMenuItem(label string, submenu *fyne.Menu) {
|
||||||
|
for _, item := range b.settingsMenu.Items {
|
||||||
|
if item.Label == label {
|
||||||
|
item.ChildMenu = submenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BrowsingPane) AddSettingsMenuSeparator() {
|
func (b *BrowsingPane) AddSettingsMenuSeparator() {
|
||||||
b.settingsMenu.Items = append(b.settingsMenu.Items,
|
b.settingsMenu.Items = append(b.settingsMenu.Items,
|
||||||
fyne.NewMenuItemSeparator())
|
fyne.NewMenuItemSeparator())
|
||||||
|
|||||||
+49
-1
@@ -43,8 +43,11 @@ type MainWindow struct {
|
|||||||
alreadyConnected bool // tracks if we have already connected to a server before
|
alreadyConnected bool // tracks if we have already connected to a server before
|
||||||
content *mainWindowContent
|
content *mainWindowContent
|
||||||
|
|
||||||
// needs to bes shown/hidden when switching between servers based on whether they support radio
|
// needs to be shown/hidden when switching between servers based on whether they support radio
|
||||||
radioBtn fyne.CanvasObject
|
radioBtn fyne.CanvasObject
|
||||||
|
|
||||||
|
// updated when changing servers or libraries
|
||||||
|
librarySubmenu *fyne.Menu
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app *backend.App) MainWindow {
|
func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app *backend.App) MainWindow {
|
||||||
@@ -102,6 +105,8 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
|||||||
})
|
})
|
||||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Log Out"), theme.LogoutIcon(), func() { app.ServerManager.Logout(true) })
|
m.BrowsingPane.AddSettingsMenuItem(lang.L("Log Out"), theme.LogoutIcon(), func() { app.ServerManager.Logout(true) })
|
||||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Switch Servers"), theme.LoginIcon(), func() { app.ServerManager.Logout(false) })
|
m.BrowsingPane.AddSettingsMenuItem(lang.L("Switch Servers"), theme.LoginIcon(), func() { app.ServerManager.Logout(false) })
|
||||||
|
m.BrowsingPane.AddSettingsSubmenu(lang.L("Select Library"), myTheme.LibraryIcon, fyne.NewMenu("",
|
||||||
|
fyne.NewMenuItem(lang.L("All Libraries"), func() { /* dummy - will get replaced on server login */ })))
|
||||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Rescan Library"), theme.ViewRefreshIcon(), func() { app.ServerManager.Server.RescanLibrary() })
|
m.BrowsingPane.AddSettingsMenuItem(lang.L("Rescan Library"), theme.ViewRefreshIcon(), func() { app.ServerManager.Server.RescanLibrary() })
|
||||||
m.BrowsingPane.AddSettingsMenuSeparator()
|
m.BrowsingPane.AddSettingsMenuSeparator()
|
||||||
m.BrowsingPane.AddSettingsSubmenu(lang.L("Visualizations"), myTheme.VisualizationIcon,
|
m.BrowsingPane.AddSettingsSubmenu(lang.L("Visualizations"), myTheme.VisualizationIcon,
|
||||||
@@ -229,6 +234,49 @@ func (m *MainWindow) RunOnServerConnectedTasks(app *backend.App, displayAppName
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doSetLibrary := func(libraryID string, menuIdx int) {
|
||||||
|
fyne.Do(func() {
|
||||||
|
m.App.ServerManager.Server.SetLibrary(libraryID)
|
||||||
|
// Pages in the history could contain content
|
||||||
|
// outside the new library, so clear history
|
||||||
|
m.BrowsingPane.ClearHistory()
|
||||||
|
// ... and reload current page for the same reason
|
||||||
|
m.BrowsingPane.Reload()
|
||||||
|
// check only the menu item for the new library
|
||||||
|
for i, menuItem := range m.librarySubmenu.Items {
|
||||||
|
menuItem.Checked = (i == menuIdx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
libraries, err := app.ServerManager.Server.GetLibraries()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error loading server libraries: %s", err.Error())
|
||||||
|
}
|
||||||
|
libraryMenu := fyne.NewMenu("")
|
||||||
|
libraryMenuItemOffset := 0
|
||||||
|
if len(libraries) != 1 {
|
||||||
|
// If there is exactly one library in the list,
|
||||||
|
// we just want to have one menu entry with that library's name.
|
||||||
|
// Otherwise, add the "All Libraries" menu item at the top.
|
||||||
|
libraryMenu.Items = append(libraryMenu.Items,
|
||||||
|
fyne.NewMenuItem(lang.L("All Libraries"), func() {
|
||||||
|
doSetLibrary("", 0)
|
||||||
|
}))
|
||||||
|
libraryMenuItemOffset = 1
|
||||||
|
}
|
||||||
|
for i, l := range libraries {
|
||||||
|
_l := l
|
||||||
|
_i := i + libraryMenuItemOffset
|
||||||
|
libraryMenu.Items = append(libraryMenu.Items,
|
||||||
|
fyne.NewMenuItem(_l.Name, func() {
|
||||||
|
doSetLibrary(_l.ID, _i)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
m.librarySubmenu = libraryMenu
|
||||||
|
m.librarySubmenu.Items[0].Checked = true
|
||||||
|
m.BrowsingPane.SetSubmenuForMenuItem(lang.L("Select Library"), libraryMenu)
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
m.BrowsingPane.EnableNavigationButtons()
|
m.BrowsingPane.EnableNavigationButtons()
|
||||||
m.Router.NavigateTo(m.StartupPage())
|
m.Router.NavigateTo(m.StartupPage())
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ var (
|
|||||||
RepeatOneIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatoneSvg)
|
RepeatOneIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatoneSvg)
|
||||||
SortIcon fyne.Resource = theme.NewThemedResource(res.ResUpdownarrowSvg)
|
SortIcon fyne.Resource = theme.NewThemedResource(res.ResUpdownarrowSvg)
|
||||||
VisualizationIcon fyne.Resource = theme.NewThemedResource(res.ResOscilloscopeSvg)
|
VisualizationIcon fyne.Resource = theme.NewThemedResource(res.ResOscilloscopeSvg)
|
||||||
|
LibraryIcon fyne.Resource = theme.NewThemedResource(res.ResLibrarySvg)
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppearanceMode string
|
type AppearanceMode string
|
||||||
|
|||||||
Reference in New Issue
Block a user