Skip to content

Commit 1e9b6cb

Browse files
authored
Merge pull request #655 from dweymouth/feature/multi-library
Allow browsing a specific music library for servers that expose multiple libraries
2 parents 5be4bd9 + d73f7e0 commit 1e9b6cb

File tree

22 files changed

+221
-41
lines changed

22 files changed

+221
-41
lines changed

backend/mediaprovider/jellyfin/artistiterator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediapro
5454
return j.client.GetAlbumArtists(jellyfin.QueryOpts{
5555
Sort: jfSort,
5656
Paging: paging,
57+
Filter: jellyfin.Filter{ParentID: j.currentLibraryID},
5758
})
5859
},
5960
sortFn,

backend/mediaprovider/jellyfin/iterators.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
4242
jfSort.Mode = jellyfin.SortDesc
4343
}
4444
jfFilt, modifiedFilter := jfFilterFromFilter(filter)
45+
if j.currentLibraryID != "" {
46+
jfFilt.ParentID = j.currentLibraryID
47+
}
4548

4649
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
4750
al, err := j.client.GetAlbums(jellyfin.QueryOpts{
@@ -74,7 +77,10 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
7477

7578
func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
7679
fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
77-
sr, err := j.client.Search(searchQuery, jellyfin.TypeAlbum, jellyfin.Paging{StartIndex: offs, Limit: limit})
80+
var opts jellyfin.QueryOpts
81+
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
82+
opts.Filter.ParentID = j.currentLibraryID
83+
sr, err := j.client.Search(searchQuery, jellyfin.TypeAlbum, opts)
7884
if err != nil {
7985
return nil, err
8086
}
@@ -89,6 +95,9 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
8995
fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) {
9096
var opts jellyfin.QueryOpts
9197
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
98+
if j.currentLibraryID != "" {
99+
opts.Filter.ParentID = j.currentLibraryID
100+
}
92101
s, err := j.client.GetSongs(opts)
93102
if err != nil {
94103
return nil, err
@@ -97,7 +106,10 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
97106
}
98107
} else {
99108
fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) {
100-
sr, err := j.client.Search(searchQuery, jellyfin.TypeSong, jellyfin.Paging{StartIndex: offs, Limit: limit})
109+
var opts jellyfin.QueryOpts
110+
opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit}
111+
opts.Filter.ParentID = j.currentLibraryID
112+
sr, err := j.client.Search(searchQuery, jellyfin.TypeSong, opts)
101113
if err != nil {
102114
return nil, err
103115
}

backend/mediaprovider/jellyfin/jellyfinmediaprovider.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ type jellyfinMediaProvider struct {
4444
client *jellyfin.Client
4545
prefetchCoverCB func(coverArtID string)
4646

47+
currentLibraryID string
48+
4749
genresCached []*mediaprovider.Genre
4850
genresCachedAt int64 // unix
4951
}
@@ -59,6 +61,22 @@ func (j *jellyfinMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID stri
5961
j.prefetchCoverCB = cb
6062
}
6163

64+
func (j *jellyfinMediaProvider) GetLibraries() ([]mediaprovider.Library, error) {
65+
v, err := j.client.GetUserViews()
66+
if err != nil {
67+
return nil, err
68+
}
69+
return sharedutil.FilterMapSlice(v, func(v *jellyfin.BaseItem) (mediaprovider.Library, bool) {
70+
return mediaprovider.Library{Name: v.Name, ID: v.ID}, v.CollectionType == string(jellyfin.CollectionTypeMusic)
71+
}), nil
72+
}
73+
74+
func (j *jellyfinMediaProvider) SetLibrary(id string) error {
75+
j.currentLibraryID = id
76+
j.genresCached = nil
77+
return nil
78+
}
79+
6280
func (j *jellyfinMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
6381
return j.client.CreatePlaylist(name, trackIDs)
6482
}
@@ -178,6 +196,9 @@ func (j *jellyfinMediaProvider) GetTopTracks(artist mediaprovider.Artist, limit
178196
opts.Filter.ArtistID = artist.ID
179197
opts.Sort.Field = jellyfin.SortByCommunityRating
180198
opts.Sort.Mode = jellyfin.SortDesc
199+
if j.currentLibraryID != "" {
200+
opts.Filter.ParentID = j.currentLibraryID
201+
}
181202
tr, err := j.client.GetSongs(opts)
182203
if err != nil {
183204
return nil, err
@@ -193,6 +214,9 @@ func (j *jellyfinMediaProvider) GetRandomTracks(genreName string, limit int) ([]
193214
opts.Paging.Limit = limit
194215
opts.Filter.Genres = []string{genreName}
195216
opts.Sort.Field = jellyfin.SortByRandom
217+
if j.currentLibraryID != "" {
218+
opts.Filter.ParentID = j.currentLibraryID
219+
}
196220
tr, err := j.client.GetSongs(opts)
197221
if err != nil {
198222
return nil, err
@@ -212,15 +236,16 @@ func (j *jellyfinMediaProvider) GetCoverArt(id string, size int) (image.Image, e
212236
return j.client.GetItemImage(id, "Primary", size, 92)
213237
}
214238

215-
func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
239+
func (j *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
216240
var wg sync.WaitGroup
217241
var favorites mediaprovider.Favorites
218242

243+
var opts jellyfin.QueryOpts
244+
opts.Filter.Favorite = true
245+
opts.Filter.ParentID = j.currentLibraryID
219246
wg.Add(1)
220247
go func() {
221-
var opts jellyfin.QueryOpts
222-
opts.Filter.Favorite = true
223-
al, err := s.client.GetAlbums(opts)
248+
al, err := j.client.GetAlbums(opts)
224249
if err == nil && len(al) > 0 {
225250
favorites.Albums = sharedutil.MapSlice(al, toAlbum)
226251
}
@@ -229,9 +254,7 @@ func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
229254

230255
wg.Add(1)
231256
go func() {
232-
var opts jellyfin.QueryOpts
233-
opts.Filter.Favorite = true
234-
ar, err := s.client.GetAlbumArtists(opts)
257+
ar, err := j.client.GetAlbumArtists(opts)
235258
if err == nil && len(ar) > 0 {
236259
favorites.Artists = sharedutil.MapSlice(ar, toArtist)
237260
}
@@ -240,9 +263,7 @@ func (s *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
240263

241264
wg.Add(1)
242265
go func() {
243-
var opts jellyfin.QueryOpts
244-
opts.Filter.Favorite = true
245-
tr, err := s.client.GetSongs(opts)
266+
tr, err := j.client.GetSongs(opts)
246267
if err == nil && len(tr) > 0 {
247268
favorites.Tracks = sharedutil.MapSlice(tr, toTrack)
248269
}
@@ -258,7 +279,7 @@ func (j *jellyfinMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
258279
return j.genresCached, nil
259280
}
260281

261-
g, err := j.client.GetGenres(jellyfin.Paging{})
282+
g, err := j.client.GetGenres(jellyfin.Paging{}, j.currentLibraryID)
262283
if err != nil {
263284
return nil, err
264285
}

backend/mediaprovider/jellyfin/searchall.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,24 @@ func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
2020
var genres []jellyfin.NameID
2121
var playlists []*jellyfin.Playlist
2222

23+
var opts jellyfin.QueryOpts
24+
opts.Paging.Limit = limit
25+
opts.Filter.ParentID = j.currentLibraryID
2326
wg.Add(1)
2427
go func() {
25-
albumResult, _ := j.client.Search(searchQuery, jellyfin.TypeAlbum, jellyfin.Paging{Limit: limit})
28+
albumResult, _ := j.client.Search(searchQuery, jellyfin.TypeAlbum, opts)
2629
albums = albumResult.Albums
2730
wg.Done()
2831
}()
2932
wg.Add(1)
3033
go func() {
31-
artistResult, _ := j.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{Limit: limit})
34+
artistResult, _ := j.client.Search(searchQuery, jellyfin.TypeArtist, opts)
3235
artists = artistResult.Artists
3336
wg.Done()
3437
}()
3538
wg.Add(1)
3639
go func() {
37-
songResult, _ := j.client.Search(searchQuery, jellyfin.TypeSong, jellyfin.Paging{Limit: limit})
40+
songResult, _ := j.client.Search(searchQuery, jellyfin.TypeSong, opts)
3841
songs = songResult.Songs
3942
wg.Done()
4043
}()
@@ -55,7 +58,7 @@ func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([
5558

5659
wg.Add(1)
5760
go func() {
58-
g, e := j.client.GetGenres(jellyfin.Paging{})
61+
g, e := j.client.GetGenres(jellyfin.Paging{}, "")
5962
if e == nil {
6063
genres = sharedutil.FilterSlice(g, func(g jellyfin.NameID) bool {
6164
return helpers.AllTermsMatch(strings.ToLower(sanitize.Accents(g.Name)), queryLowerWords)

backend/mediaprovider/mediaprovider.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@ type Server interface {
197197
type MediaProvider interface {
198198
SetPrefetchCoverCallback(cb func(coverArtID string))
199199

200+
// GetLibraries gets the list of top-level music libraries
201+
// (musicFolders in Subsonic)
202+
GetLibraries() ([]Library, error)
203+
204+
// SetLibrary sets the current library that all other
205+
// MediaProvider API calls will filter to. Use empty string
206+
// to reset to all libraries.
207+
SetLibrary(id string) error
208+
200209
GetTrack(trackID string) (*Track, error)
201210

202211
GetAlbum(albumID string) (*AlbumWithTracks, error)

backend/mediaprovider/model.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ const (
3232
ReleaseTypeSpokenWord ReleaseType = 0x8000
3333
)
3434

35+
type Library struct {
36+
ID string
37+
Name string
38+
}
39+
3540
type ItemDate struct {
3641
Year *int
3742
Month *int

backend/mediaprovider/subsonic/albumiterator.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
6262
modifiedOptions.Genres = nil
6363
modifiedFilter.SetOptions(modifiedOptions)
6464
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
65-
return s.client.GetAlbumList2("byGenre",
66-
map[string]string{"genre": genre, "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
65+
params := map[string]string{"genre": genre, "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}
66+
if s.currentLibraryID != "" {
67+
params["musicFolderId"] = s.currentLibraryID
68+
}
69+
return s.client.GetAlbumList2("byGenre", params)
6770
}
6871
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), modifiedFilter, s.prefetchCoverCB)
6972
}
@@ -92,14 +95,20 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
9295
return s.baseIterFromSimpleSortOrder("alphabeticalByArtist", filter)
9396
case mediaprovider.AlbumSortYearAscending:
9497
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
95-
return s.client.GetAlbumList2("byYear",
96-
map[string]string{"fromYear": "0", "toYear": "3000", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
98+
params := map[string]string{"fromYear": "0", "toYear": "3000", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}
99+
if s.currentLibraryID != "" {
100+
params["musicFolderId"] = s.currentLibraryID
101+
}
102+
return s.client.GetAlbumList2("byYear", params)
97103
}
98104
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB)
99105
case mediaprovider.AlbumSortYearDescending:
100106
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
101-
return s.client.GetAlbumList2("byYear",
102-
map[string]string{"fromYear": "3000", "toYear": "0", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
107+
params := map[string]string{"fromYear": "3000", "toYear": "0", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}
108+
if s.currentLibraryID != "" {
109+
params["musicFolderId"] = s.currentLibraryID
110+
}
111+
return s.client.GetAlbumList2("byYear", params)
103112
}
104113
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB)
105114
default:
@@ -126,8 +135,9 @@ type searchAlbumIter struct {
126135
func (s *subsonicMediaProvider) newSearchAlbumIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchAlbumIter {
127136
return &searchAlbumIter{
128137
searchIterBase: searchIterBase{
129-
query: query,
130-
s: s.client,
138+
query: query,
139+
s: s.client,
140+
musicFolderId: s.currentLibraryID,
131141
},
132142
prefetchCB: cb,
133143
filter: filter,
@@ -218,6 +228,9 @@ func (s *subsonicMediaProvider) newRandomIter(filter mediaprovider.AlbumFilter,
218228
"size": strconv.Itoa(limit),
219229
"offset": strconv.Itoa(offset),
220230
}
231+
if s.currentLibraryID != "" {
232+
args["musicFolderId"] = s.currentLibraryID
233+
}
221234
return s.client.GetAlbumList2("random", args)
222235
}),
223236
filter, s.prefetchCoverCB)
@@ -229,7 +242,11 @@ func (s *subsonicMediaProvider) baseIterFromSimpleSortOrder(sort string, filter
229242

230243
func (s *subsonicMediaProvider) fetchFnFromStandardSort(sort string) helpers.AlbumFetchFn {
231244
return makeFetchFn(func(offset, limit int) ([]*subsonic.AlbumID3, error) {
232-
return s.client.GetAlbumList2(sort, map[string]string{"size": strconv.Itoa(limit), "offset": strconv.Itoa(offset)})
245+
params := map[string]string{"size": strconv.Itoa(limit), "offset": strconv.Itoa(offset)}
246+
if s.currentLibraryID != "" {
247+
params["musicFolderId"] = s.currentLibraryID
248+
}
249+
return s.client.GetAlbumList2(sort, params)
233250
})
234251
}
235252

backend/mediaprovider/subsonic/artistiterator.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ type searchArtistIter struct {
9595
func (s *subsonicMediaProvider) newSearchArtistIter(query string, filter mediaprovider.ArtistFilter, cb func(string)) *searchArtistIter {
9696
return &searchArtistIter{
9797
searchIterBase: searchIterBase{
98-
query: query,
99-
s: s.client,
98+
query: query,
99+
s: s.client,
100+
musicFolderId: s.currentLibraryID,
100101
},
101102
prefetchCB: cb,
102103
filter: filter,
@@ -165,7 +166,11 @@ func (s *subsonicMediaProvider) artistFetchFnFromStandardSort(sortFn func([]*sub
165166
return nil, nil
166167
}
167168

168-
idxs, err := s.client.GetArtists(map[string]string{})
169+
var params map[string]string
170+
if s.currentLibraryID != "" {
171+
params = map[string]string{"musicFolderId": s.currentLibraryID}
172+
}
173+
idxs, err := s.client.GetArtists(params)
169174
if err != nil {
170175
return nil, err
171176
}

backend/mediaprovider/subsonic/searchall.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ func (s *subsonicMediaProvider) SearchAll(searchQuery string, maxResults int) ([
2323
wg.Add(1)
2424
go func() {
2525
count := strconv.Itoa(maxResults / 3)
26-
res, e := s.client.Search3(searchQuery, map[string]string{
26+
params := map[string]string{
2727
"artistCount": count,
2828
"albumCount": count,
2929
"songCount": count,
30-
})
30+
}
31+
if s.currentLibraryID != "" {
32+
params["musicFolderId"] = s.currentLibraryID
33+
}
34+
res, e := s.client.Search3(searchQuery, params)
3135
if e != nil {
3236
err = e
3337
} else {

backend/mediaprovider/subsonic/searchiterbase.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import (
88
)
99

1010
type searchIterBase struct {
11-
query string
12-
artistOffset int
13-
albumOffset int
14-
songOffset int
15-
s *subsonic.Client
11+
musicFolderId string
12+
query string
13+
artistOffset int
14+
albumOffset int
15+
songOffset int
16+
s *subsonic.Client
1617
}
1718

1819
func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
@@ -21,6 +22,9 @@ func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
2122
"albumOffset": strconv.Itoa(s.albumOffset),
2223
"songOffset": strconv.Itoa(s.songOffset),
2324
}
25+
if s.musicFolderId != "" {
26+
searchOpts["musicFolderId"] = s.musicFolderId
27+
}
2428
results, err := s.s.Search3(s.query, searchOpts)
2529
if err != nil {
2630
log.Println(err)

0 commit comments

Comments
 (0)