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 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
}
-14
View File
@@ -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
}
+8 -8
View File
@@ -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
+7 -4
View File
@@ -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
View File
@@ -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] = &copy
@@ -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 -2
View File
@@ -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 {
LoggedInUser string
ServerID uuid.UUID
Server *subsonic.Client
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{}
}
}
-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"
"sort"
"github.com/dweymouth/go-subsonic/subsonic"
"github.com/dweymouth/supersonic/backend/mediaprovider"
)
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
}
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
func FindTrackByID(id string, tracks []*mediaprovider.Track) *mediaprovider.Track {
for _, tr := range tracks {
if id == tr.ID {
return tr
@@ -49,15 +49,15 @@ func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
return nil
}
func TrackIDOrEmptyStr(track *subsonic.Child) string {
func TrackIDOrEmptyStr(track *mediaprovider.Track) string {
if track == nil {
return ""
}
return track.ID
}
func TracksToIDs(tracks []*subsonic.Child) []string {
return MapSlice(tracks, func(tr *subsonic.Child) string {
func TracksToIDs(tracks []*mediaprovider.Track) []string {
return MapSlice(tracks, func(tr *mediaprovider.Track) string {
return tr.ID
})
}
@@ -73,8 +73,8 @@ const (
// Reorder tracks and return a new track slice.
// idxToMove must contain only valid indexes into tracks, and no repeats
func ReorderTracks(tracks []*subsonic.Child, idxToMove []int, op TrackReorderOp) []*subsonic.Child {
newTracks := make([]*subsonic.Child, len(tracks))
func ReorderTracks(tracks []*mediaprovider.Track, idxToMove []int, op TrackReorderOp) []*mediaprovider.Track {
newTracks := make([]*mediaprovider.Track, len(tracks))
switch op {
case MoveToTop:
topIdx := 0
+7 -7
View File
@@ -3,11 +3,11 @@ package sharedutil
import (
"testing"
"github.com/dweymouth/go-subsonic/subsonic"
"github.com/dweymouth/supersonic/backend/mediaprovider"
)
func Test_ReorderTracks(t *testing.T) {
tracks := []*subsonic.Child{
tracks := []*mediaprovider.Track{
{ID: "a"}, // 0
{ID: "b"}, // 1
{ID: "c"}, // 2
@@ -18,7 +18,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveToTop:
idxToMove := []int{0, 2, 3, 5}
want := []*subsonic.Child{
want := []*mediaprovider.Track{
{ID: "a"},
{ID: "c"},
{ID: "d"},
@@ -33,7 +33,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveToBottom:
idxToMove = []int{0, 2, 5}
want = []*subsonic.Child{
want = []*mediaprovider.Track{
{ID: "b"},
{ID: "d"},
{ID: "e"},
@@ -48,7 +48,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveUp:
idxToMove = []int{0, 1, 3, 5}
want = []*subsonic.Child{
want = []*mediaprovider.Track{
{ID: "a"},
{ID: "b"},
{ID: "d"},
@@ -63,7 +63,7 @@ func Test_ReorderTracks(t *testing.T) {
// test MoveDown:
idxToMove = []int{2, 4, 5}
want = []*subsonic.Child{
want = []*mediaprovider.Track{
{ID: "a"},
{ID: "b"},
{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()
if len(a) != len(b) {
return false
+6 -6
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
@@ -15,7 +16,6 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
type BottomPanel struct {
@@ -69,7 +69,7 @@ func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel
contr.NavigateTo(controller.AlbumRoute(bp.playbackManager.NowPlaying().AlbumID))
})
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() {
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 {
bp.NowPlaying.Update("", "", false, "", nil)
} else {
bp.coverArtID = song.CoverArt
bp.coverArtID = song.CoverArtID
var im image.Image
if bp.ImageManager != nil {
// 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
// if the user navigates to the album page for the track
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"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
@@ -17,8 +18,6 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
type AlbumPage struct {
@@ -92,11 +91,11 @@ func (a *AlbumPage) Route() controller.Route {
return controller.AlbumRoute(a.albumID)
}
func (a *AlbumPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) {
if song == nil {
func (a *AlbumPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track) {
if track == nil {
a.nowPlayingID = ""
} else {
a.nowPlayingID = song.ID
a.nowPlayingID = track.ID
}
a.tracklist.SetNowPlaying(a.nowPlayingID)
a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny))
@@ -116,14 +115,14 @@ func (a *AlbumPage) SelectAll() {
// should be called asynchronously
func (a *AlbumPage) load() {
album, err := a.lm.GetAlbum(a.albumID)
album, err := a.sm.Server.GetAlbum(a.albumID)
if err != nil {
log.Printf("Failed to get album: %s", err.Error())
return
}
a.header.Update(album, a.im)
a.tracklist.ShowDiscNumber = album.Song[0].DiscNumber != album.Song[len(album.Song)-1].DiscNumber
a.tracklist.Tracks = album.Song
a.tracklist.ShowDiscNumber = album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber
a.tracklist.Tracks = album.Tracks
a.tracklist.SetNowPlaying(a.nowPlayingID)
}
@@ -215,20 +214,20 @@ func (a *AlbumPageHeader) CreateRenderer() fyne.WidgetRenderer {
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.coverID = album.CoverArt
a.artistID = album.ArtistID
a.coverID = album.CoverArtID
a.artistID = album.ArtistIDs[0]
a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name
a.artistLabel.SetText(album.Artist)
a.genre = album.Genre
a.genreLabel.SetText(album.Genre)
a.artistLabel.SetText(album.ArtistNames[0])
a.genre = album.Genres[0]
a.genreLabel.SetText(album.Genres[0])
a.miscLabel.SetText(formatMiscLabelStr(album))
a.toggleFavButton.IsFavorited = !album.Starred.IsZero()
a.toggleFavButton.IsFavorited = album.Favorite
a.Refresh()
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.Refresh()
} else {
@@ -238,11 +237,8 @@ func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageMana
}
func (a *AlbumPageHeader) toggleFavorited() {
if a.toggleFavButton.IsFavorited {
a.page.sm.Server.Star(subsonic.StarParameters{AlbumIDs: []string{a.albumID}})
} else {
a.page.sm.Server.Unstar(subsonic.StarParameters{AlbumIDs: []string{a.albumID}})
}
params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}}
a.page.sm.Server.SetFavorite(params, a.toggleFavButton.IsFavorited)
}
func (a *AlbumPageHeader) showPopUpCover() {
@@ -254,16 +250,16 @@ func (a *AlbumPageHeader) showPopUpCover() {
a.page.contr.ShowPopUpImage(cover)
}
func formatMiscLabelStr(a *subsonic.AlbumID3) string {
func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) 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)
}
tracks := "tracks"
if a.SongCount == 1 {
if a.TrackCount == 1 {
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 {
+2 -1
View File
@@ -2,6 +2,7 @@ package browsing
import (
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/util"
@@ -29,7 +30,7 @@ type AlbumsPage struct {
searcher *widgets.SearchEntry
filterBtn *widgets.AlbumFilterButton
searchText string
filter backend.AlbumFilter
filter mediaprovider.AlbumFilter
titleDisp *widget.RichText
sortOrder *selectWidget
container *fyne.Container
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller"
@@ -118,7 +119,7 @@ func (a *ArtistPage) Save() SavedPage {
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)
if a.tracklistCtr != nil {
tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist)
+3 -4
View File
@@ -2,6 +2,7 @@ package browsing
import (
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme"
@@ -11,8 +12,6 @@ import (
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
type Page interface {
@@ -37,7 +36,7 @@ type CanSelectAll interface {
}
type CanShowNowPlaying interface {
OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child)
OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track)
}
type BrowsingPane struct {
@@ -160,7 +159,7 @@ func (b *BrowsingPane) doSetPage(p Page) bool {
return true
}
func (b *BrowsingPane) onSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) {
func (b *BrowsingPane) onSongChange(song, lastScrobbledIfAny *mediaprovider.Track) {
if b.curPage == nil {
return
}
+8 -9
View File
@@ -4,6 +4,7 @@ import (
"log"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme"
@@ -15,8 +16,6 @@ import (
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
type FavoritesPage struct {
@@ -216,7 +215,7 @@ func (a *FavoritesPage) OnSearched(query string) {
var _ CanShowNowPlaying = (*FavoritesPage)(nil)
func (a *FavoritesPage) OnSongChange(song *subsonic.Child, _ *subsonic.Child) {
func (a *FavoritesPage) OnSongChange(song, _ *mediaprovider.Track) {
a.nowPlayingID = ""
if song != nil {
a.nowPlayingID = song.ID
@@ -271,12 +270,12 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
a.createContainer(layout.NewSpacer())
}
go func() {
s, err := a.sm.Server.GetStarred2(nil)
fav, err := a.sm.Server.GetFavorites()
if err != nil {
log.Printf("error getting starred items: %s", err.Error())
return
}
model := buildArtistListModel(s.Artist)
model := buildArtistListModel(fav.Artists)
artistList := widgets.NewArtistGenreList(model)
artistList.ShowAlbumCount = true
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)
for _, ar := range artists {
model = append(model, widgets.ArtistGenreListItemModel{
@@ -320,12 +319,12 @@ func (a *FavoritesPage) onShowFavoriteSongs() {
a.createContainer(layout.NewSpacer())
}
go func() {
s, err := a.sm.Server.GetStarred2(nil)
fav, err := a.sm.Server.GetFavorites()
if err != nil {
log.Printf("error getting starred items: %s", err.Error())
return
}
tracklist := widgets.NewTracklist(s.Song)
tracklist := widgets.NewTracklist(fav.Tracks)
tracklist.AutoNumber = true
tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -360,7 +359,7 @@ type savedFavoritesPage struct {
lm *backend.LibraryManager
gridState widgets.GridViewState
searchGridState widgets.GridViewState
filter backend.AlbumFilter
filter mediaprovider.AlbumFilter
searchText string
activeToggleBtn int
}
+3 -3
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/ui/browsing"
"github.com/dweymouth/supersonic/ui/controller"
@@ -15,7 +16,6 @@ import (
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
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.Window.SetContent(m.container)
m.Window.Resize(size)
app.PlaybackManager.OnSongChange(func(song *subsonic.Child, _ *subsonic.Child) {
app.PlaybackManager.OnSongChange(func(song, _ *mediaprovider.Track) {
if song == nil {
m.Window.SetTitle(appName)
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() {
m.BrowsingPane.EnableNavigationButtons()
+3 -3
View File
@@ -9,7 +9,7 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"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/util"
)
@@ -21,11 +21,11 @@ type AlbumFilterButton struct {
GenreDisabled bool
FavoriteDisabled bool
filter *backend.AlbumFilter
filter *mediaprovider.AlbumFilter
dialog *widget.PopUp
}
func NewAlbumFilterButton(filter *backend.AlbumFilter) *AlbumFilterButton {
func NewAlbumFilterButton(filter *mediaprovider.AlbumFilter) *AlbumFilterButton {
a := &AlbumFilterButton{
filter: filter,
Button: widget.Button{
+27 -32
View File
@@ -5,8 +5,8 @@ import (
"log"
"strconv"
"sync"
"time"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/supersonic/ui/os"
@@ -18,7 +18,6 @@ import (
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic"
)
const (
@@ -40,7 +39,7 @@ type Tracklist struct {
// Tracks is the set of tracks displayed by the widget.
// Direct access to this is not thread-safe but OK for
// 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,
// or to use the number from the track's metadata
@@ -60,8 +59,8 @@ type Tracklist struct {
// user action callbacks
OnPlayTrackAt func(int)
OnPlaySelection func(tracks []*subsonic.Child, shuffle bool)
OnAddToQueue func(trackIDs []*subsonic.Child)
OnPlaySelection func(tracks []*mediaprovider.Track, shuffle bool)
OnAddToQueue func(trackIDs []*mediaprovider.Track)
OnAddToPlaylist func(trackIDs []string)
OnSetFavorite func(trackIDs []string, fav bool)
OnSetRating func(trackIDs []string, rating int)
@@ -85,7 +84,7 @@ type Tracklist struct {
container *fyne.Container
}
func NewTracklist(tracks []*subsonic.Child) *Tracklist {
func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
t := &Tracklist{Tracks: tracks, visibleColumns: make([]bool, 12)}
t.ExtendBaseWidget(t)
@@ -144,7 +143,7 @@ func (t *Tracklist) buildHeader() {
}
// 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()
defer t.tracksMutex.RUnlock()
if idx >= len(t.Tracks) {
@@ -216,7 +215,7 @@ func (t *Tracklist) Clear() {
}
// 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()
defer t.tracksMutex.Unlock()
t.Tracks = append(t.Tracks, trs...)
@@ -330,16 +329,12 @@ func (t *Tracklist) onSetFavorite(trackID string, fav bool) {
t.tracksMutex.RLock()
tr := sharedutil.FindTrackByID(trackID, t.Tracks)
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 {
if fav {
tr.Starred = time.Now()
} else {
tr.Starred = time.Time{}
}
tr.Favorite = fav
}
if needRefresh {
t.Refresh()
@@ -355,12 +350,12 @@ func (t *Tracklist) onSetRating(trackID string, rating int) {
t.tracksMutex.RLock()
tr := sharedutil.FindTrackByID(trackID, t.Tracks)
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 {
tr.UserRating = rating
tr.Rating = rating
}
if needRefresh {
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()
tracks := make([]*subsonic.Child, 0, len(sel))
tracks := make([]*mediaprovider.Track, 0, len(sel))
t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock()
for _, idx := range sel {
@@ -484,7 +479,7 @@ type TrackRow struct {
albumID string
isPlaying bool
isFavorite bool
playCount int64
playCount int
num *widget.RichText
name *widget.RichText
@@ -543,37 +538,37 @@ func newTrailingAlignRichText() *widget.RichText {
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
// a new track (*subsonic.Child)
// a new track (*mediaprovider.Track)
if tr.ID != t.trackID {
if t.Focused {
fyne.CurrentApp().Driver().CanvasForObject(t).Focus(nil)
t.Focused = false
}
t.trackID = tr.ID
t.artistID = tr.ArtistID
t.artistID = tr.ArtistIDs[0]
t.albumID = tr.AlbumID
t.name.Segments[0].(*widget.TextSegment).Text = tr.Title
t.artist.SetText(tr.Artist)
t.artist.Disabled = tr.ArtistID == ""
t.name.Segments[0].(*widget.TextSegment).Text = tr.Name
t.artist.SetText(tr.ArtistNames[0])
t.artist.Disabled = tr.ArtistIDs[0] == ""
t.album.SetText(tr.Album)
t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration))
t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year)
t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount))
t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate)
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
// (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 {
discNum := -1
var str string
if rowNum < 0 {
rowNum = tr.Track
rowNum = tr.TrackNumber
if t.tracklist.ShowDiscNumber {
discNum = tr.DiscNumber
}
@@ -612,7 +607,7 @@ func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) {
}
// Render favorite column
if tr.Starred.IsZero() {
if tr.Favorite {
t.isFavorite = false
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon
} else {
@@ -620,7 +615,7 @@ func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) {
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
t.artist.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnArtist)]
+5 -7
View File
@@ -1,25 +1,23 @@
package widgets
import (
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/go-subsonic/subsonic"
"github.com/dweymouth/supersonic/backend/mediaprovider"
)
// Component that manages lazily loading more tracks into a Tracklist
// as the user scrolls near the bottom.
type TracklistLoader struct {
tracklist *Tracklist
iter backend.TrackIterator
iter mediaprovider.TrackIterator
trackBuffer []*subsonic.Child
trackBuffer []*mediaprovider.Track
fetching bool
done bool
len int
highestShown int
}
func NewTracklistLoader(tracklist *Tracklist, iter backend.TrackIterator) TracklistLoader {
func NewTracklistLoader(tracklist *Tracklist, iter mediaprovider.TrackIterator) TracklistLoader {
t := TracklistLoader{
tracklist: tracklist,
iter: iter,
@@ -44,7 +42,7 @@ func (t *TracklistLoader) loadMoreTracks(num int) {
// repeat fetch task as long as user has scrolled near bottom
for !t.done && t.highestShown >= t.len-25 {
if t.trackBuffer == nil {
t.trackBuffer = make([]*subsonic.Child, 0, num)
t.trackBuffer = make([]*mediaprovider.Track, 0, num)
}
t.trackBuffer = t.trackBuffer[:0]
for i := 0; i < num; i++ {