diff --git a/backend/mediaprovider/jellyfin/artistiterator.go b/backend/mediaprovider/jellyfin/artistiterator.go
index ddadb9d..ab85ee2 100644
--- a/backend/mediaprovider/jellyfin/artistiterator.go
+++ b/backend/mediaprovider/jellyfin/artistiterator.go
@@ -54,6 +54,7 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediapro
return j.client.GetAlbumArtists(jellyfin.QueryOpts{
Sort: jfSort,
Paging: paging,
+ Filter: jellyfin.Filter{ParentID: j.currentLibraryID},
})
},
sortFn,
diff --git a/backend/mediaprovider/jellyfin/iterators.go b/backend/mediaprovider/jellyfin/iterators.go
index 05f87f8..10df9f7 100644
--- a/backend/mediaprovider/jellyfin/iterators.go
+++ b/backend/mediaprovider/jellyfin/iterators.go
@@ -42,6 +42,9 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
jfSort.Mode = jellyfin.SortDesc
}
jfFilt, modifiedFilter := jfFilterFromFilter(filter)
+ if j.currentLibraryID != "" {
+ jfFilt.ParentID = j.currentLibraryID
+ }
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
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 {
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 {
return nil, err
}
@@ -89,6 +95,9 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) {
var opts jellyfin.QueryOpts
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
+ if j.currentLibraryID != "" {
+ opts.Filter.ParentID = j.currentLibraryID
+ }
s, err := j.client.GetSongs(opts)
if err != nil {
return nil, err
@@ -97,7 +106,10 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
}
} else {
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 {
return nil, err
}
diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go
index 19faba7..f84b951 100644
--- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go
+++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go
@@ -44,6 +44,8 @@ type jellyfinMediaProvider struct {
client *jellyfin.Client
prefetchCoverCB func(coverArtID string)
+ currentLibraryID string
+
genresCached []*mediaprovider.Genre
genresCachedAt int64 // unix
}
@@ -59,6 +61,22 @@ func (j *jellyfinMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID stri
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 {
return j.client.CreatePlaylist(name, trackIDs)
}
@@ -178,6 +196,9 @@ func (j *jellyfinMediaProvider) GetTopTracks(artist mediaprovider.Artist, limit
opts.Filter.ArtistID = artist.ID
opts.Sort.Field = jellyfin.SortByCommunityRating
opts.Sort.Mode = jellyfin.SortDesc
+ if j.currentLibraryID != "" {
+ opts.Filter.ParentID = j.currentLibraryID
+ }
tr, err := j.client.GetSongs(opts)
if err != nil {
return nil, err
@@ -193,6 +214,9 @@ func (j *jellyfinMediaProvider) GetRandomTracks(genreName string, limit int) ([]
opts.Paging.Limit = limit
opts.Filter.Genres = []string{genreName}
opts.Sort.Field = jellyfin.SortByRandom
+ if j.currentLibraryID != "" {
+ opts.Filter.ParentID = j.currentLibraryID
+ }
tr, err := j.client.GetSongs(opts)
if err != nil {
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)
}
-func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
+func (j *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
var wg sync.WaitGroup
var favorites mediaprovider.Favorites
+ var opts jellyfin.QueryOpts
+ opts.Filter.Favorite = true
+ opts.Filter.ParentID = j.currentLibraryID
wg.Add(1)
go func() {
- var opts jellyfin.QueryOpts
- opts.Filter.Favorite = true
- al, err := s.client.GetAlbums(opts)
+ al, err := j.client.GetAlbums(opts)
if err == nil && len(al) > 0 {
favorites.Albums = sharedutil.MapSlice(al, toAlbum)
}
@@ -229,9 +254,7 @@ func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
wg.Add(1)
go func() {
- var opts jellyfin.QueryOpts
- opts.Filter.Favorite = true
- ar, err := s.client.GetAlbumArtists(opts)
+ ar, err := j.client.GetAlbumArtists(opts)
if err == nil && len(ar) > 0 {
favorites.Artists = sharedutil.MapSlice(ar, toArtist)
}
@@ -240,9 +263,7 @@ func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
wg.Add(1)
go func() {
- var opts jellyfin.QueryOpts
- opts.Filter.Favorite = true
- tr, err := s.client.GetSongs(opts)
+ tr, err := j.client.GetSongs(opts)
if err == nil && len(tr) > 0 {
favorites.Tracks = sharedutil.MapSlice(tr, toTrack)
}
@@ -258,7 +279,7 @@ func (j *jellyfinMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
return j.genresCached, nil
}
- g, err := j.client.GetGenres(jellyfin.Paging{})
+ g, err := j.client.GetGenres(jellyfin.Paging{}, j.currentLibraryID)
if err != nil {
return nil, err
}
diff --git a/backend/mediaprovider/jellyfin/searchall.go b/backend/mediaprovider/jellyfin/searchall.go
index 1494818..ee14380 100644
--- a/backend/mediaprovider/jellyfin/searchall.go
+++ b/backend/mediaprovider/jellyfin/searchall.go
@@ -20,21 +20,24 @@ func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
var genres []jellyfin.NameID
var playlists []*jellyfin.Playlist
+ var opts jellyfin.QueryOpts
+ opts.Paging.Limit = limit
+ opts.Filter.ParentID = j.currentLibraryID
wg.Add(1)
go func() {
- albumResult, _ := j.client.Search(searchQuery, jellyfin.TypeAlbum, jellyfin.Paging{Limit: limit})
+ albumResult, _ := j.client.Search(searchQuery, jellyfin.TypeAlbum, opts)
albums = albumResult.Albums
wg.Done()
}()
wg.Add(1)
go func() {
- artistResult, _ := j.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{Limit: limit})
+ artistResult, _ := j.client.Search(searchQuery, jellyfin.TypeArtist, opts)
artists = artistResult.Artists
wg.Done()
}()
wg.Add(1)
go func() {
- songResult, _ := j.client.Search(searchQuery, jellyfin.TypeSong, jellyfin.Paging{Limit: limit})
+ songResult, _ := j.client.Search(searchQuery, jellyfin.TypeSong, opts)
songs = songResult.Songs
wg.Done()
}()
@@ -55,7 +58,7 @@ func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
wg.Add(1)
go func() {
- g, e := j.client.GetGenres(jellyfin.Paging{})
+ g, e := j.client.GetGenres(jellyfin.Paging{}, "")
if e == nil {
genres = sharedutil.FilterSlice(g, func(g jellyfin.NameID) bool {
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(g.Name)), queryLowerWords)
diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go
index 4f5fc79..5fa2c48 100644
--- a/backend/mediaprovider/mediaprovider.go
+++ b/backend/mediaprovider/mediaprovider.go
@@ -197,6 +197,15 @@ type Server interface {
type MediaProvider interface {
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)
GetAlbum(albumID string) (*AlbumWithTracks, error)
diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go
index 8a4e659..f2ebb27 100644
--- a/backend/mediaprovider/model.go
+++ b/backend/mediaprovider/model.go
@@ -32,6 +32,11 @@ const (
ReleaseTypeSpokenWord ReleaseType = 0x8000
)
+type Library struct {
+ ID string
+ Name string
+}
+
type ItemDate struct {
Year *int
Month *int
diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go
index ad44c6d..3e1363a 100644
--- a/backend/mediaprovider/subsonic/albumiterator.go
+++ b/backend/mediaprovider/subsonic/albumiterator.go
@@ -62,8 +62,11 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
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)})
+ params := 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)
}
@@ -92,14 +95,20 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
return s.baseIterFromSimpleSortOrder("alphabeticalByArtist", filter)
case mediaprovider.AlbumSortYearAscending:
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
- return s.client.GetAlbumList2("byYear",
- map[string]string{"fromYear": "0", "toYear": "3000", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
+ params := 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)
case mediaprovider.AlbumSortYearDescending:
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
- return s.client.GetAlbumList2("byYear",
- map[string]string{"fromYear": "3000", "toYear": "0", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
+ params := 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)
default:
@@ -126,8 +135,9 @@ type searchAlbumIter struct {
func (s *subsonicMediaProvider) newSearchAlbumIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchAlbumIter {
return &searchAlbumIter{
searchIterBase: searchIterBase{
- query: query,
- s: s.client,
+ query: query,
+ s: s.client,
+ musicFolderId: s.currentLibraryID,
},
prefetchCB: cb,
filter: filter,
@@ -218,6 +228,9 @@ func (s *subsonicMediaProvider) newRandomIter(filter mediaprovider.AlbumFilter,
"size": strconv.Itoa(limit),
"offset": strconv.Itoa(offset),
}
+ if s.currentLibraryID != "" {
+ args["musicFolderId"] = s.currentLibraryID
+ }
return s.client.GetAlbumList2("random", args)
}),
filter, s.prefetchCoverCB)
@@ -229,7 +242,11 @@ func (s *subsonicMediaProvider) baseIterFromSimpleSortOrder(sort string, filter
func (s *subsonicMediaProvider) fetchFnFromStandardSort(sort string) helpers.AlbumFetchFn {
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)
})
}
diff --git a/backend/mediaprovider/subsonic/artistiterator.go b/backend/mediaprovider/subsonic/artistiterator.go
index e02d97d..35e2587 100644
--- a/backend/mediaprovider/subsonic/artistiterator.go
+++ b/backend/mediaprovider/subsonic/artistiterator.go
@@ -95,8 +95,9 @@ type searchArtistIter struct {
func (s *subsonicMediaProvider) newSearchArtistIter(query string, filter mediaprovider.ArtistFilter, cb func(string)) *searchArtistIter {
return &searchArtistIter{
searchIterBase: searchIterBase{
- query: query,
- s: s.client,
+ query: query,
+ s: s.client,
+ musicFolderId: s.currentLibraryID,
},
prefetchCB: cb,
filter: filter,
@@ -165,7 +166,11 @@ func (s *subsonicMediaProvider) artistFetchFnFromStandardSort(sortFn func([]*sub
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 {
return nil, err
}
diff --git a/backend/mediaprovider/subsonic/searchall.go b/backend/mediaprovider/subsonic/searchall.go
index e7f7d78..c071ca0 100644
--- a/backend/mediaprovider/subsonic/searchall.go
+++ b/backend/mediaprovider/subsonic/searchall.go
@@ -23,11 +23,15 @@ func (s *subsonicMediaProvider) SearchAll(searchQuery string, maxResults int) ([
wg.Add(1)
go func() {
count := strconv.Itoa(maxResults / 3)
- res, e := s.client.Search3(searchQuery, map[string]string{
+ params := map[string]string{
"artistCount": count,
"albumCount": count,
"songCount": count,
- })
+ }
+ if s.currentLibraryID != "" {
+ params["musicFolderId"] = s.currentLibraryID
+ }
+ res, e := s.client.Search3(searchQuery, params)
if e != nil {
err = e
} else {
diff --git a/backend/mediaprovider/subsonic/searchiterbase.go b/backend/mediaprovider/subsonic/searchiterbase.go
index d7ffc1a..e5825ae 100644
--- a/backend/mediaprovider/subsonic/searchiterbase.go
+++ b/backend/mediaprovider/subsonic/searchiterbase.go
@@ -8,11 +8,12 @@ import (
)
type searchIterBase struct {
- query string
- artistOffset int
- albumOffset int
- songOffset int
- s *subsonic.Client
+ musicFolderId string
+ query string
+ artistOffset int
+ albumOffset int
+ songOffset int
+ s *subsonic.Client
}
func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
@@ -21,6 +22,9 @@ func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
"albumOffset": strconv.Itoa(s.albumOffset),
"songOffset": strconv.Itoa(s.songOffset),
}
+ if s.musicFolderId != "" {
+ searchOpts["musicFolderId"] = s.musicFolderId
+ }
results, err := s.s.Search3(s.query, searchOpts)
if err != nil {
log.Println(err)
diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go
index 3d5d22a..3f38069 100644
--- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go
+++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go
@@ -24,6 +24,8 @@ const (
)
type subsonicMediaProvider struct {
+ currentLibraryID string
+
client *subsonic.Client
prefetchCoverCB func(coverArtID string)
@@ -45,6 +47,21 @@ func (s *subsonicMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID stri
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 {
s.playlistsCached = nil
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) {
- 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 {
return mediaprovider.Favorites{}, err
}
@@ -219,6 +240,9 @@ func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]
if genreName != "" {
opts["genre"] = genreName
}
+ if s.currentLibraryID != "" {
+ opts["musicFolderId"] = s.currentLibraryID
+ }
tr, err := s.client.GetRandomSongs(opts)
if err != nil {
return nil, err
diff --git a/backend/mediaprovider/subsonic/trackiterator.go b/backend/mediaprovider/subsonic/trackiterator.go
index ed8ddb0..dd17579 100644
--- a/backend/mediaprovider/subsonic/trackiterator.go
+++ b/backend/mediaprovider/subsonic/trackiterator.go
@@ -19,8 +19,9 @@ func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider.
}
return &searchTracksIterator{
searchIterBase: searchIterBase{
- s: s.client,
- query: searchQuery,
+ s: s.client,
+ query: searchQuery,
+ musicFolderId: s.currentLibraryID,
},
trackIDset: make(map[string]bool),
}
diff --git a/go.mod b/go.mod
index 56284a4..13ba0ec 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c
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/google/uuid v1.3.0
github.com/hashicorp/go-retryablehttp v0.7.7
diff --git a/go.sum b/go.sum
index d2640c8..2ae2ab4 100644
--- a/go.sum
+++ b/go.sum
@@ -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/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-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/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
diff --git a/res/bundled.go b/res/bundled.go
index 154b27b..4956f1a 100644
--- a/res/bundled.go
+++ b/res/bundled.go
@@ -20,6 +20,11 @@ var ResPlaybuttonPng = &fyne.StaticResource{
StaticContent: []byte(
"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x80\x00\x00\x00\x80\b\x03\x00\x00\x00\xf4\xe0\x91\xf9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\v\xfca\x05\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x01\xcePLTE\x00\x00\x00\"\xd7)!\xd3(#\xe2+!\xd6( \xce'\"\xd9)!\xd5(\"\xd6)\x1b\xae!!\xd2(\"\xd8)\x1b\xb0!\"\xd8)!\xd5(!\xd5(\"\xd7)!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)!\xd6(\"\xdb)!\xd6(!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)\"\xd6)\"\xd6)\"\xd7)!\xd6(!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)!\xd6(!\xd5(!\xd6(\"\xd7)!\xd6(#\xde*!\xd5(!\xd6(!\xd6(!\xd6( \xcd'!\xd3(!\xd6(!\xd6(!\xd2(!\xd5(!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)!\xd6(!\xd6(\"\xd7)!\xd6(!\xd5(!\xd6(\"\xd6)\"\xd7)\"\xd6)!\xd6(!\xd6(\"\xd6)!\xd6(!\xd5(!\xd6(\"\xd6)!\xd6(!\xd6(!\xd6(\"\xd6)!\xd5(!\xd6(\"\xd6)\"\xd7)\"\xd7)!\xd6(\"\xd7)!\xd6(!\xd5(\"\xd6)!\xd6(!\xd5(\"\xd7)!\xd6(!\xd6(!\xd6(!\xd6(!\xd5(!\xd6(!\xd4(!\xd6(!\xd5(!\xd6(!\xd6(!\xd5(\"\xd7)\"\xd7)!\xd6(!\xd6(\"\xd7)\"\xd7)\"\xd6)!\xd6(!\xd6(\"\xd7)\"\xd6) \xd6'(\xd8/)\xd80\x1f\xd6&>\xdcD\x97\xec\x9b\xc3\xf4\xc5\xc6\xf4Ǐ\xea\x93?\xdcE\xc5\xf4\xc7\xff\xff\xff\xfe\xfe\xfe\xdc\xf8\xddz\xe6~-\xd94\x97\xec\x9a\xfa\xfe\xfa\xba\xf2\xbcQ\xdfW\xec\xfb\xec\x90\xeb\x944\xda:9\xdb?\xe0\xf9\xe1\xfc\xfe\xfc\xcf\xf6\xd0f\xe3k$\xd7+<\xdbB\xe4\xfa\xe5\xf2\xfc\xf2\xa7\xef\xa9D\xddI\xbf\xf3\xc1&\xd7-\x98원\xea\x93(2|%\x00\x00\x00qtRNS\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x04\x141S\x7f\x93\xbe\xcf\xdd\x01\x1cEq\xa6\xcb\xe7\xf8\xfe\v3f\xa9\xd9\xfe\x05'e\xb4\xee\xfd\x01\x11L\xa0\xe4\x01\x16\xc7\xf6\x15o\xd0\xfb\x14\xca\nM\xbf\xfc\x01)\xa1\xf5\ro(\xa4\xfb\x04Q\xd7\x0f\x80\xf0\"\xfc*\xbdCC\xdd\xd6\"\x0e\x03\xfd)\rp\tN\x15\xc9p\xfc\x17\xce\x12Ɵ\n3\xed\xa8\xd8\x03\xa60\x92\x9c\x8e\xa7 \x00\x00\x00\x01bKGD\x7fH\xbfq\xe5\x00\x00\x00\atIME\a\xe7\x01\x13\x01%2\x99\xbf\xbb\v\x00\x00\x04[IDATx\xda\xe5\x9bgC\x13A\x10\x86wÑP\x83\xb4H\rx\x82tA\x9a\"\x1d%H\x93Б\xa2\"Ho\xb7\x89\x82\xf5,\xd8{\x03\xfd\xb7\xe6\x12\x90\x00Iؽ\xdd\xcb|\xf0\xfd\xbe\xf7>\x99\x9d\xdcm\x99A\x88Q8ޚp*1)9%զ\x90=)\xb6Ԕ\xe4\xa4\xc4S\t\xd6x\xcc\xfa<6\xf3\xd3ִ\xf4\x8c̬l{\x0e9\xa6\x1c{vVfFz\x9a\xf5\xb4A\x10\x18瞑\xcf\xe6\xd9\xf3I\b\xe5\xdb\xf3\xce\xcagr\xb1x\x06|\xae\xa0\xb0\xa8\xb8\x84P\xa8\xa4\xb8\xa8\xb0\xe0\x9cP\x04\\Zv\xbe\xbc\xa2\xc4Ec\xaf\xc9URQ~\xbe\xacT\x14\x83\xe9B\xa5\\UMk\xbe\xaf\xea*\xb9\xf2\x82I\x84}DM\xed\xc5K\xac\xf6\x9a.]\xac\xad\x89\xe0E\xc0R\x9d|Y\x97\xbd\x17\xe1\xb2\\'qM\x04\xaeohl\xd2k\xaf\xa9\xa9\xb1\xa1^?\x01nnim\xa3μ\xc0r\xb5\xb5\xb64\xebD\xc0W\xae\x1e\xbc\xec\xf4K\xb1]\xbd\xa2\x87\x00G\xb6;:\xf8\xed5u8\xda#\x99\x11\xf0\xb5\xce.\xce\xe8\xfb\xcdCW\xe75F\x02\xdc\xdd\xd3+\xcc\xdfC\xd0\xdb\xd3\xcdB\x80\xa5\xeb\x8e>q\xf6\x9a\xfa\x1c\xd7\xe9\xff\x90\xd8\xec\xec\x17k\xaf\xa9\xdfi\xa6$\xc0x\xa0K@\xf6\x1f\x95\xd25@\xf7\x91\xc4xpH\xbc\xbd\xa6\xa1A\x1a\x02\xe3\xfc\xe9\b\x8c\xf4\xa7!0\xd6\xffd\x02lq\x0e\x1b\xe9OȰ\xd3\x12\x8a\x00\x8f\xf4\x1b\x90\xff\xfeR\xfaGB\x00H\xa3\x0ec\xed59F\xa5\xa0\xfec=7\x8c\a\xb8\xd13\x16\x84\x00\xe3\xce^\xe3\xfd\t\xe9\xed\f\x92\x88\xd2\xf8\xb0\xc0\xefOp\xb9\x86\xc7\x03\x86@\x9ap\x84\xc5\xdfC\xe0\x98\b@\x80\xa3&\x05\xad?NV\xc7d\xd4\xf1I\xb08m\xe1\xf2'\xc4\xe6\xb4\x1c\x9b\x80\xa9V\x83\xdf\x00\xfeRZ\xa7\x8eLB\xb4\xf9f[\xf8\xfc\ti\xbbi\x8e>\x1c\x80[\x8da\xca@\x9f\\\x8d\xb7\x0e\x85 &V\xe6\xda\x7f\xb0\xabI\x8e\x8d\xf1\x030U\xde\x0e\xaf?!\xb7+\xfd\xb6\x8dX\x9a\xbe\x13n\x80;\xd3~\x8bT\xd3\xccl\xb8\xfd\t\x99\x9d1\x1d\x04@\x0e{\x00ݢ\x8d\x81A\x00\xea6\xed\xc2\xdd(\x80\x9d\xdd\xff\x1d\x00z\n\x80\x93\x90\xe9o\b\xfe\"\x02\x7f\x15\x83\x7f\x8c\xc0?\xc7\xe0\v\x12\xf0%\x19\xf8\xa2\x14|Y\x0e\xbe1\x01ߚ\x81oN\xc1\xb7\xe7\xe0\a\x14\xe0G4\xe0\x87T\xe0\xc7t\xf0\a\x95\xe0G\xb5\xe0\x87\xd5\xcc\xc7\xf5\xbfwv\xfe\b=\xaeg\xbe\xb0\xd8\xdd\x15{a\x01\x7fe\x03~i\x05~m\a\x7fq\t~u\v\x7fy\r~}\x0f_\xc0\x00^\xc2\x01_\xc4\x02_\xc6\x03^\xc8\x04_\xca\x05_\xcc\x06^\xce\a_\xd0\b_\xd2\t_\xd4\n^\xd6\v_\xd8\f_\xda\r_\xdc\x0e_\xde\x0f\xdf\xe0\x00\xdf\xe2\x81\xc0\x9b\\\x10|\x9b\x0f|\xa3\x13\x02o\xf5B\xf0\xcdn\b\xbc\xdd\x0f\xc17<\"\xf0\x96O/\x02lӫw\"`\xdb~}\f\xa0\x8d\xcf>\x04\xd8\xd6o\x1f\x03h\xf3\xfb>\x84\xe0\xf6\xff\xbfN\xd2\xc9+\xf1{\x02!\x00\x00\x00%tEXtdate:create\x002023-01-19T01:37:03+00:00/\x9b0\x17\x00\x00\x00%tEXtdate:modify\x002023-01-19T01:36:39+00:00\x9b\xfb\xbb8\x00\x00\x00\x00IEND\xaeB`\x82"),
}
+var ResLibrarySvg = &fyne.StaticResource{
+ StaticName: "library.svg",
+ StaticContent: []byte(
+ "\n"),
+}
var ResCastSvg = &fyne.StaticResource{
StaticName: "cast.svg",
StaticContent: []byte(
diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh
index 9470b62..2792568 100755
--- a/res/bundled_gen.sh
+++ b/res/bundled_gen.sh
@@ -3,6 +3,7 @@
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/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/disc.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/headphones.svg >> bundled.go
diff --git a/res/icons/majesticons/icons-attrib.txt b/res/icons/majesticons/icons-attrib.txt
new file mode 100644
index 0000000..ae0e1f4
--- /dev/null
+++ b/res/icons/majesticons/icons-attrib.txt
@@ -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
diff --git a/res/icons/majesticons/library.svg b/res/icons/majesticons/library.svg
new file mode 100644
index 0000000..080de51
--- /dev/null
+++ b/res/icons/majesticons/library.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/res/translations/en.json b/res/translations/en.json
index e72fe8f..1170cff 100644
--- a/res/translations/en.json
+++ b/res/translations/en.json
@@ -15,6 +15,7 @@
"Albums": "Albums",
"albums": "albums",
"All": "All",
+ "All Libraries": "All Libraries",
"All Tracks": "All Tracks",
"Allow multiple app instances": "Allow multiple app instances",
"Alt. URL": "Alt. URL",
@@ -182,6 +183,7 @@
"Search page": "Search page",
"Search playlists or new playlist name": "Search playlists or new playlist name",
"sec": "sec",
+ "Select Library": "Select Library",
"selected": "selected",
"Send playback statistics to server": "Send playback statistics to server",
"Server": "Server",
diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go
index a1956c3..e666ae7 100644
--- a/ui/browsing/browsingpane.go
+++ b/ui/browsing/browsingpane.go
@@ -153,6 +153,14 @@ func (b *BrowsingPane) AddSettingsSubmenu(label string, icon fyne.Resource, menu
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() {
b.settingsMenu.Items = append(b.settingsMenu.Items,
fyne.NewMenuItemSeparator())
diff --git a/ui/mainwindow.go b/ui/mainwindow.go
index 52d4fc7..c77b98d 100644
--- a/ui/mainwindow.go
+++ b/ui/mainwindow.go
@@ -43,8 +43,11 @@ type MainWindow struct {
alreadyConnected bool // tracks if we have already connected to a server before
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
+
+ // updated when changing servers or libraries
+ librarySubmenu *fyne.Menu
}
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("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.AddSettingsMenuSeparator()
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() {
m.BrowsingPane.EnableNavigationButtons()
m.Router.NavigateTo(m.StartupPage())
diff --git a/ui/theme/theme.go b/ui/theme/theme.go
index 3ff3f13..feb3435 100644
--- a/ui/theme/theme.go
+++ b/ui/theme/theme.go
@@ -62,6 +62,7 @@ var (
RepeatOneIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatoneSvg)
SortIcon fyne.Resource = theme.NewThemedResource(res.ResUpdownarrowSvg)
VisualizationIcon fyne.Resource = theme.NewThemedResource(res.ResOscilloscopeSvg)
+ LibraryIcon fyne.Resource = theme.NewThemedResource(res.ResLibrarySvg)
)
type AppearanceMode string