begin the refactoring - doesnt compile
This commit is contained in:
@@ -1,397 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
)
|
||||
|
||||
type AlbumSortOrder string
|
||||
|
||||
const (
|
||||
AlbumSortRecentlyAdded AlbumSortOrder = "Recently Added"
|
||||
AlbumSortRecentlyPlayed AlbumSortOrder = "Recently Played"
|
||||
AlbumSortFrequentlyPlayed AlbumSortOrder = "Frequently Played"
|
||||
AlbumSortRandom AlbumSortOrder = "Random"
|
||||
AlbumSortTitleAZ AlbumSortOrder = "Title (A-Z)"
|
||||
AlbumSortArtistAZ AlbumSortOrder = "Artist (A-Z)"
|
||||
AlbumSortYearAscending AlbumSortOrder = "Year (ascending)"
|
||||
AlbumSortYearDescending AlbumSortOrder = "Year (descending)"
|
||||
)
|
||||
|
||||
var (
|
||||
AlbumSortOrders []string = []string{
|
||||
string(AlbumSortRecentlyAdded),
|
||||
string(AlbumSortRecentlyPlayed),
|
||||
string(AlbumSortFrequentlyPlayed),
|
||||
string(AlbumSortRandom),
|
||||
string(AlbumSortTitleAZ),
|
||||
string(AlbumSortArtistAZ),
|
||||
string(AlbumSortYearAscending),
|
||||
string(AlbumSortYearDescending),
|
||||
}
|
||||
)
|
||||
|
||||
type AlbumFilter struct {
|
||||
MinYear int
|
||||
MaxYear int // 0 == unset/match any
|
||||
Genres []string // len(0) == unset/match any
|
||||
|
||||
ExcludeFavorited bool // mut. exc. with ExcludeUnfavorited
|
||||
ExcludeUnfavorited bool // mut. exc. with ExcludeFavorited
|
||||
}
|
||||
|
||||
func (f *AlbumFilter) Matches(album *subsonic.AlbumID3) bool {
|
||||
if album == nil {
|
||||
return false
|
||||
}
|
||||
if f.ExcludeFavorited && !album.Starred.IsZero() {
|
||||
return false
|
||||
}
|
||||
if f.ExcludeUnfavorited && album.Starred.IsZero() {
|
||||
return false
|
||||
}
|
||||
if y := album.Year; y < f.MinYear || (f.MaxYear > 0 && y > f.MaxYear) {
|
||||
return false
|
||||
}
|
||||
if len(f.Genres) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, g := range f.Genres {
|
||||
if strings.EqualFold(g, album.Genre) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *AlbumFilter) IsEmpty() bool {
|
||||
return !f.ExcludeFavorited && !f.ExcludeUnfavorited &&
|
||||
f.MinYear == 0 && f.MaxYear == 0 && len(f.Genres) == 0
|
||||
}
|
||||
|
||||
func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder, filter AlbumFilter) AlbumIterator {
|
||||
switch sort {
|
||||
case AlbumSortRecentlyAdded:
|
||||
return l.newBaseIter("newest", filter, make(map[string]string))
|
||||
case AlbumSortRecentlyPlayed:
|
||||
return l.newBaseIter("recent", filter, make(map[string]string))
|
||||
case AlbumSortFrequentlyPlayed:
|
||||
return l.newBaseIter("frequent", filter, make(map[string]string))
|
||||
case AlbumSortRandom:
|
||||
return l.newRandomIter()
|
||||
case AlbumSortTitleAZ:
|
||||
return l.newBaseIter("alphabeticalByName", filter, make(map[string]string))
|
||||
case AlbumSortArtistAZ:
|
||||
return l.newBaseIter("alphabeticalByArtist", filter, make(map[string]string))
|
||||
case AlbumSortYearAscending:
|
||||
return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "0", "toYear": "3000"})
|
||||
case AlbumSortYearDescending:
|
||||
return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "3000", "toYear": "0"})
|
||||
default:
|
||||
log.Printf("Undefined album sort order: %s", sort)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LibraryManager) StarredIter(filter AlbumFilter) AlbumIterator {
|
||||
return l.newBaseIter("starred", filter, make(map[string]string))
|
||||
}
|
||||
|
||||
func (l *LibraryManager) GenreIter(genre string, filter AlbumFilter) AlbumIterator {
|
||||
return l.newBaseIter("byGenre", filter, map[string]string{"genre": genre})
|
||||
}
|
||||
|
||||
func (l *LibraryManager) SearchIter(query string) AlbumIterator {
|
||||
return l.newSearchIter(query, AlbumFilter{})
|
||||
}
|
||||
|
||||
func (l *LibraryManager) SearchIterWithFilter(query string, filter AlbumFilter) AlbumIterator {
|
||||
return l.newSearchIter(query, filter)
|
||||
}
|
||||
|
||||
func (l *LibraryManager) GetAlbum(id string) (*subsonic.AlbumID3, error) {
|
||||
a, err := l.s.Server.GetAlbum(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
type baseIter struct {
|
||||
listType string
|
||||
filter AlbumFilter
|
||||
serverPos int
|
||||
l *LibraryManager
|
||||
s *subsonic.Client
|
||||
opts map[string]string
|
||||
prefetched []*subsonic.AlbumID3
|
||||
prefetchedPos int
|
||||
done bool
|
||||
}
|
||||
|
||||
func (l *LibraryManager) newBaseIter(listType string, filter AlbumFilter, opts map[string]string) *baseIter {
|
||||
return &baseIter{
|
||||
listType: listType,
|
||||
filter: filter,
|
||||
l: l,
|
||||
s: l.s.Server,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *baseIter) Next() *subsonic.AlbumID3 {
|
||||
if r.done {
|
||||
return nil
|
||||
}
|
||||
if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) {
|
||||
a := r.prefetched[r.prefetchedPos]
|
||||
r.prefetchedPos++
|
||||
return a
|
||||
}
|
||||
r.prefetched = nil
|
||||
for { // keep fetching until we are done or have mathcing results
|
||||
r.opts["offset"] = strconv.Itoa(r.serverPos)
|
||||
albums, err := r.s.GetAlbumList2(r.listType, r.opts)
|
||||
if err != nil {
|
||||
log.Printf("error fetching albums: %s", err.Error())
|
||||
albums = nil
|
||||
}
|
||||
if len(albums) == 0 {
|
||||
r.done = true
|
||||
return nil
|
||||
}
|
||||
r.serverPos += len(albums)
|
||||
albums = sharedutil.FilterSlice(albums, r.filter.Matches)
|
||||
r.prefetched = albums
|
||||
if len(albums) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.prefetchedPos = 1
|
||||
if r.l.PreCacheCoverFn != nil {
|
||||
for _, album := range r.prefetched {
|
||||
go r.l.PreCacheCoverFn(album.CoverArt)
|
||||
}
|
||||
}
|
||||
|
||||
return r.prefetched[0]
|
||||
}
|
||||
|
||||
type searchIter struct {
|
||||
searchIterBase
|
||||
|
||||
l *LibraryManager
|
||||
filter AlbumFilter
|
||||
prefetched []*subsonic.AlbumID3
|
||||
prefetchedPos int
|
||||
albumIDset map[string]bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func (l *LibraryManager) newSearchIter(query string, filter AlbumFilter) *searchIter {
|
||||
return &searchIter{
|
||||
searchIterBase: searchIterBase{
|
||||
query: query,
|
||||
s: l.s.Server,
|
||||
},
|
||||
l: l,
|
||||
filter: filter,
|
||||
albumIDset: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *searchIter) Next() *subsonic.AlbumID3 {
|
||||
if s.done {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prefetch more search results from server
|
||||
if s.prefetched == nil {
|
||||
results := s.searchIterBase.fetchResults()
|
||||
if results == nil {
|
||||
s.done = true
|
||||
s.albumIDset = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// add results from albums search
|
||||
s.addNewAlbums(results.Album)
|
||||
s.albumOffset += len(results.Album)
|
||||
|
||||
// add results from artists search
|
||||
for _, artist := range results.Artist {
|
||||
artist, err := s.s.GetArtist(artist.ID)
|
||||
if err != nil || artist == nil {
|
||||
log.Printf("error fetching artist: %s", err.Error())
|
||||
} else {
|
||||
s.addNewAlbums(artist.Album)
|
||||
}
|
||||
}
|
||||
s.artistOffset += len(results.Artist)
|
||||
|
||||
// add results from songs search
|
||||
for _, song := range results.Song {
|
||||
if song.AlbumID == "" {
|
||||
continue
|
||||
}
|
||||
album, err := s.s.GetAlbum(song.AlbumID)
|
||||
if err != nil || album == nil {
|
||||
log.Printf("error fetching album: %s", err.Error())
|
||||
} else {
|
||||
s.addNewAlbums([]*subsonic.AlbumID3{album})
|
||||
}
|
||||
}
|
||||
s.songOffset += len(results.Song)
|
||||
}
|
||||
|
||||
// return from prefetched results
|
||||
if len(s.prefetched) > 0 {
|
||||
a := s.prefetched[s.prefetchedPos]
|
||||
s.prefetchedPos++
|
||||
if s.prefetchedPos == len(s.prefetched) {
|
||||
s.prefetched = nil
|
||||
s.prefetchedPos = 0
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) {
|
||||
for _, album := range al {
|
||||
if _, have := s.albumIDset[album.ID]; have {
|
||||
continue
|
||||
}
|
||||
if !s.filter.Matches(album) {
|
||||
continue
|
||||
}
|
||||
s.prefetched = append(s.prefetched, album)
|
||||
if s.l.PreCacheCoverFn != nil {
|
||||
go s.l.PreCacheCoverFn(album.CoverArt)
|
||||
}
|
||||
s.albumIDset[album.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
type randomIter struct {
|
||||
albumIDSet map[string]bool
|
||||
l *LibraryManager
|
||||
s *subsonic.Client
|
||||
prefetched []*subsonic.AlbumID3
|
||||
prefetchedPos int
|
||||
// Random iter works in two phases - phase 1 by requesting random
|
||||
// albums from the server. Since the Subsonic API provides no way
|
||||
// of paginating a single random sort, we may get albums back twice.
|
||||
// We use albumIDSet to keep track of which albums have already been returned.
|
||||
// Once we start getting back too many already-returned albums,
|
||||
// switch to requesting more albums from a deterministic sort order.
|
||||
phaseTwo bool
|
||||
offset int
|
||||
done bool
|
||||
}
|
||||
|
||||
func (l *LibraryManager) newRandomIter() *randomIter {
|
||||
return &randomIter{
|
||||
l: l,
|
||||
s: l.s.Server,
|
||||
albumIDSet: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *randomIter) Next() *subsonic.AlbumID3 {
|
||||
if r.done {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.prefetched == nil {
|
||||
if r.phaseTwo {
|
||||
for len(r.prefetched) == 0 {
|
||||
albums, err := r.s.GetAlbumList2("newest", map[string]string{"size": "20", "offset": strconv.Itoa(r.offset)})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
albums = nil
|
||||
}
|
||||
if len(albums) == 0 {
|
||||
r.done = true
|
||||
r.albumIDSet = nil
|
||||
return nil
|
||||
}
|
||||
r.offset += len(albums)
|
||||
for _, album := range albums {
|
||||
if _, ok := r.albumIDSet[album.ID]; !ok {
|
||||
r.prefetched = append(r.prefetched, album)
|
||||
if r.l.PreCacheCoverFn != nil {
|
||||
go r.l.PreCacheCoverFn(album.CoverArt)
|
||||
}
|
||||
r.albumIDSet[album.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
r.prefetchedPos = 0
|
||||
} else {
|
||||
albums, err := r.s.GetAlbumList2("random", map[string]string{"size": "25"})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
r.done = true
|
||||
r.albumIDSet = nil
|
||||
return nil
|
||||
}
|
||||
var hitCount int
|
||||
for _, album := range albums {
|
||||
if _, ok := r.albumIDSet[album.ID]; !ok {
|
||||
hitCount++
|
||||
r.prefetched = append(r.prefetched, album)
|
||||
if r.l.PreCacheCoverFn != nil {
|
||||
go r.l.PreCacheCoverFn(album.CoverArt)
|
||||
}
|
||||
r.albumIDSet[album.ID] = true
|
||||
}
|
||||
}
|
||||
if successRatio := float64(hitCount) / float64(25); successRatio < 0.3 {
|
||||
r.phaseTwo = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return from prefetched results
|
||||
if len(r.prefetched) > 0 {
|
||||
a := r.prefetched[r.prefetchedPos]
|
||||
r.prefetchedPos++
|
||||
if r.prefetchedPos == len(r.prefetched) {
|
||||
r.prefetched = nil
|
||||
r.prefetchedPos = 0
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type BatchingIterator struct {
|
||||
iter AlbumIterator
|
||||
}
|
||||
|
||||
func NewBatchingIterator(iter AlbumIterator) *BatchingIterator {
|
||||
return &BatchingIterator{iter}
|
||||
}
|
||||
|
||||
func (b *BatchingIterator) NextN(n int) []*subsonic.AlbumID3 {
|
||||
results := make([]*subsonic.AlbumID3, 0, n)
|
||||
i := 0
|
||||
for i < n {
|
||||
album := b.iter.Next()
|
||||
if album == nil {
|
||||
break
|
||||
}
|
||||
results = append(results, album)
|
||||
i++
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
|
||||
const CachedImageValidTime = 24 * time.Hour
|
||||
|
||||
const coverArtThumbnailSize = 300
|
||||
|
||||
type ImageManager struct {
|
||||
s *ServerManager
|
||||
baseCacheDir string
|
||||
@@ -72,7 +74,7 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error)
|
||||
if i.cachedFullSizeCoverID == coverID {
|
||||
return i.cachedFullSizeCover, nil
|
||||
}
|
||||
im, err := i.s.Server.GetCoverArt(coverID, nil)
|
||||
im, err := i.s.Server.GetCoverArt(coverID, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -143,7 +145,7 @@ func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(coverID string, ttl ti
|
||||
}
|
||||
|
||||
func (i *ImageManager) fetchAndCacheCoverFromServer(coverID string, ttl time.Duration) (image.Image, error) {
|
||||
img, err := i.s.Server.GetCoverArt(coverID, map[string]string{"size": "300"})
|
||||
img, err := i.s.Server.GetCoverArt(coverID, coverArtThumbnailSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,17 +23,3 @@ func NewLibraryManager(s *ServerManager) *LibraryManager {
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LibraryManager) GetUserOwnedPlaylists() ([]*subsonic.Playlist, error) {
|
||||
pl, err := l.s.Server.GetPlaylists(nil)
|
||||
userPl := make([]*subsonic.Playlist, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range pl {
|
||||
if p.Owner == l.s.Server.User {
|
||||
userPl = append(userPl, p)
|
||||
}
|
||||
}
|
||||
return userPl, nil
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ type RatingFavoriteParameters struct {
|
||||
}
|
||||
|
||||
type Favorites struct {
|
||||
Albums []Album
|
||||
Artists []Artist
|
||||
Tracks []Track
|
||||
Albums []*Album
|
||||
Artists []*Artist
|
||||
Tracks []*Track
|
||||
}
|
||||
|
||||
type MediaProvider interface {
|
||||
@@ -48,19 +48,19 @@ type MediaProvider interface {
|
||||
|
||||
IterateTracks(searchQuery string) TrackIterator
|
||||
|
||||
GetRandomTracks(genre string, count int) ([]Track, error)
|
||||
GetRandomTracks(genre string, count int) ([]*Track, error)
|
||||
|
||||
GetSimilarTracks(artistID string, count int) ([]Track, error)
|
||||
GetSimilarTracks(artistID string, count int) ([]*Track, error)
|
||||
|
||||
GetArtists() ([]Artist, error)
|
||||
GetArtists() ([]*Artist, error)
|
||||
|
||||
GetGenres() ([]Genre, error)
|
||||
GetGenres() ([]*Genre, error)
|
||||
|
||||
GetFavorites() (Favorites, error)
|
||||
|
||||
GetStreamURL(trackID string) (string, error)
|
||||
|
||||
SetFavorite(params RatingFavoriteParameters) error
|
||||
SetFavorite(params RatingFavoriteParameters, favorite bool) error
|
||||
|
||||
SetRating(params RatingFavoriteParameters, rating int) error
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type Album struct {
|
||||
|
||||
type AlbumWithTracks struct {
|
||||
Album
|
||||
Tracks []Track
|
||||
Tracks []*Track
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
@@ -26,14 +26,14 @@ type Artist struct {
|
||||
|
||||
type ArtistWithAlbums struct {
|
||||
Artist
|
||||
Albums []Album
|
||||
Albums []*Album
|
||||
}
|
||||
|
||||
type ArtistInfo struct {
|
||||
Biography string
|
||||
LastFMUrl string
|
||||
ImageURL string
|
||||
SimilarArtists []Artist
|
||||
SimilarArtists []*Artist
|
||||
}
|
||||
|
||||
type Genre struct {
|
||||
@@ -54,11 +54,14 @@ type Track struct {
|
||||
ArtistIDs []string
|
||||
ArtistNames []string
|
||||
Album string
|
||||
AlbumID string
|
||||
Year int
|
||||
Rating int
|
||||
Favorite bool
|
||||
Size int64
|
||||
PlayCount int
|
||||
FilePath string
|
||||
BitRate int
|
||||
}
|
||||
|
||||
type Playlist struct {
|
||||
@@ -73,5 +76,5 @@ type Playlist struct {
|
||||
|
||||
type PlaylistWithTracks struct {
|
||||
Playlist
|
||||
Tracks []Track
|
||||
Tracks []*Track
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ type baseIter struct {
|
||||
serverPos int
|
||||
s *subsonic.Client
|
||||
opts map[string]string
|
||||
prefetched []mediaprovider.Album
|
||||
prefetched []*mediaprovider.Album
|
||||
prefetchedPos int
|
||||
done bool
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (r *baseIter) Next() *mediaprovider.Album {
|
||||
if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) {
|
||||
a := r.prefetched[r.prefetchedPos]
|
||||
r.prefetchedPos++
|
||||
return &a
|
||||
return a
|
||||
}
|
||||
r.prefetched = nil
|
||||
for { // keep fetching until we are done or have mathcing results
|
||||
@@ -129,8 +129,7 @@ func (r *baseIter) Next() *mediaprovider.Album {
|
||||
}
|
||||
*/
|
||||
|
||||
ret := r.prefetched[0]
|
||||
return &ret
|
||||
return r.prefetched[0]
|
||||
}
|
||||
|
||||
type searchIter struct {
|
||||
@@ -207,8 +206,7 @@ func (s *searchIter) Next() *mediaprovider.Album {
|
||||
s.prefetchedPos = 0
|
||||
}
|
||||
|
||||
al := toAlbum(a)
|
||||
return &al
|
||||
return toAlbum(a)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -323,8 +321,7 @@ func (r *randomIter) Next() *mediaprovider.Album {
|
||||
r.prefetchedPos = 0
|
||||
}
|
||||
|
||||
al := toAlbum(a)
|
||||
return &al
|
||||
return toAlbum(a)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,7 +2,6 @@ package subsonic
|
||||
|
||||
import (
|
||||
"image"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -115,12 +114,12 @@ func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) GetArtists() ([]mediaprovider.Artist, error) {
|
||||
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
|
||||
var artists []*mediaprovider.Artist
|
||||
for _, idx := range idxs.Index {
|
||||
for _, ar := range idx.Artist {
|
||||
artists = append(artists, toArtistFromID3(ar))
|
||||
@@ -130,7 +129,11 @@ func (s *subsonicMediaProvider) GetArtists() ([]mediaprovider.Artist, error) {
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) {
|
||||
return s.client.GetCoverArt(id, map[string]string{"size": strconv.Itoa(size)})
|
||||
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) {
|
||||
@@ -145,13 +148,13 @@ func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) GetGenres() ([]mediaprovider.Genre, error) {
|
||||
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{
|
||||
return sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre {
|
||||
return &mediaprovider.Genre{
|
||||
Name: g.Name,
|
||||
AlbumCount: g.AlbumCount,
|
||||
TrackCount: g.SongCount,
|
||||
@@ -178,7 +181,7 @@ func (s *subsonicMediaProvider) GetPlaylists() ([]mediaprovider.Playlist, error)
|
||||
return sharedutil.MapSlice(pl, toPlaylist), nil
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]mediaprovider.Track, error) {
|
||||
func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]*mediaprovider.Track, error) {
|
||||
opts := map[string]string{"size": strconv.Itoa(count)}
|
||||
if genreName != "" {
|
||||
opts["genre"] = genreName
|
||||
@@ -190,7 +193,7 @@ func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]
|
||||
return sharedutil.MapSlice(tr, toTrack), nil
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([]mediaprovider.Track, error) {
|
||||
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
|
||||
@@ -216,12 +219,16 @@ func (s *subsonicMediaProvider) Scrobble(trackID string, submission bool) error
|
||||
"submission": strconv.FormatBool(submission)})
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters) error {
|
||||
return s.client.Star(subsonic.StarParameters{
|
||||
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 {
|
||||
@@ -263,12 +270,11 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar
|
||||
return err
|
||||
}
|
||||
|
||||
func toTrack(ch *subsonic.Child) mediaprovider.Track {
|
||||
func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
||||
if ch == nil {
|
||||
log.Println("subsonicMediaProvider: toTrack called on nil track")
|
||||
return mediaprovider.Track{}
|
||||
return nil
|
||||
}
|
||||
return mediaprovider.Track{
|
||||
return &mediaprovider.Track{
|
||||
ID: ch.ID,
|
||||
CoverArtID: ch.CoverArt,
|
||||
ParentID: ch.Parent,
|
||||
@@ -280,19 +286,21 @@ func toTrack(ch *subsonic.Child) mediaprovider.Track {
|
||||
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,
|
||||
BitRate: ch.BitRate,
|
||||
}
|
||||
}
|
||||
|
||||
func toAlbum(al *subsonic.AlbumID3) mediaprovider.Album {
|
||||
func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album {
|
||||
if al == nil {
|
||||
log.Println("subsonicMediaProvider: toAlbum called on nil album")
|
||||
return mediaprovider.Album{}
|
||||
return nil
|
||||
}
|
||||
return mediaprovider.Album{
|
||||
return &mediaprovider.Album{
|
||||
ID: al.ID,
|
||||
CoverArtID: al.CoverArt,
|
||||
Name: al.Name,
|
||||
@@ -306,23 +314,21 @@ func toAlbum(al *subsonic.AlbumID3) mediaprovider.Album {
|
||||
}
|
||||
}
|
||||
|
||||
func toArtist(ar *subsonic.Artist) mediaprovider.Artist {
|
||||
func toArtist(ar *subsonic.Artist) *mediaprovider.Artist {
|
||||
if ar == nil {
|
||||
log.Println("subsonicMediaProvider: toArtist called on nil artist")
|
||||
return mediaprovider.Artist{}
|
||||
return nil
|
||||
}
|
||||
return mediaprovider.Artist{
|
||||
return &mediaprovider.Artist{
|
||||
ID: ar.ID,
|
||||
Name: ar.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toArtistFromID3(ar *subsonic.ArtistID3) mediaprovider.Artist {
|
||||
func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist {
|
||||
if ar == nil {
|
||||
log.Println("subsonicMediaProvider: toArtistFromID3 called on nil artistID3")
|
||||
return mediaprovider.Artist{}
|
||||
return nil
|
||||
}
|
||||
return mediaprovider.Artist{
|
||||
return &mediaprovider.Artist{
|
||||
ID: ar.ID,
|
||||
Name: ar.Name,
|
||||
AlbumCount: ar.AlbumCount,
|
||||
|
||||
@@ -58,7 +58,7 @@ func (a *allTracksIterator) Next() *mediaprovider.Track {
|
||||
|
||||
tr := a.curAlbum.Tracks[a.curTrackIdx]
|
||||
a.curTrackIdx += 1
|
||||
return &tr
|
||||
return tr
|
||||
}
|
||||
|
||||
type searchTracksIterator struct {
|
||||
@@ -109,8 +109,7 @@ func (s *searchTracksIterator) Next() *mediaprovider.Track {
|
||||
s.prefetched = s.prefetched[:0]
|
||||
s.prefetchedPos = 0
|
||||
}
|
||||
track := toTrack(tr)
|
||||
return &track
|
||||
return toTrack(tr)
|
||||
}
|
||||
|
||||
// no more results
|
||||
|
||||
+21
-35
@@ -3,14 +3,12 @@ package backend
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/backend/util"
|
||||
"github.com/dweymouth/supersonic/player"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,14 +31,14 @@ type PlaybackManager struct {
|
||||
curTrackTime float64
|
||||
callbacksDisabled bool
|
||||
|
||||
playQueue []*subsonic.Child
|
||||
playQueue []*mediaprovider.Track
|
||||
nowPlayingIdx int64
|
||||
|
||||
// to pass to onSongChange listeners; clear once listeners have been called
|
||||
lastScrobbled *subsonic.Child
|
||||
lastScrobbled *mediaprovider.Track
|
||||
scrobbleCfg *ScrobbleConfig
|
||||
|
||||
onSongChange []func(nowPlaying *subsonic.Child, justScrobbledIfAny *subsonic.Child)
|
||||
onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track)
|
||||
onPlayTimeUpdate []func(float64, float64)
|
||||
}
|
||||
|
||||
@@ -111,7 +109,7 @@ func (p *PlaybackManager) DisableCallbacks() {
|
||||
}
|
||||
|
||||
// Gets the curently playing song, if any.
|
||||
func (p *PlaybackManager) NowPlaying() *subsonic.Child {
|
||||
func (p *PlaybackManager) NowPlaying() *mediaprovider.Track {
|
||||
if len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped {
|
||||
return nil
|
||||
}
|
||||
@@ -119,7 +117,7 @@ func (p *PlaybackManager) NowPlaying() *subsonic.Child {
|
||||
}
|
||||
|
||||
// Sets a callback that is notified whenever a new song begins playing.
|
||||
func (p *PlaybackManager) OnSongChange(cb func(nowPlaying *subsonic.Child, justScrobbledIfAny *subsonic.Child)) {
|
||||
func (p *PlaybackManager) OnSongChange(cb func(nowPlaying *mediaprovider.Track, justScrobbledIfAny *mediaprovider.Track)) {
|
||||
p.onSongChange = append(p.onSongChange, cb)
|
||||
}
|
||||
|
||||
@@ -134,7 +132,7 @@ func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.LoadTracks(album.Song, appendToQueue, shuffle)
|
||||
return p.LoadTracks(album.Tracks, appendToQueue, shuffle)
|
||||
}
|
||||
|
||||
// Loads the specified playlist into the play queue.
|
||||
@@ -143,10 +141,10 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool, sh
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.LoadTracks(playlist.Entry, appendToQueue, shuffle)
|
||||
return p.LoadTracks(playlist.Tracks, appendToQueue, shuffle)
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue, shuffle bool) error {
|
||||
func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, appendToQueue, shuffle bool) error {
|
||||
if !appendToQueue {
|
||||
p.player.Stop()
|
||||
p.nowPlayingIdx = 0
|
||||
@@ -157,11 +155,11 @@ func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue, sh
|
||||
util.ShuffleSlice(nums)
|
||||
}
|
||||
for _, i := range nums {
|
||||
url, err := p.sm.Server.GetStreamURL(tracks[i].ID, map[string]string{})
|
||||
url, err := p.sm.Server.GetStreamURL(tracks[i].ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.player.AppendFile(url.String())
|
||||
p.player.AppendFile(url)
|
||||
// ensure a deep copy of the track info so that we can maintain our own state
|
||||
// (tracking play count increases, favorite, and rating) without messing up
|
||||
// other views' track models
|
||||
@@ -200,11 +198,7 @@ func (p *PlaybackManager) PlayTrackAt(idx int) error {
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) PlayRandomSongs(genreName string) {
|
||||
params := map[string]string{"size": "100"}
|
||||
if genreName != "" {
|
||||
params["genre"] = genreName
|
||||
}
|
||||
if songs, err := p.sm.Server.GetRandomSongs(params); err != nil {
|
||||
if songs, err := p.sm.Server.GetRandomTracks(genreName, 100); err != nil {
|
||||
log.Printf("error getting random songs: %s", err.Error())
|
||||
} else {
|
||||
p.LoadTracks(songs, false, false)
|
||||
@@ -213,8 +207,7 @@ func (p *PlaybackManager) PlayRandomSongs(genreName string) {
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) PlaySimilarSongs(id string) {
|
||||
params := map[string]string{"size": "100"}
|
||||
if songs, err := p.sm.Server.GetSimilarSongs2(id, params); err != nil {
|
||||
if songs, err := p.sm.Server.GetSimilarTracks(id, 100); err != nil {
|
||||
log.Printf("error getting similar songs: %s", err.Error())
|
||||
} else {
|
||||
p.LoadTracks(songs, false, false)
|
||||
@@ -222,8 +215,8 @@ func (p *PlaybackManager) PlaySimilarSongs(id string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child {
|
||||
pq := make([]*subsonic.Child, len(p.playQueue))
|
||||
func (p *PlaybackManager) GetPlayQueue() []*mediaprovider.Track {
|
||||
pq := make([]*mediaprovider.Track, len(p.playQueue))
|
||||
for i, tr := range p.playQueue {
|
||||
copy := *tr
|
||||
pq[i] = ©
|
||||
@@ -235,11 +228,7 @@ func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child {
|
||||
// this should be called to ensure the in-memory track model is updated.
|
||||
func (p *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) {
|
||||
if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil {
|
||||
if fav {
|
||||
tr.Starred = time.Now()
|
||||
} else {
|
||||
tr.Starred = time.Time{}
|
||||
}
|
||||
tr.Favorite = fav
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,13 +236,13 @@ func (p *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) {
|
||||
// this should be called to ensure the in-memory track model is updated.
|
||||
func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) {
|
||||
if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil {
|
||||
tr.UserRating = rating
|
||||
tr.Rating = rating
|
||||
}
|
||||
}
|
||||
|
||||
// trackIdxs must be sorted
|
||||
func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) {
|
||||
newQueue := make([]*subsonic.Child, 0, len(p.playQueue)-len(trackIdxs))
|
||||
newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIdxs))
|
||||
rmCount := 0
|
||||
rmIdx := 0
|
||||
for i, tr := range p.playQueue {
|
||||
@@ -310,10 +299,10 @@ func (p *PlaybackManager) checkScrobble(playDur time.Duration) {
|
||||
playDur.Seconds() >= float64(p.scrobbleCfg.ThresholdTimeSeconds)
|
||||
if timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent) {
|
||||
song := p.playQueue[p.nowPlayingIdx]
|
||||
log.Printf("Scrobbling %q", song.Title)
|
||||
log.Printf("Scrobbling %q", song.Name)
|
||||
song.PlayCount += 1
|
||||
p.lastScrobbled = song
|
||||
go p.sm.Server.Scrobble(song.ID, map[string]string{"time": strconv.FormatInt(time.Now().Unix()*1000, 10)})
|
||||
go p.sm.Server.Scrobble(song.ID, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,10 +311,7 @@ func (p *PlaybackManager) sendNowPlayingScrobble() {
|
||||
return
|
||||
}
|
||||
song := p.playQueue[p.nowPlayingIdx]
|
||||
go p.sm.Server.Scrobble(song.ID, map[string]string{
|
||||
"time": strconv.FormatInt(time.Now().Unix()*1000, 10),
|
||||
"submission": "false",
|
||||
})
|
||||
go p.sm.Server.Scrobble(song.ID, false)
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) invokeOnSongChangeCallbacks() {
|
||||
|
||||
@@ -7,13 +7,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
subsonicMP "github.com/dweymouth/supersonic/backend/mediaprovider/subsonic"
|
||||
"github.com/google/uuid"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
type ServerManager struct {
|
||||
ServerID uuid.UUID
|
||||
Server *subsonic.Client
|
||||
LoggedInUser string
|
||||
ServerID uuid.UUID
|
||||
Server mediaprovider.MediaProvider
|
||||
|
||||
appName string
|
||||
onServerConnected []func()
|
||||
@@ -31,7 +34,8 @@ func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Server = cli
|
||||
s.Server = subsonicMP.SubsonicMediaProvider(cli)
|
||||
s.LoggedInUser = conf.Username
|
||||
s.ServerID = conf.ID
|
||||
for _, cb := range s.onServerConnected {
|
||||
cb()
|
||||
@@ -116,6 +120,7 @@ func (s *ServerManager) Logout() {
|
||||
cb()
|
||||
}
|
||||
s.Server = nil
|
||||
s.LoggedInUser = ""
|
||||
s.ServerID = uuid.UUID{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/dweymouth/go-subsonic/subsonic"
|
||||
)
|
||||
|
||||
func (l *LibraryManager) AllTracksIterator() TrackIterator {
|
||||
return &allTracksIterator{
|
||||
l: l,
|
||||
albumIter: l.AlbumsIter(AlbumSortArtistAZ, AlbumFilter{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LibraryManager) SearchTracksIterator(query string) TrackIterator {
|
||||
return &searchTracksIterator{
|
||||
searchIterBase: searchIterBase{
|
||||
s: l.s.Server,
|
||||
query: query,
|
||||
},
|
||||
trackIDset: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
type allTracksIterator struct {
|
||||
l *LibraryManager
|
||||
albumIter AlbumIterator
|
||||
curAlbum *subsonic.AlbumID3
|
||||
curTrackIdx int
|
||||
done bool
|
||||
}
|
||||
|
||||
func (a *allTracksIterator) Next() *subsonic.Child {
|
||||
if a.done {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetch next album
|
||||
if a.curAlbum == nil || a.curTrackIdx >= len(a.curAlbum.Song) {
|
||||
al := a.albumIter.Next()
|
||||
if al == nil {
|
||||
a.done = true
|
||||
return nil
|
||||
}
|
||||
al, err := a.l.s.Server.GetAlbum(al.ID)
|
||||
if err != nil {
|
||||
log.Printf("error fetching album: %s", err.Error())
|
||||
}
|
||||
if len(al.Song) == 0 {
|
||||
// in the unlikely case of an album with zero tracks,
|
||||
// just call recursively to move to next album
|
||||
return a.Next()
|
||||
}
|
||||
a.curAlbum = al
|
||||
a.curTrackIdx = 0
|
||||
}
|
||||
|
||||
tr := a.curAlbum.Song[a.curTrackIdx]
|
||||
a.curTrackIdx += 1
|
||||
return tr
|
||||
}
|
||||
|
||||
type searchTracksIterator struct {
|
||||
searchIterBase
|
||||
|
||||
prefetched []*subsonic.Child
|
||||
prefetchedPos int
|
||||
trackIDset map[string]bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func (s *searchTracksIterator) Next() *subsonic.Child {
|
||||
if s.done {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prefetch more search results from server
|
||||
if len(s.prefetched) == 0 {
|
||||
results := s.searchIterBase.fetchResults()
|
||||
|
||||
if results != nil {
|
||||
// add results from songs search
|
||||
s.addNewTracks(results.Song)
|
||||
s.songOffset += len(results.Song)
|
||||
|
||||
// add results from artists search
|
||||
for _, artist := range results.Artist {
|
||||
artist, err := s.s.GetArtist(artist.ID)
|
||||
if err != nil {
|
||||
log.Printf("error fetching artist: %s", err.Error())
|
||||
} else {
|
||||
s.addNewTracksFromAlbums(artist.Album)
|
||||
}
|
||||
}
|
||||
s.artistOffset += len(results.Artist)
|
||||
|
||||
// add results from albums search
|
||||
s.addNewTracksFromAlbums(results.Album)
|
||||
s.albumOffset += len(results.Album)
|
||||
}
|
||||
}
|
||||
|
||||
// return from prefetched results
|
||||
if len(s.prefetched) > 0 {
|
||||
tr := s.prefetched[s.prefetchedPos]
|
||||
s.prefetchedPos++
|
||||
if s.prefetchedPos == len(s.prefetched) {
|
||||
s.prefetched = s.prefetched[:0]
|
||||
s.prefetchedPos = 0
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// no more results
|
||||
s.done = true
|
||||
s.prefetched = nil
|
||||
s.trackIDset = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *searchTracksIterator) addNewTracks(tracks []*subsonic.Child) {
|
||||
for _, tr := range tracks {
|
||||
if _, have := s.trackIDset[tr.ID]; have {
|
||||
continue
|
||||
}
|
||||
s.prefetched = append(s.prefetched, tr)
|
||||
s.trackIDset[tr.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *searchTracksIterator) addNewTracksFromAlbums(albums []*subsonic.AlbumID3) {
|
||||
for _, al := range albums {
|
||||
if album, err := s.s.GetAlbum(al.ID); err != nil {
|
||||
log.Printf("error fetching album: %s", err.Error())
|
||||
} else {
|
||||
s.addNewTracks(album.Song)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user