From d701375d352a875cbd47774e1e84ee425e5c793d Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 12 Jul 2025 12:43:55 -0700 Subject: [PATCH] implement library browsing for Subsonic --- .../jellyfin/jellyfinmediaprovider.go | 8 +++++ backend/mediaprovider/mediaprovider.go | 9 +++++ backend/mediaprovider/model.go | 5 +++ .../mediaprovider/subsonic/albumiterator.go | 35 ++++++++++++++----- .../mediaprovider/subsonic/artistiterator.go | 11 ++++-- backend/mediaprovider/subsonic/searchall.go | 8 +++-- .../mediaprovider/subsonic/searchiterbase.go | 14 +++++--- .../subsonic/subsonicmediaprovider.go | 26 +++++++++++++- .../mediaprovider/subsonic/trackiterator.go | 5 +-- 9 files changed, 99 insertions(+), 22 deletions(-) diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index 19faba7..4857cac 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -59,6 +59,14 @@ func (j *jellyfinMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID stri j.prefetchCoverCB = cb } +func (j *jellyfinMediaProvider) GetLibraries() ([]mediaprovider.Library, error) { + return nil, nil +} + +func (j *jellyfinMediaProvider) SetLibrary(string) error { + return nil +} + func (j *jellyfinMediaProvider) CreatePlaylist(name string, trackIDs []string) error { return j.client.CreatePlaylist(name, trackIDs) } 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), }