- Adding the option to download the selected track or tracks. When the download is clicked it opens a dialog that lets you choose where you want to save the track and the file name (by default the file name is the track number, the track name and the extension, example "03 - my song.mp3") If there are many tracks selected it will open one dialog for each track - Next steps: download an album and a playlist using zip
338 lines
9.3 KiB
Go
338 lines
9.3 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"io"
|
|
"math"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dweymouth/go-subsonic/subsonic"
|
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
"github.com/dweymouth/supersonic/sharedutil"
|
|
)
|
|
|
|
type subsonicMediaProvider struct {
|
|
client *subsonic.Client
|
|
prefetchCoverCB func(coverArtID string)
|
|
}
|
|
|
|
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
|
return &subsonicMediaProvider{client: subsonicClient}
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID string)) {
|
|
s.prefetchCoverCB = cb
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
|
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name})
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) DeletePlaylist(id string) error {
|
|
return s.client.DeletePlaylist(id)
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) EditPlaylist(id, name, description string, public bool) error {
|
|
return s.client.UpdatePlaylist(id, map[string]string{
|
|
"name": name,
|
|
"comment": description,
|
|
"public": strconv.FormatBool(public),
|
|
})
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) EditPlaylistTracks(id string, trackIDsToAdd []string, trackIndexesToRemove []int) error {
|
|
return s.client.UpdatePlaylistTracks(id, trackIDsToAdd, trackIndexesToRemove)
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWithTracks, error) {
|
|
al, err := s.client.GetAlbum(albumID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
album := &mediaprovider.AlbumWithTracks{
|
|
Tracks: sharedutil.MapSlice(al.Song, toTrack),
|
|
}
|
|
fillAlbum(al, &album.Album)
|
|
return album, nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetArtist(artistID string) (*mediaprovider.ArtistWithAlbums, error) {
|
|
ar, err := s.client.GetArtist(artistID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &mediaprovider.ArtistWithAlbums{
|
|
Artist: mediaprovider.Artist{
|
|
ID: ar.ID,
|
|
Name: ar.Name,
|
|
Favorite: !ar.Starred.IsZero(),
|
|
AlbumCount: ar.AlbumCount,
|
|
},
|
|
Albums: sharedutil.MapSlice(ar.Album, toAlbum),
|
|
}, nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.ArtistInfo, error) {
|
|
info, err := s.client.GetArtistInfo2(artistID, map[string]string{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info == nil {
|
|
return nil, errors.New("server returned empty artist info")
|
|
}
|
|
return &mediaprovider.ArtistInfo{
|
|
Biography: info.Biography,
|
|
LastFMUrl: info.LastFmUrl,
|
|
ImageURL: info.LargeImageUrl,
|
|
SimilarArtists: sharedutil.MapSlice(info.SimilarArtist, toArtistFromID3),
|
|
}, nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) {
|
|
idxs, err := s.client.GetArtists(map[string]string{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var artists []*mediaprovider.Artist
|
|
for _, idx := range idxs.Index {
|
|
for _, ar := range idx.Artist {
|
|
artists = append(artists, toArtistFromID3(ar))
|
|
}
|
|
}
|
|
return artists, nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) {
|
|
params := map[string]string{}
|
|
if size > 0 {
|
|
params["size"] = strconv.Itoa(size)
|
|
}
|
|
return s.client.GetCoverArt(id, params)
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
|
|
fav, err := s.client.GetStarred2(map[string]string{})
|
|
if err != nil {
|
|
return mediaprovider.Favorites{}, err
|
|
}
|
|
return mediaprovider.Favorites{
|
|
Albums: sharedutil.MapSlice(fav.Album, toAlbum),
|
|
Artists: sharedutil.MapSlice(fav.Artist, toArtistFromID3),
|
|
Tracks: sharedutil.MapSlice(fav.Song, toTrack),
|
|
}, nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
|
|
g, err := s.client.GetGenres()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre {
|
|
return &mediaprovider.Genre{
|
|
Name: g.Name,
|
|
AlbumCount: g.AlbumCount,
|
|
TrackCount: g.SongCount,
|
|
}
|
|
}), nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.PlaylistWithTracks, error) {
|
|
pl, err := s.client.GetPlaylist(playlistID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
playlist := &mediaprovider.PlaylistWithTracks{
|
|
Tracks: sharedutil.MapSlice(pl.Entry, toTrack),
|
|
}
|
|
fillPlaylist(pl, &playlist.Playlist)
|
|
return playlist, nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) {
|
|
pl, err := s.client.GetPlaylists(map[string]string{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sharedutil.MapSlice(pl, toPlaylist), nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]*mediaprovider.Track, error) {
|
|
opts := map[string]string{"size": strconv.Itoa(count)}
|
|
if genreName != "" {
|
|
opts["genre"] = genreName
|
|
}
|
|
tr, err := s.client.GetRandomSongs(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sharedutil.MapSlice(tr, toTrack), nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([]*mediaprovider.Track, error) {
|
|
tr, err := s.client.GetSimilarSongs2(artistID, map[string]string{"count": strconv.Itoa(count)})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sharedutil.MapSlice(tr, toTrack), nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetStreamURL(trackID string) (string, error) {
|
|
u, err := s.client.GetStreamURL(trackID, map[string]string{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return u.String(), nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetTopTracks(artist mediaprovider.Artist, count int) ([]*mediaprovider.Track, error) {
|
|
params := map[string]string{}
|
|
if count > 0 {
|
|
params["count"] = strconv.Itoa(count)
|
|
}
|
|
tr, err := s.client.GetTopSongs(artist.Name, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sharedutil.MapSlice(tr, toTrack), nil
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) ReplacePlaylistTracks(playlistID string, trackIDs []string) error {
|
|
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"playlistId": playlistID})
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) Scrobble(trackID string, submission bool) error {
|
|
return s.client.Scrobble(trackID, map[string]string{
|
|
"time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
|
"submission": strconv.FormatBool(submission)})
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error {
|
|
subParams := subsonic.StarParameters{
|
|
AlbumIDs: params.AlbumIDs,
|
|
ArtistIDs: params.ArtistIDs,
|
|
SongIDs: params.TrackIDs,
|
|
}
|
|
if favorite {
|
|
return s.client.Star(subParams)
|
|
}
|
|
return s.client.Unstar(subParams)
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoriteParameters, rating int) error {
|
|
// Subsonic doesn't allow bulk setting ratings.
|
|
// To not overwhelm the server with requests, set rating for
|
|
// only 5 tracks at a time concurrently
|
|
batchSize := 5
|
|
var err error
|
|
batchSetRating := func(offs int, wg *sync.WaitGroup) {
|
|
for i := 0; i < batchSize && offs+i < len(params.TrackIDs); i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
newErr := s.client.SetRating(params.TrackIDs[idx], rating)
|
|
if err == nil && newErr != nil {
|
|
err = newErr
|
|
}
|
|
wg.Done()
|
|
}(offs + i)
|
|
}
|
|
}
|
|
|
|
numBatches := int(math.Ceil(float64(len(params.TrackIDs)) / float64(batchSize)))
|
|
for i := 0; i < numBatches; i++ {
|
|
var wg sync.WaitGroup
|
|
batchSetRating(i*batchSize, &wg)
|
|
wg.Wait()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) DownloadTrack(trackID string) (io.Reader, error) {
|
|
return s.client.Download(trackID)
|
|
}
|
|
|
|
func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
|
if ch == nil {
|
|
return nil
|
|
}
|
|
return &mediaprovider.Track{
|
|
ID: ch.ID,
|
|
CoverArtID: ch.CoverArt,
|
|
ParentID: ch.Parent,
|
|
Name: ch.Title,
|
|
Duration: ch.Duration,
|
|
TrackNumber: ch.Track,
|
|
DiscNumber: ch.DiscNumber,
|
|
Genre: ch.Genre,
|
|
ArtistIDs: []string{ch.ArtistID},
|
|
ArtistNames: []string{ch.Artist},
|
|
Album: ch.Album,
|
|
AlbumID: ch.AlbumID,
|
|
Year: ch.Year,
|
|
Rating: ch.UserRating,
|
|
Favorite: !ch.Starred.IsZero(),
|
|
PlayCount: int(ch.PlayCount),
|
|
FilePath: ch.Path,
|
|
Size: ch.Size,
|
|
BitRate: ch.BitRate,
|
|
}
|
|
}
|
|
|
|
func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album {
|
|
if al == nil {
|
|
return nil
|
|
}
|
|
album := &mediaprovider.Album{}
|
|
fillAlbum(al, album)
|
|
return album
|
|
}
|
|
|
|
func fillAlbum(subAlbum *subsonic.AlbumID3, album *mediaprovider.Album) {
|
|
album.ID = subAlbum.ID
|
|
album.CoverArtID = subAlbum.CoverArt
|
|
album.Name = subAlbum.Name
|
|
album.Duration = subAlbum.Duration
|
|
album.ArtistIDs = []string{subAlbum.ArtistID}
|
|
album.ArtistNames = []string{subAlbum.Artist}
|
|
album.Year = subAlbum.Year
|
|
album.TrackCount = subAlbum.SongCount
|
|
album.Genres = []string{subAlbum.Genre}
|
|
album.Favorite = !subAlbum.Starred.IsZero()
|
|
}
|
|
|
|
func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist {
|
|
if ar == nil {
|
|
return nil
|
|
}
|
|
return &mediaprovider.Artist{
|
|
ID: ar.ID,
|
|
CoverArtID: ar.CoverArt,
|
|
Name: ar.Name,
|
|
Favorite: !ar.Starred.IsZero(),
|
|
AlbumCount: ar.AlbumCount,
|
|
}
|
|
}
|
|
|
|
func toPlaylist(pl *subsonic.Playlist) *mediaprovider.Playlist {
|
|
if pl == nil {
|
|
return nil
|
|
}
|
|
playlist := &mediaprovider.Playlist{}
|
|
fillPlaylist(pl, playlist)
|
|
return playlist
|
|
}
|
|
|
|
func fillPlaylist(pl *subsonic.Playlist, playlist *mediaprovider.Playlist) {
|
|
playlist.Name = pl.Name
|
|
playlist.ID = pl.ID
|
|
playlist.CoverArtID = pl.CoverArt
|
|
playlist.Description = pl.Comment
|
|
playlist.Owner = pl.Owner
|
|
playlist.Public = pl.Public
|
|
playlist.TrackCount = pl.SongCount
|
|
playlist.Duration = pl.Duration
|
|
}
|