implement library browsing for Subsonic
This commit is contained in:
@@ -59,6 +59,14 @@ func (j *jellyfinMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID stri
|
|||||||
j.prefetchCoverCB = cb
|
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 {
|
func (j *jellyfinMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
||||||
return j.client.CreatePlaylist(name, trackIDs)
|
return j.client.CreatePlaylist(name, trackIDs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user