begin the refactoring - doesnt compile

This commit is contained in:
Drew Weymouth
2023-05-14 19:31:04 -07:00
parent 0ebbf621b1
commit 85042fcbd4
23 changed files with 183 additions and 747 deletions
-397
View File
@@ -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
}
+4 -2
View File
@@ -18,6 +18,8 @@ import (
const CachedImageValidTime = 24 * time.Hour const CachedImageValidTime = 24 * time.Hour
const coverArtThumbnailSize = 300
type ImageManager struct { type ImageManager struct {
s *ServerManager s *ServerManager
baseCacheDir string baseCacheDir string
@@ -72,7 +74,7 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error)
if i.cachedFullSizeCoverID == coverID { if i.cachedFullSizeCoverID == coverID {
return i.cachedFullSizeCover, nil return i.cachedFullSizeCover, nil
} }
im, err := i.s.Server.GetCoverArt(coverID, nil) im, err := i.s.Server.GetCoverArt(coverID, 0)
if err != nil { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err return nil, err
} }
-14
View File
@@ -23,17 +23,3 @@ func NewLibraryManager(s *ServerManager) *LibraryManager {
s: s, 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
}
+8 -8
View File
@@ -26,9 +26,9 @@ type RatingFavoriteParameters struct {
} }
type Favorites struct { type Favorites struct {
Albums []Album Albums []*Album
Artists []Artist Artists []*Artist
Tracks []Track Tracks []*Track
} }
type MediaProvider interface { type MediaProvider interface {
@@ -48,19 +48,19 @@ type MediaProvider interface {
IterateTracks(searchQuery string) TrackIterator 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) GetFavorites() (Favorites, error)
GetStreamURL(trackID string) (string, error) GetStreamURL(trackID string) (string, error)
SetFavorite(params RatingFavoriteParameters) error SetFavorite(params RatingFavoriteParameters, favorite bool) error
SetRating(params RatingFavoriteParameters, rating int) error SetRating(params RatingFavoriteParameters, rating int) error
+7 -4
View File
@@ -15,7 +15,7 @@ type Album struct {
type AlbumWithTracks struct { type AlbumWithTracks struct {
Album Album
Tracks []Track Tracks []*Track
} }
type Artist struct { type Artist struct {
@@ -26,14 +26,14 @@ type Artist struct {
type ArtistWithAlbums struct { type ArtistWithAlbums struct {
Artist Artist
Albums []Album Albums []*Album
} }
type ArtistInfo struct { type ArtistInfo struct {
Biography string Biography string
LastFMUrl string LastFMUrl string
ImageURL string ImageURL string
SimilarArtists []Artist SimilarArtists []*Artist
} }
type Genre struct { type Genre struct {
@@ -54,11 +54,14 @@ type Track struct {
ArtistIDs []string ArtistIDs []string
ArtistNames []string ArtistNames []string
Album string Album string
AlbumID string
Year int
Rating int Rating int
Favorite bool Favorite bool
Size int64 Size int64
PlayCount int PlayCount int
FilePath string FilePath string
BitRate int
} }
type Playlist struct { type Playlist struct {
@@ -73,5 +76,5 @@ type Playlist struct {
type PlaylistWithTracks struct { type PlaylistWithTracks struct {
Playlist Playlist
Tracks []Track Tracks []*Track
} }
@@ -78,7 +78,7 @@ type baseIter struct {
serverPos int serverPos int
s *subsonic.Client s *subsonic.Client
opts map[string]string opts map[string]string
prefetched []mediaprovider.Album prefetched []*mediaprovider.Album
prefetchedPos int prefetchedPos int
done bool done bool
} }
@@ -99,7 +99,7 @@ func (r *baseIter) Next() *mediaprovider.Album {
if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) { if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) {
a := r.prefetched[r.prefetchedPos] a := r.prefetched[r.prefetchedPos]
r.prefetchedPos++ r.prefetchedPos++
return &a return a
} }
r.prefetched = nil r.prefetched = nil
for { // keep fetching until we are done or have mathcing results 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 r.prefetched[0]
return &ret
} }
type searchIter struct { type searchIter struct {
@@ -207,8 +206,7 @@ func (s *searchIter) Next() *mediaprovider.Album {
s.prefetchedPos = 0 s.prefetchedPos = 0
} }
al := toAlbum(a) return toAlbum(a)
return &al
} }
return nil return nil
@@ -323,8 +321,7 @@ func (r *randomIter) Next() *mediaprovider.Album {
r.prefetchedPos = 0 r.prefetchedPos = 0
} }
al := toAlbum(a) return toAlbum(a)
return &al
} }
return nil return nil
@@ -2,7 +2,6 @@ package subsonic
import ( import (
"image" "image"
"log"
"math" "math"
"strconv" "strconv"
"sync" "sync"
@@ -115,12 +114,12 @@ func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A
}, nil }, nil
} }
func (s *subsonicMediaProvider) GetArtists() ([]mediaprovider.Artist, error) { func (s *subsonicMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) {
idxs, err := s.client.GetArtists(map[string]string{}) idxs, err := s.client.GetArtists(map[string]string{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
var artists []mediaprovider.Artist var artists []*mediaprovider.Artist
for _, idx := range idxs.Index { for _, idx := range idxs.Index {
for _, ar := range idx.Artist { for _, ar := range idx.Artist {
artists = append(artists, toArtistFromID3(ar)) 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) { 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) { func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
@@ -145,13 +148,13 @@ func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error)
}, nil }, nil
} }
func (s *subsonicMediaProvider) GetGenres() ([]mediaprovider.Genre, error) { func (s *subsonicMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
g, err := s.client.GetGenres() g, err := s.client.GetGenres()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return sharedutil.MapSlice(g, func(g *subsonic.Genre) mediaprovider.Genre { return sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre {
return mediaprovider.Genre{ return &mediaprovider.Genre{
Name: g.Name, Name: g.Name,
AlbumCount: g.AlbumCount, AlbumCount: g.AlbumCount,
TrackCount: g.SongCount, TrackCount: g.SongCount,
@@ -178,7 +181,7 @@ func (s *subsonicMediaProvider) GetPlaylists() ([]mediaprovider.Playlist, error)
return sharedutil.MapSlice(pl, toPlaylist), nil 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)} opts := map[string]string{"size": strconv.Itoa(count)}
if genreName != "" { if genreName != "" {
opts["genre"] = genreName opts["genre"] = genreName
@@ -190,7 +193,7 @@ func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]
return sharedutil.MapSlice(tr, toTrack), nil 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)}) tr, err := s.client.GetSimilarSongs2(artistID, map[string]string{"count": strconv.Itoa(count)})
if err != nil { if err != nil {
return nil, err return nil, err
@@ -216,12 +219,16 @@ func (s *subsonicMediaProvider) Scrobble(trackID string, submission bool) error
"submission": strconv.FormatBool(submission)}) "submission": strconv.FormatBool(submission)})
} }
func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters) error { func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error {
return s.client.Star(subsonic.StarParameters{ subParams := subsonic.StarParameters{
AlbumIDs: params.AlbumIDs, AlbumIDs: params.AlbumIDs,
ArtistIDs: params.ArtistIDs, ArtistIDs: params.ArtistIDs,
SongIDs: params.TrackIDs, 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 { func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoriteParameters, rating int) error {
@@ -263,12 +270,11 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar
return err return err
} }
func toTrack(ch *subsonic.Child) mediaprovider.Track { func toTrack(ch *subsonic.Child) *mediaprovider.Track {
if ch == nil { if ch == nil {
log.Println("subsonicMediaProvider: toTrack called on nil track") return nil
return mediaprovider.Track{}
} }
return mediaprovider.Track{ return &mediaprovider.Track{
ID: ch.ID, ID: ch.ID,
CoverArtID: ch.CoverArt, CoverArtID: ch.CoverArt,
ParentID: ch.Parent, ParentID: ch.Parent,
@@ -280,19 +286,21 @@ func toTrack(ch *subsonic.Child) mediaprovider.Track {
ArtistIDs: []string{ch.ArtistID}, ArtistIDs: []string{ch.ArtistID},
ArtistNames: []string{ch.Artist}, ArtistNames: []string{ch.Artist},
Album: ch.Album, Album: ch.Album,
AlbumID: ch.AlbumID,
Year: ch.Year,
Rating: ch.UserRating, Rating: ch.UserRating,
Favorite: !ch.Starred.IsZero(), Favorite: !ch.Starred.IsZero(),
PlayCount: int(ch.PlayCount), PlayCount: int(ch.PlayCount),
FilePath: ch.Path, FilePath: ch.Path,
BitRate: ch.BitRate,
} }
} }
func toAlbum(al *subsonic.AlbumID3) mediaprovider.Album { func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album {
if al == nil { if al == nil {
log.Println("subsonicMediaProvider: toAlbum called on nil album") return nil
return mediaprovider.Album{}
} }
return mediaprovider.Album{ return &mediaprovider.Album{
ID: al.ID, ID: al.ID,
CoverArtID: al.CoverArt, CoverArtID: al.CoverArt,
Name: al.Name, 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 { if ar == nil {
log.Println("subsonicMediaProvider: toArtist called on nil artist") return nil
return mediaprovider.Artist{}
} }
return mediaprovider.Artist{ return &mediaprovider.Artist{
ID: ar.ID, ID: ar.ID,
Name: ar.Name, Name: ar.Name,
} }
} }
func toArtistFromID3(ar *subsonic.ArtistID3) mediaprovider.Artist { func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist {
if ar == nil { if ar == nil {
log.Println("subsonicMediaProvider: toArtistFromID3 called on nil artistID3") return nil
return mediaprovider.Artist{}
} }
return mediaprovider.Artist{ return &mediaprovider.Artist{
ID: ar.ID, ID: ar.ID,
Name: ar.Name, Name: ar.Name,
AlbumCount: ar.AlbumCount, AlbumCount: ar.AlbumCount,
@@ -58,7 +58,7 @@ func (a *allTracksIterator) Next() *mediaprovider.Track {
tr := a.curAlbum.Tracks[a.curTrackIdx] tr := a.curAlbum.Tracks[a.curTrackIdx]
a.curTrackIdx += 1 a.curTrackIdx += 1
return &tr return tr
} }
type searchTracksIterator struct { type searchTracksIterator struct {
@@ -109,8 +109,7 @@ func (s *searchTracksIterator) Next() *mediaprovider.Track {
s.prefetched = s.prefetched[:0] s.prefetched = s.prefetched[:0]
s.prefetchedPos = 0 s.prefetchedPos = 0
} }
track := toTrack(tr) return toTrack(tr)
return &track
} }
// no more results // no more results
+21 -35
View File
@@ -3,14 +3,12 @@ package backend
import ( import (
"context" "context"
"log" "log"
"strconv"
"time" "time"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/util"
"github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/go-subsonic/subsonic"
) )
const ( const (
@@ -33,14 +31,14 @@ type PlaybackManager struct {
curTrackTime float64 curTrackTime float64
callbacksDisabled bool callbacksDisabled bool
playQueue []*subsonic.Child playQueue []*mediaprovider.Track
nowPlayingIdx int64 nowPlayingIdx int64
// to pass to onSongChange listeners; clear once listeners have been called // to pass to onSongChange listeners; clear once listeners have been called
lastScrobbled *subsonic.Child lastScrobbled *mediaprovider.Track
scrobbleCfg *ScrobbleConfig scrobbleCfg *ScrobbleConfig
onSongChange []func(nowPlaying *subsonic.Child, justScrobbledIfAny *subsonic.Child) onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track)
onPlayTimeUpdate []func(float64, float64) onPlayTimeUpdate []func(float64, float64)
} }
@@ -111,7 +109,7 @@ func (p *PlaybackManager) DisableCallbacks() {
} }
// Gets the curently playing song, if any. // 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 { if len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped {
return nil return nil
} }
@@ -119,7 +117,7 @@ func (p *PlaybackManager) NowPlaying() *subsonic.Child {
} }
// Sets a callback that is notified whenever a new song begins playing. // 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) p.onSongChange = append(p.onSongChange, cb)
} }
@@ -134,7 +132,7 @@ func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle
if err != nil { if err != nil {
return err return err
} }
return p.LoadTracks(album.Song, appendToQueue, shuffle) return p.LoadTracks(album.Tracks, appendToQueue, shuffle)
} }
// Loads the specified playlist into the play queue. // Loads the specified playlist into the play queue.
@@ -143,10 +141,10 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool, sh
if err != nil { if err != nil {
return err 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 { if !appendToQueue {
p.player.Stop() p.player.Stop()
p.nowPlayingIdx = 0 p.nowPlayingIdx = 0
@@ -157,11 +155,11 @@ func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue, sh
util.ShuffleSlice(nums) util.ShuffleSlice(nums)
} }
for _, i := range 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 { if err != nil {
return err 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 // 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 // (tracking play count increases, favorite, and rating) without messing up
// other views' track models // other views' track models
@@ -200,11 +198,7 @@ func (p *PlaybackManager) PlayTrackAt(idx int) error {
} }
func (p *PlaybackManager) PlayRandomSongs(genreName string) { func (p *PlaybackManager) PlayRandomSongs(genreName string) {
params := map[string]string{"size": "100"} if songs, err := p.sm.Server.GetRandomTracks(genreName, 100); err != nil {
if genreName != "" {
params["genre"] = genreName
}
if songs, err := p.sm.Server.GetRandomSongs(params); err != nil {
log.Printf("error getting random songs: %s", err.Error()) log.Printf("error getting random songs: %s", err.Error())
} else { } else {
p.LoadTracks(songs, false, false) p.LoadTracks(songs, false, false)
@@ -213,8 +207,7 @@ func (p *PlaybackManager) PlayRandomSongs(genreName string) {
} }
func (p *PlaybackManager) PlaySimilarSongs(id string) { func (p *PlaybackManager) PlaySimilarSongs(id string) {
params := map[string]string{"size": "100"} if songs, err := p.sm.Server.GetSimilarTracks(id, 100); err != nil {
if songs, err := p.sm.Server.GetSimilarSongs2(id, params); err != nil {
log.Printf("error getting similar songs: %s", err.Error()) log.Printf("error getting similar songs: %s", err.Error())
} else { } else {
p.LoadTracks(songs, false, false) p.LoadTracks(songs, false, false)
@@ -222,8 +215,8 @@ func (p *PlaybackManager) PlaySimilarSongs(id string) {
} }
} }
func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child { func (p *PlaybackManager) GetPlayQueue() []*mediaprovider.Track {
pq := make([]*subsonic.Child, len(p.playQueue)) pq := make([]*mediaprovider.Track, len(p.playQueue))
for i, tr := range p.playQueue { for i, tr := range p.playQueue {
copy := *tr copy := *tr
pq[i] = &copy pq[i] = &copy
@@ -235,11 +228,7 @@ func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child {
// this should be called to ensure the in-memory track model is updated. // this should be called to ensure the in-memory track model is updated.
func (p *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) { func (p *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) {
if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil { if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil {
if fav { tr.Favorite = fav
tr.Starred = time.Now()
} else {
tr.Starred = time.Time{}
}
} }
} }
@@ -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. // this should be called to ensure the in-memory track model is updated.
func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) { func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) {
if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil { if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil {
tr.UserRating = rating tr.Rating = rating
} }
} }
// trackIdxs must be sorted // trackIdxs must be sorted
func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) { 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 rmCount := 0
rmIdx := 0 rmIdx := 0
for i, tr := range p.playQueue { for i, tr := range p.playQueue {
@@ -310,10 +299,10 @@ func (p *PlaybackManager) checkScrobble(playDur time.Duration) {
playDur.Seconds() >= float64(p.scrobbleCfg.ThresholdTimeSeconds) playDur.Seconds() >= float64(p.scrobbleCfg.ThresholdTimeSeconds)
if timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent) { if timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent) {
song := p.playQueue[p.nowPlayingIdx] song := p.playQueue[p.nowPlayingIdx]
log.Printf("Scrobbling %q", song.Title) log.Printf("Scrobbling %q", song.Name)
song.PlayCount += 1 song.PlayCount += 1
p.lastScrobbled = song 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 return
} }
song := p.playQueue[p.nowPlayingIdx] song := p.playQueue[p.nowPlayingIdx]
go p.sm.Server.Scrobble(song.ID, map[string]string{ go p.sm.Server.Scrobble(song.ID, false)
"time": strconv.FormatInt(time.Now().Unix()*1000, 10),
"submission": "false",
})
} }
func (p *PlaybackManager) invokeOnSongChangeCallbacks() { func (p *PlaybackManager) invokeOnSongChangeCallbacks() {
+7 -2
View File
@@ -7,13 +7,16 @@ import (
"time" "time"
"github.com/dweymouth/go-subsonic/subsonic" "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/google/uuid"
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
) )
type ServerManager struct { type ServerManager struct {
LoggedInUser string
ServerID uuid.UUID ServerID uuid.UUID
Server *subsonic.Client Server mediaprovider.MediaProvider
appName string appName string
onServerConnected []func() onServerConnected []func()
@@ -31,7 +34,8 @@ func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) err
if err != nil { if err != nil {
return err return err
} }
s.Server = cli s.Server = subsonicMP.SubsonicMediaProvider(cli)
s.LoggedInUser = conf.Username
s.ServerID = conf.ID s.ServerID = conf.ID
for _, cb := range s.onServerConnected { for _, cb := range s.onServerConnected {
cb() cb()
@@ -116,6 +120,7 @@ func (s *ServerManager) Logout() {
cb() cb()
} }
s.Server = nil s.Server = nil
s.LoggedInUser = ""
s.ServerID = uuid.UUID{} s.ServerID = uuid.UUID{}
} }
} }
-140
View File
@@ -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)
}
}
}
+7 -7
View File
@@ -4,7 +4,7 @@ import (
"math" "math"
"sort" "sort"
"github.com/dweymouth/go-subsonic/subsonic" "github.com/dweymouth/supersonic/backend/mediaprovider"
) )
func SliceContains[T comparable](ts []T, t T) bool { func SliceContains[T comparable](ts []T, t T) bool {
@@ -40,7 +40,7 @@ func MapSlice[T any, U any](ts []T, f func(T) U) []U {
return result return result
} }
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child { func FindTrackByID(id string, tracks []*mediaprovider.Track) *mediaprovider.Track {
for _, tr := range tracks { for _, tr := range tracks {
if id == tr.ID { if id == tr.ID {
return tr return tr
@@ -49,15 +49,15 @@ func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
return nil return nil
} }
func TrackIDOrEmptyStr(track *subsonic.Child) string { func TrackIDOrEmptyStr(track *mediaprovider.Track) string {
if track == nil { if track == nil {
return "" return ""
} }
return track.ID return track.ID
} }
func TracksToIDs(tracks []*subsonic.Child) []string { func TracksToIDs(tracks []*mediaprovider.Track) []string {
return MapSlice(tracks, func(tr *subsonic.Child) string { return MapSlice(tracks, func(tr *mediaprovider.Track) string {
return tr.ID return tr.ID
}) })
} }
@@ -73,8 +73,8 @@ const (
// Reorder tracks and return a new track slice. // Reorder tracks and return a new track slice.
// idxToMove must contain only valid indexes into tracks, and no repeats // idxToMove must contain only valid indexes into tracks, and no repeats
func ReorderTracks(tracks []*subsonic.Child, idxToMove []int, op TrackReorderOp) []*subsonic.Child { func ReorderTracks(tracks []*mediaprovider.Track, idxToMove []int, op TrackReorderOp) []*mediaprovider.Track {
newTracks := make([]*subsonic.Child, len(tracks)) newTracks := make([]*mediaprovider.Track, len(tracks))
switch op { switch op {
case MoveToTop: case MoveToTop:
topIdx := 0 topIdx := 0
+7 -7
View File
@@ -3,11 +3,11 @@ package sharedutil
import ( import (
"testing" "testing"
"github.com/dweymouth/go-subsonic/subsonic" "github.com/dweymouth/supersonic/backend/mediaprovider"
) )
func Test_ReorderTracks(t *testing.T) { func Test_ReorderTracks(t *testing.T) {
tracks := []*subsonic.Child{ tracks := []*mediaprovider.Track{
{ID: "a"}, // 0 {ID: "a"}, // 0
{ID: "b"}, // 1 {ID: "b"}, // 1
{ID: "c"}, // 2 {ID: "c"}, // 2
@@ -18,7 +18,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveToTop: // test MoveToTop:
idxToMove := []int{0, 2, 3, 5} idxToMove := []int{0, 2, 3, 5}
want := []*subsonic.Child{ want := []*mediaprovider.Track{
{ID: "a"}, {ID: "a"},
{ID: "c"}, {ID: "c"},
{ID: "d"}, {ID: "d"},
@@ -33,7 +33,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveToBottom: // test MoveToBottom:
idxToMove = []int{0, 2, 5} idxToMove = []int{0, 2, 5}
want = []*subsonic.Child{ want = []*mediaprovider.Track{
{ID: "b"}, {ID: "b"},
{ID: "d"}, {ID: "d"},
{ID: "e"}, {ID: "e"},
@@ -48,7 +48,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveUp: // test MoveUp:
idxToMove = []int{0, 1, 3, 5} idxToMove = []int{0, 1, 3, 5}
want = []*subsonic.Child{ want = []*mediaprovider.Track{
{ID: "a"}, {ID: "a"},
{ID: "b"}, {ID: "b"},
{ID: "d"}, {ID: "d"},
@@ -63,7 +63,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveDown: // test MoveDown:
idxToMove = []int{2, 4, 5} idxToMove = []int{2, 4, 5}
want = []*subsonic.Child{ want = []*mediaprovider.Track{
{ID: "a"}, {ID: "a"},
{ID: "b"}, {ID: "b"},
{ID: "d"}, {ID: "d"},
@@ -77,7 +77,7 @@ func Test_ReorderTracks(t *testing.T) {
} }
} }
func tracklistsEqual(t *testing.T, a, b []*subsonic.Child) bool { func tracklistsEqual(t *testing.T, a, b []*mediaprovider.Track) bool {
t.Helper() t.Helper()
if len(a) != len(b) { if len(a) != len(b) {
return false return false
+6 -6
View File
@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
@@ -15,7 +16,6 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
) )
type BottomPanel struct { type BottomPanel struct {
@@ -69,7 +69,7 @@ func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel
contr.NavigateTo(controller.AlbumRoute(bp.playbackManager.NowPlaying().AlbumID)) contr.NavigateTo(controller.AlbumRoute(bp.playbackManager.NowPlaying().AlbumID))
}) })
bp.NowPlaying.OnArtistNameTapped(func() { bp.NowPlaying.OnArtistNameTapped(func() {
contr.NavigateTo(controller.ArtistRoute(bp.playbackManager.NowPlaying().ArtistID)) contr.NavigateTo(controller.ArtistRoute(bp.playbackManager.NowPlaying().ArtistIDs[0]))
}) })
bp.NowPlaying.OnTrackNameTapped(func() { bp.NowPlaying.OnTrackNameTapped(func() {
contr.NavigateTo(controller.NowPlayingRoute(bp.playbackManager.NowPlaying().ID)) contr.NavigateTo(controller.NowPlayingRoute(bp.playbackManager.NowPlaying().ID))
@@ -108,11 +108,11 @@ func (bp *BottomPanel) SetPlaybackManager(pm *backend.PlaybackManager) {
}) })
} }
func (bp *BottomPanel) onSongChange(song *subsonic.Child, _ *subsonic.Child) { func (bp *BottomPanel) onSongChange(song, _ *mediaprovider.Track) {
if song == nil { if song == nil {
bp.NowPlaying.Update("", "", false, "", nil) bp.NowPlaying.Update("", "", false, "", nil)
} else { } else {
bp.coverArtID = song.CoverArt bp.coverArtID = song.CoverArtID
var im image.Image var im image.Image
if bp.ImageManager != nil { if bp.ImageManager != nil {
// set image to expire not long after the length of the song // set image to expire not long after the length of the song
@@ -120,9 +120,9 @@ func (bp *BottomPanel) onSongChange(song *subsonic.Child, _ *subsonic.Child) {
// be in cache for the next song if it's from the same album, or // be in cache for the next song if it's from the same album, or
// if the user navigates to the album page for the track // if the user navigates to the album page for the track
imgTTLSec := song.Duration + 30 imgTTLSec := song.Duration + 30
im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArt, time.Duration(imgTTLSec)*time.Second) im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArtID, time.Duration(imgTTLSec)*time.Second)
} }
bp.NowPlaying.Update(song.Title, song.Artist, song.ArtistID != "", song.Album, im) bp.NowPlaying.Update(song.Name, song.ArtistNames[0], song.ArtistIDs[0] != "", song.Album, im)
} }
} }
+21 -25
View File
@@ -5,6 +5,7 @@ import (
"log" "log"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
@@ -17,8 +18,6 @@ import (
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
) )
type AlbumPage struct { type AlbumPage struct {
@@ -92,11 +91,11 @@ func (a *AlbumPage) Route() controller.Route {
return controller.AlbumRoute(a.albumID) return controller.AlbumRoute(a.albumID)
} }
func (a *AlbumPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) { func (a *AlbumPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track) {
if song == nil { if track == nil {
a.nowPlayingID = "" a.nowPlayingID = ""
} else { } else {
a.nowPlayingID = song.ID a.nowPlayingID = track.ID
} }
a.tracklist.SetNowPlaying(a.nowPlayingID) a.tracklist.SetNowPlaying(a.nowPlayingID)
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny)) a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
@@ -116,14 +115,14 @@ func (a *AlbumPage) SelectAll() {
// should be called asynchronously // should be called asynchronously
func (a *AlbumPage) load() { func (a *AlbumPage) load() {
album, err := a.lm.GetAlbum(a.albumID) album, err := a.sm.Server.GetAlbum(a.albumID)
if err != nil { if err != nil {
log.Printf("Failed to get album: %s", err.Error()) log.Printf("Failed to get album: %s", err.Error())
return return
} }
a.header.Update(album, a.im) a.header.Update(album, a.im)
a.tracklist.ShowDiscNumber = album.Song[0].DiscNumber != album.Song[len(album.Song)-1].DiscNumber a.tracklist.ShowDiscNumber = album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber
a.tracklist.Tracks = album.Song a.tracklist.Tracks = album.Tracks
a.tracklist.SetNowPlaying(a.nowPlayingID) a.tracklist.SetNowPlaying(a.nowPlayingID)
} }
@@ -215,20 +214,20 @@ func (a *AlbumPageHeader) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container) return widget.NewSimpleRenderer(a.container)
} }
func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageManager) { func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backend.ImageManager) {
a.albumID = album.ID a.albumID = album.ID
a.coverID = album.CoverArt a.coverID = album.CoverArtID
a.artistID = album.ArtistID a.artistID = album.ArtistIDs[0]
a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name
a.artistLabel.SetText(album.Artist) a.artistLabel.SetText(album.ArtistNames[0])
a.genre = album.Genre a.genre = album.Genres[0]
a.genreLabel.SetText(album.Genre) a.genreLabel.SetText(album.Genres[0])
a.miscLabel.SetText(formatMiscLabelStr(album)) a.miscLabel.SetText(formatMiscLabelStr(album))
a.toggleFavButton.IsFavorited = !album.Starred.IsZero() a.toggleFavButton.IsFavorited = album.Favorite
a.Refresh() a.Refresh()
go func() { go func() {
if cover, err := im.GetCoverThumbnail(album.CoverArt); err == nil { if cover, err := im.GetCoverThumbnail(album.CoverArtID); err == nil {
a.cover.Image.Image = cover a.cover.Image.Image = cover
a.cover.Refresh() a.cover.Refresh()
} else { } else {
@@ -238,11 +237,8 @@ func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageMana
} }
func (a *AlbumPageHeader) toggleFavorited() { func (a *AlbumPageHeader) toggleFavorited() {
if a.toggleFavButton.IsFavorited { params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}}
a.page.sm.Server.Star(subsonic.StarParameters{AlbumIDs: []string{a.albumID}}) a.page.sm.Server.SetFavorite(params, a.toggleFavButton.IsFavorited)
} else {
a.page.sm.Server.Unstar(subsonic.StarParameters{AlbumIDs: []string{a.albumID}})
}
} }
func (a *AlbumPageHeader) showPopUpCover() { func (a *AlbumPageHeader) showPopUpCover() {
@@ -254,16 +250,16 @@ func (a *AlbumPageHeader) showPopUpCover() {
a.page.contr.ShowPopUpImage(cover) a.page.contr.ShowPopUpImage(cover)
} }
func formatMiscLabelStr(a *subsonic.AlbumID3) string { func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string {
var discs string var discs string
if discCount := a.Song[len(a.Song)-1].DiscNumber; discCount > 1 { if discCount := a.Tracks[len(a.Tracks)-1].DiscNumber; discCount > 1 {
discs = fmt.Sprintf("%d discs · ", discCount) discs = fmt.Sprintf("%d discs · ", discCount)
} }
tracks := "tracks" tracks := "tracks"
if a.SongCount == 1 { if a.TrackCount == 1 {
tracks = "track" tracks = "track"
} }
return fmt.Sprintf("%d · %d %s · %s%s", a.Year, a.SongCount, tracks, discs, util.SecondsToTimeString(float64(a.Duration))) return fmt.Sprintf("%d · %d %s · %s%s", a.Year, a.TrackCount, tracks, discs, util.SecondsToTimeString(float64(a.Duration)))
} }
func (s *albumPageState) Restore() Page { func (s *albumPageState) Restore() Page {
+2 -1
View File
@@ -2,6 +2,7 @@ package browsing
import ( import (
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
@@ -29,7 +30,7 @@ type AlbumsPage struct {
searcher *widgets.SearchEntry searcher *widgets.SearchEntry
filterBtn *widgets.AlbumFilterButton filterBtn *widgets.AlbumFilterButton
searchText string searchText string
filter backend.AlbumFilter filter mediaprovider.AlbumFilter
titleDisp *widget.RichText titleDisp *widget.RichText
sortOrder *selectWidget sortOrder *selectWidget
container *fyne.Container container *fyne.Container
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
@@ -118,7 +119,7 @@ func (a *ArtistPage) Save() SavedPage {
var _ CanShowNowPlaying = (*ArtistPage)(nil) var _ CanShowNowPlaying = (*ArtistPage)(nil)
func (a *ArtistPage) OnSongChange(track *subsonic.Child, lastScrobbledIfAny *subsonic.Child) { func (a *ArtistPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track) {
a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(track) a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(track)
if a.tracklistCtr != nil { if a.tracklistCtr != nil {
tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist)
+3 -4
View File
@@ -2,6 +2,7 @@ package browsing
import ( import (
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme" myTheme "github.com/dweymouth/supersonic/ui/theme"
@@ -11,8 +12,6 @@ import (
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
) )
type Page interface { type Page interface {
@@ -37,7 +36,7 @@ type CanSelectAll interface {
} }
type CanShowNowPlaying interface { type CanShowNowPlaying interface {
OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track)
} }
type BrowsingPane struct { type BrowsingPane struct {
@@ -160,7 +159,7 @@ func (b *BrowsingPane) doSetPage(p Page) bool {
return true return true
} }
func (b *BrowsingPane) onSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) { func (b *BrowsingPane) onSongChange(song, lastScrobbledIfAny *mediaprovider.Track) {
if b.curPage == nil { if b.curPage == nil {
return return
} }
+8 -9
View File
@@ -4,6 +4,7 @@ import (
"log" "log"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme" myTheme "github.com/dweymouth/supersonic/ui/theme"
@@ -15,8 +16,6 @@ import (
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
) )
type FavoritesPage struct { type FavoritesPage struct {
@@ -216,7 +215,7 @@ func (a *FavoritesPage) OnSearched(query string) {
var _ CanShowNowPlaying = (*FavoritesPage)(nil) var _ CanShowNowPlaying = (*FavoritesPage)(nil)
func (a *FavoritesPage) OnSongChange(song *subsonic.Child, _ *subsonic.Child) { func (a *FavoritesPage) OnSongChange(song, _ *mediaprovider.Track) {
a.nowPlayingID = "" a.nowPlayingID = ""
if song != nil { if song != nil {
a.nowPlayingID = song.ID a.nowPlayingID = song.ID
@@ -271,12 +270,12 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
a.createContainer(layout.NewSpacer()) a.createContainer(layout.NewSpacer())
} }
go func() { go func() {
s, err := a.sm.Server.GetStarred2(nil) fav, err := a.sm.Server.GetFavorites()
if err != nil { if err != nil {
log.Printf("error getting starred items: %s", err.Error()) log.Printf("error getting starred items: %s", err.Error())
return return
} }
model := buildArtistListModel(s.Artist) model := buildArtistListModel(fav.Artists)
artistList := widgets.NewArtistGenreList(model) artistList := widgets.NewArtistGenreList(model)
artistList.ShowAlbumCount = true artistList.ShowAlbumCount = true
artistList.OnNavTo = func(artistID string) { artistList.OnNavTo = func(artistID string) {
@@ -295,7 +294,7 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
} }
} }
func buildArtistListModel(artists []*subsonic.ArtistID3) []widgets.ArtistGenreListItemModel { func buildArtistListModel(artists []*mediaprovider.Artist) []widgets.ArtistGenreListItemModel {
model := make([]widgets.ArtistGenreListItemModel, 0) model := make([]widgets.ArtistGenreListItemModel, 0)
for _, ar := range artists { for _, ar := range artists {
model = append(model, widgets.ArtistGenreListItemModel{ model = append(model, widgets.ArtistGenreListItemModel{
@@ -320,12 +319,12 @@ func (a *FavoritesPage) onShowFavoriteSongs() {
a.createContainer(layout.NewSpacer()) a.createContainer(layout.NewSpacer())
} }
go func() { go func() {
s, err := a.sm.Server.GetStarred2(nil) fav, err := a.sm.Server.GetFavorites()
if err != nil { if err != nil {
log.Printf("error getting starred items: %s", err.Error()) log.Printf("error getting starred items: %s", err.Error())
return return
} }
tracklist := widgets.NewTracklist(s.Song) tracklist := widgets.NewTracklist(fav.Tracks)
tracklist.AutoNumber = true tracklist.AutoNumber = true
tracklist.SetVisibleColumns(a.cfg.TracklistColumns) tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.OnVisibleColumnsChanged = func(cols []string) { tracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -360,7 +359,7 @@ type savedFavoritesPage struct {
lm *backend.LibraryManager lm *backend.LibraryManager
gridState widgets.GridViewState gridState widgets.GridViewState
searchGridState widgets.GridViewState searchGridState widgets.GridViewState
filter backend.AlbumFilter filter mediaprovider.AlbumFilter
searchText string searchText string
activeToggleBtn int activeToggleBtn int
} }
+3 -3
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/ui/browsing" "github.com/dweymouth/supersonic/ui/browsing"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
@@ -15,7 +16,6 @@ import (
"fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
) )
var ( var (
@@ -81,12 +81,12 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane)
m.Window.SetContent(m.container) m.Window.SetContent(m.container)
m.Window.Resize(size) m.Window.Resize(size)
app.PlaybackManager.OnSongChange(func(song *subsonic.Child, _ *subsonic.Child) { app.PlaybackManager.OnSongChange(func(song, _ *mediaprovider.Track) {
if song == nil { if song == nil {
m.Window.SetTitle(appName) m.Window.SetTitle(appName)
return return
} }
m.Window.SetTitle(fmt.Sprintf("%s %s · %s", song.Title, song.Artist, appName)) m.Window.SetTitle(fmt.Sprintf("%s %s · %s", song.Name, song.ArtistNames[0], appName))
}) })
app.ServerManager.OnServerConnected(func() { app.ServerManager.OnServerConnected(func() {
m.BrowsingPane.EnableNavigationButtons() m.BrowsingPane.EnableNavigationButtons()
+3 -3
View File
@@ -9,7 +9,7 @@ import (
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
) )
@@ -21,11 +21,11 @@ type AlbumFilterButton struct {
GenreDisabled bool GenreDisabled bool
FavoriteDisabled bool FavoriteDisabled bool
filter *backend.AlbumFilter filter *mediaprovider.AlbumFilter
dialog *widget.PopUp dialog *widget.PopUp
} }
func NewAlbumFilterButton(filter *backend.AlbumFilter) *AlbumFilterButton { func NewAlbumFilterButton(filter *mediaprovider.AlbumFilter) *AlbumFilterButton {
a := &AlbumFilterButton{ a := &AlbumFilterButton{
filter: filter, filter: filter,
Button: widget.Button{ Button: widget.Button{
+27 -32
View File
@@ -5,8 +5,8 @@ import (
"log" "log"
"strconv" "strconv"
"sync" "sync"
"time"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/supersonic/ui/os" "github.com/dweymouth/supersonic/ui/os"
@@ -18,7 +18,6 @@ import (
"fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
) )
const ( const (
@@ -40,7 +39,7 @@ type Tracklist struct {
// Tracks is the set of tracks displayed by the widget. // Tracks is the set of tracks displayed by the widget.
// Direct access to this is not thread-safe but OK for // Direct access to this is not thread-safe but OK for
// views that only load tracks into the widget once at page load. // views that only load tracks into the widget once at page load.
Tracks []*subsonic.Child Tracks []*mediaprovider.Track
// AutoNumber sets whether to auto-number the tracks 1..N in display order, // AutoNumber sets whether to auto-number the tracks 1..N in display order,
// or to use the number from the track's metadata // or to use the number from the track's metadata
@@ -60,8 +59,8 @@ type Tracklist struct {
// user action callbacks // user action callbacks
OnPlayTrackAt func(int) OnPlayTrackAt func(int)
OnPlaySelection func(tracks []*subsonic.Child, shuffle bool) OnPlaySelection func(tracks []*mediaprovider.Track, shuffle bool)
OnAddToQueue func(trackIDs []*subsonic.Child) OnAddToQueue func(trackIDs []*mediaprovider.Track)
OnAddToPlaylist func(trackIDs []string) OnAddToPlaylist func(trackIDs []string)
OnSetFavorite func(trackIDs []string, fav bool) OnSetFavorite func(trackIDs []string, fav bool)
OnSetRating func(trackIDs []string, rating int) OnSetRating func(trackIDs []string, rating int)
@@ -85,7 +84,7 @@ type Tracklist struct {
container *fyne.Container container *fyne.Container
} }
func NewTracklist(tracks []*subsonic.Child) *Tracklist { func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
t := &Tracklist{Tracks: tracks, visibleColumns: make([]bool, 12)} t := &Tracklist{Tracks: tracks, visibleColumns: make([]bool, 12)}
t.ExtendBaseWidget(t) t.ExtendBaseWidget(t)
@@ -144,7 +143,7 @@ func (t *Tracklist) buildHeader() {
} }
// Gets the track at the given index. Thread-safe. // Gets the track at the given index. Thread-safe.
func (t *Tracklist) TrackAt(idx int) *subsonic.Child { func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()
if idx >= len(t.Tracks) { if idx >= len(t.Tracks) {
@@ -216,7 +215,7 @@ func (t *Tracklist) Clear() {
} }
// Append more tracks to the tracklist. Thread-safe. // Append more tracks to the tracklist. Thread-safe.
func (t *Tracklist) AppendTracks(trs []*subsonic.Child) { func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) {
t.tracksMutex.Lock() t.tracksMutex.Lock()
defer t.tracksMutex.Unlock() defer t.tracksMutex.Unlock()
t.Tracks = append(t.Tracks, trs...) t.Tracks = append(t.Tracks, trs...)
@@ -330,16 +329,12 @@ func (t *Tracklist) onSetFavorite(trackID string, fav bool) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
tr := sharedutil.FindTrackByID(trackID, t.Tracks) tr := sharedutil.FindTrackByID(trackID, t.Tracks)
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
t.onSetFavorites([]*subsonic.Child{tr}, fav, false) t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false)
} }
func (t *Tracklist) onSetFavorites(tracks []*subsonic.Child, fav bool, needRefresh bool) { func (t *Tracklist) onSetFavorites(tracks []*mediaprovider.Track, fav bool, needRefresh bool) {
for _, tr := range tracks { for _, tr := range tracks {
if fav { tr.Favorite = fav
tr.Starred = time.Now()
} else {
tr.Starred = time.Time{}
}
} }
if needRefresh { if needRefresh {
t.Refresh() t.Refresh()
@@ -355,12 +350,12 @@ func (t *Tracklist) onSetRating(trackID string, rating int) {
t.tracksMutex.RLock() t.tracksMutex.RLock()
tr := sharedutil.FindTrackByID(trackID, t.Tracks) tr := sharedutil.FindTrackByID(trackID, t.Tracks)
t.tracksMutex.RUnlock() t.tracksMutex.RUnlock()
t.onSetRatings([]*subsonic.Child{tr}, rating, false) t.onSetRatings([]*mediaprovider.Track{tr}, rating, false)
} }
func (t *Tracklist) onSetRatings(tracks []*subsonic.Child, rating int, needRefresh bool) { func (t *Tracklist) onSetRatings(tracks []*mediaprovider.Track, rating int, needRefresh bool) {
for _, tr := range tracks { for _, tr := range tracks {
tr.UserRating = rating tr.Rating = rating
} }
if needRefresh { if needRefresh {
t.Refresh() t.Refresh()
@@ -383,9 +378,9 @@ func (t *Tracklist) onAlbumTapped(albumID string) {
} }
} }
func (t *Tracklist) selectedTracks() []*subsonic.Child { func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
sel := t.selectionMgr.GetSelection() sel := t.selectionMgr.GetSelection()
tracks := make([]*subsonic.Child, 0, len(sel)) tracks := make([]*mediaprovider.Track, 0, len(sel))
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()
for _, idx := range sel { for _, idx := range sel {
@@ -484,7 +479,7 @@ type TrackRow struct {
albumID string albumID string
isPlaying bool isPlaying bool
isFavorite bool isFavorite bool
playCount int64 playCount int
num *widget.RichText num *widget.RichText
name *widget.RichText name *widget.RichText
@@ -543,37 +538,37 @@ func newTrailingAlignRichText() *widget.RichText {
return rt return rt
} }
func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) { func (t *TrackRow) Update(tr *mediaprovider.Track, rowNum int) {
// Update info that can change if this row is bound to // Update info that can change if this row is bound to
// a new track (*subsonic.Child) // a new track (*mediaprovider.Track)
if tr.ID != t.trackID { if tr.ID != t.trackID {
if t.Focused { if t.Focused {
fyne.CurrentApp().Driver().CanvasForObject(t).Focus(nil) fyne.CurrentApp().Driver().CanvasForObject(t).Focus(nil)
t.Focused = false t.Focused = false
} }
t.trackID = tr.ID t.trackID = tr.ID
t.artistID = tr.ArtistID t.artistID = tr.ArtistIDs[0]
t.albumID = tr.AlbumID t.albumID = tr.AlbumID
t.name.Segments[0].(*widget.TextSegment).Text = tr.Title t.name.Segments[0].(*widget.TextSegment).Text = tr.Name
t.artist.SetText(tr.Artist) t.artist.SetText(tr.ArtistNames[0])
t.artist.Disabled = tr.ArtistID == "" t.artist.Disabled = tr.ArtistIDs[0] == ""
t.album.SetText(tr.Album) t.album.SetText(tr.Album)
t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration)) t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration))
t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year) t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year)
t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount)) t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount))
t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate) t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate)
t.size.Segments[0].(*widget.TextSegment).Text = util.BytesToSizeString(tr.Size) t.size.Segments[0].(*widget.TextSegment).Text = util.BytesToSizeString(tr.Size)
t.path.Segments[0].(*widget.TextSegment).Text = tr.Path t.path.Segments[0].(*widget.TextSegment).Text = tr.FilePath
} }
// Update track num if needed // Update track num if needed
// (which can change based on bound *subsonic.Child or tracklist.AutoNumber) // (which can change based on bound *mediaprovider.Track or tracklist.AutoNumber)
if t.trackNum != rowNum { if t.trackNum != rowNum {
discNum := -1 discNum := -1
var str string var str string
if rowNum < 0 { if rowNum < 0 {
rowNum = tr.Track rowNum = tr.TrackNumber
if t.tracklist.ShowDiscNumber { if t.tracklist.ShowDiscNumber {
discNum = tr.DiscNumber discNum = tr.DiscNumber
} }
@@ -612,7 +607,7 @@ func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) {
} }
// Render favorite column // Render favorite column
if tr.Starred.IsZero() { if tr.Favorite {
t.isFavorite = false t.isFavorite = false
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon
} else { } else {
@@ -620,7 +615,7 @@ func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) {
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon
} }
t.rating.Rating = tr.UserRating t.rating.Rating = tr.Rating
// Show only columns configured to be visible // Show only columns configured to be visible
t.artist.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnArtist)] t.artist.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnArtist)]
+5 -7
View File
@@ -1,25 +1,23 @@
package widgets package widgets
import ( import (
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/go-subsonic/subsonic"
) )
// Component that manages lazily loading more tracks into a Tracklist // Component that manages lazily loading more tracks into a Tracklist
// as the user scrolls near the bottom. // as the user scrolls near the bottom.
type TracklistLoader struct { type TracklistLoader struct {
tracklist *Tracklist tracklist *Tracklist
iter backend.TrackIterator iter mediaprovider.TrackIterator
trackBuffer []*subsonic.Child trackBuffer []*mediaprovider.Track
fetching bool fetching bool
done bool done bool
len int len int
highestShown int highestShown int
} }
func NewTracklistLoader(tracklist *Tracklist, iter backend.TrackIterator) TracklistLoader { func NewTracklistLoader(tracklist *Tracklist, iter mediaprovider.TrackIterator) TracklistLoader {
t := TracklistLoader{ t := TracklistLoader{
tracklist: tracklist, tracklist: tracklist,
iter: iter, iter: iter,
@@ -44,7 +42,7 @@ func (t *TracklistLoader) loadMoreTracks(num int) {
// repeat fetch task as long as user has scrolled near bottom // repeat fetch task as long as user has scrolled near bottom
for !t.done && t.highestShown >= t.len-25 { for !t.done && t.highestShown >= t.len-25 {
if t.trackBuffer == nil { if t.trackBuffer == nil {
t.trackBuffer = make([]*subsonic.Child, 0, num) t.trackBuffer = make([]*mediaprovider.Track, 0, num)
} }
t.trackBuffer = t.trackBuffer[:0] t.trackBuffer = t.trackBuffer[:0]
for i := 0; i < num; i++ { for i := 0; i < num; i++ {