Merge pull request #164 from dweymouth/refactor/generic-server-adapter
Prepare for eventual Jellyfin integration: create a generic MediaProvider model + interface
This commit is contained in:
+2
-4
@@ -25,7 +25,6 @@ type App struct {
|
|||||||
Config *Config
|
Config *Config
|
||||||
ServerManager *ServerManager
|
ServerManager *ServerManager
|
||||||
ImageManager *ImageManager
|
ImageManager *ImageManager
|
||||||
LibraryManager *LibraryManager
|
|
||||||
PlaybackManager *PlaybackManager
|
PlaybackManager *PlaybackManager
|
||||||
Player *player.Player
|
Player *player.Player
|
||||||
UpdateChecker UpdateChecker
|
UpdateChecker UpdateChecker
|
||||||
@@ -63,11 +62,10 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A
|
|||||||
|
|
||||||
a.ServerManager = NewServerManager(appName)
|
a.ServerManager = NewServerManager(appName)
|
||||||
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
|
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
|
||||||
a.LibraryManager = NewLibraryManager(a.ServerManager)
|
|
||||||
a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName))
|
a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName))
|
||||||
a.LibraryManager.PreCacheCoverFn = func(coverID string) {
|
a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) {
|
||||||
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
|
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
|
||||||
}
|
})
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -124,7 +124,7 @@ func DefaultConfig(appVersionTag string) *Config {
|
|||||||
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"},
|
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"},
|
||||||
},
|
},
|
||||||
AlbumsPage: AlbumsPageConfig{
|
AlbumsPage: AlbumsPageConfig{
|
||||||
SortOrder: string(AlbumSortRecentlyAdded),
|
SortOrder: string("Recently Added"),
|
||||||
},
|
},
|
||||||
ArtistPage: ArtistPageConfig{
|
ArtistPage: ArtistPageConfig{
|
||||||
InitialView: "Discography",
|
InitialView: "Discography",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package backend
|
|
||||||
|
|
||||||
import (
|
|
||||||
subsonic "github.com/dweymouth/go-subsonic/subsonic"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AlbumIterator interface {
|
|
||||||
Next() *subsonic.AlbumID3
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrackIterator interface {
|
|
||||||
Next() *subsonic.Child
|
|
||||||
}
|
|
||||||
|
|
||||||
type LibraryManager struct {
|
|
||||||
PreCacheCoverFn func(coverID string)
|
|
||||||
|
|
||||||
s *ServerManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLibraryManager(s *ServerManager) *LibraryManager {
|
|
||||||
return &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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package mediaprovider
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlbumIterator interface {
|
||||||
|
Next() *Album
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackIterator interface {
|
||||||
|
Next() *Track
|
||||||
|
}
|
||||||
|
|
||||||
|
type RatingFavoriteParameters struct {
|
||||||
|
AlbumIDs []string
|
||||||
|
ArtistIDs []string
|
||||||
|
TrackIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Favorites struct {
|
||||||
|
Albums []*Album
|
||||||
|
Artists []*Artist
|
||||||
|
Tracks []*Track
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaProvider interface {
|
||||||
|
SetPrefetchCoverCallback(cb func(coverArtID string))
|
||||||
|
|
||||||
|
GetAlbum(albumID string) (*AlbumWithTracks, error)
|
||||||
|
|
||||||
|
GetArtist(artistID string) (*ArtistWithAlbums, error)
|
||||||
|
|
||||||
|
GetArtistInfo(artistID string) (*ArtistInfo, error)
|
||||||
|
|
||||||
|
GetPlaylist(playlistID string) (*PlaylistWithTracks, error)
|
||||||
|
|
||||||
|
GetCoverArt(coverArtID string, size int) (image.Image, error)
|
||||||
|
|
||||||
|
AlbumSortOrders() []string
|
||||||
|
|
||||||
|
IterateAlbums(sortOrder string, filter AlbumFilter) AlbumIterator
|
||||||
|
|
||||||
|
IterateTracks(searchQuery string) TrackIterator
|
||||||
|
|
||||||
|
SearchAlbums(searchQuery string, filter AlbumFilter) AlbumIterator
|
||||||
|
|
||||||
|
GetRandomTracks(genre string, count int) ([]*Track, error)
|
||||||
|
|
||||||
|
GetSimilarTracks(artistID string, count int) ([]*Track, error)
|
||||||
|
|
||||||
|
GetArtists() ([]*Artist, error)
|
||||||
|
|
||||||
|
GetGenres() ([]*Genre, error)
|
||||||
|
|
||||||
|
GetFavorites() (Favorites, error)
|
||||||
|
|
||||||
|
GetStreamURL(trackID string) (string, error)
|
||||||
|
|
||||||
|
GetTopTracks(artist Artist, count int) ([]*Track, error)
|
||||||
|
|
||||||
|
SetFavorite(params RatingFavoriteParameters, favorite bool) error
|
||||||
|
|
||||||
|
SetRating(params RatingFavoriteParameters, rating int) error
|
||||||
|
|
||||||
|
GetPlaylists() ([]*Playlist, error)
|
||||||
|
|
||||||
|
CreatePlaylist(name string, trackIDs []string) error
|
||||||
|
|
||||||
|
EditPlaylist(id, name, description string, public bool) error
|
||||||
|
|
||||||
|
EditPlaylistTracks(id string, trackIDsToAdd []string, trackIndexesToRemove []int) error
|
||||||
|
|
||||||
|
ReplacePlaylistTracks(id string, trackIDs []string) error
|
||||||
|
|
||||||
|
DeletePlaylist(id string) error
|
||||||
|
|
||||||
|
Scrobble(trackID string, submission bool) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package mediaprovider
|
||||||
|
|
||||||
|
type Album struct {
|
||||||
|
ID string
|
||||||
|
CoverArtID string
|
||||||
|
Name string
|
||||||
|
Duration int
|
||||||
|
ArtistIDs []string
|
||||||
|
ArtistNames []string
|
||||||
|
Year int
|
||||||
|
Genres []string
|
||||||
|
TrackCount int
|
||||||
|
Favorite bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlbumWithTracks struct {
|
||||||
|
Album
|
||||||
|
Tracks []*Track
|
||||||
|
}
|
||||||
|
|
||||||
|
type Artist struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Favorite bool
|
||||||
|
AlbumCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistWithAlbums struct {
|
||||||
|
Artist
|
||||||
|
Albums []*Album
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistInfo struct {
|
||||||
|
Biography string
|
||||||
|
LastFMUrl string
|
||||||
|
ImageURL string
|
||||||
|
SimilarArtists []*Artist
|
||||||
|
}
|
||||||
|
|
||||||
|
type Genre struct {
|
||||||
|
Name string
|
||||||
|
AlbumCount int
|
||||||
|
TrackCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Track struct {
|
||||||
|
ID string
|
||||||
|
CoverArtID string
|
||||||
|
ParentID string
|
||||||
|
Name string
|
||||||
|
Duration int
|
||||||
|
TrackNumber int
|
||||||
|
DiscNumber int
|
||||||
|
Genre string
|
||||||
|
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 {
|
||||||
|
ID string
|
||||||
|
CoverArtID string
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Public bool
|
||||||
|
Owner string
|
||||||
|
Duration int
|
||||||
|
TrackCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaylistWithTracks struct {
|
||||||
|
Playlist
|
||||||
|
Tracks []*Track
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package backend
|
package subsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
@@ -6,45 +6,35 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
"github.com/dweymouth/supersonic/sharedutil"
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlbumSortOrder string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AlbumSortRecentlyAdded AlbumSortOrder = "Recently Added"
|
AlbumSortRecentlyAdded string = "Recently Added"
|
||||||
AlbumSortRecentlyPlayed AlbumSortOrder = "Recently Played"
|
AlbumSortRecentlyPlayed string = "Recently Played"
|
||||||
AlbumSortFrequentlyPlayed AlbumSortOrder = "Frequently Played"
|
AlbumSortFrequentlyPlayed string = "Frequently Played"
|
||||||
AlbumSortRandom AlbumSortOrder = "Random"
|
AlbumSortRandom string = "Random"
|
||||||
AlbumSortTitleAZ AlbumSortOrder = "Title (A-Z)"
|
AlbumSortTitleAZ string = "Title (A-Z)"
|
||||||
AlbumSortArtistAZ AlbumSortOrder = "Artist (A-Z)"
|
AlbumSortArtistAZ string = "Artist (A-Z)"
|
||||||
AlbumSortYearAscending AlbumSortOrder = "Year (ascending)"
|
AlbumSortYearAscending string = "Year (ascending)"
|
||||||
AlbumSortYearDescending AlbumSortOrder = "Year (descending)"
|
AlbumSortYearDescending string = "Year (descending)"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (s *subsonicMediaProvider) AlbumSortOrders() []string {
|
||||||
AlbumSortOrders []string = []string{
|
return []string{
|
||||||
string(AlbumSortRecentlyAdded),
|
AlbumSortRecentlyAdded,
|
||||||
string(AlbumSortRecentlyPlayed),
|
AlbumSortRecentlyPlayed,
|
||||||
string(AlbumSortFrequentlyPlayed),
|
AlbumSortFrequentlyPlayed,
|
||||||
string(AlbumSortRandom),
|
AlbumSortRandom,
|
||||||
string(AlbumSortTitleAZ),
|
AlbumSortTitleAZ,
|
||||||
string(AlbumSortArtistAZ),
|
AlbumSortArtistAZ,
|
||||||
string(AlbumSortYearAscending),
|
AlbumSortYearAscending,
|
||||||
string(AlbumSortYearDescending),
|
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 {
|
func filterMatches(f mediaprovider.AlbumFilter, album *subsonic.AlbumID3) bool {
|
||||||
if album == nil {
|
if album == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -68,82 +58,66 @@ func (f *AlbumFilter) Matches(album *subsonic.AlbumID3) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *AlbumFilter) IsEmpty() bool {
|
func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
|
||||||
return !f.ExcludeFavorited && !f.ExcludeUnfavorited &&
|
if sortOrder == "" && len(filter.Genres) == 1 {
|
||||||
f.MinYear == 0 && f.MaxYear == 0 && len(f.Genres) == 0
|
return s.newBaseIter("byGenre", filter, s.prefetchCoverCB, map[string]string{"genre": filter.Genres[0]})
|
||||||
}
|
}
|
||||||
|
if sortOrder == "" && filter.ExcludeUnfavorited {
|
||||||
func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder, filter AlbumFilter) AlbumIterator {
|
return s.newBaseIter("starred", filter, s.prefetchCoverCB, make(map[string]string))
|
||||||
switch sort {
|
}
|
||||||
|
if sortOrder == "" {
|
||||||
|
sortOrder = AlbumSortRecentlyAdded // default
|
||||||
|
}
|
||||||
|
switch sortOrder {
|
||||||
case AlbumSortRecentlyAdded:
|
case AlbumSortRecentlyAdded:
|
||||||
return l.newBaseIter("newest", filter, make(map[string]string))
|
return s.newBaseIter("newest", filter, s.prefetchCoverCB, make(map[string]string))
|
||||||
case AlbumSortRecentlyPlayed:
|
case AlbumSortRecentlyPlayed:
|
||||||
return l.newBaseIter("recent", filter, make(map[string]string))
|
return s.newBaseIter("recent", filter, s.prefetchCoverCB, make(map[string]string))
|
||||||
case AlbumSortFrequentlyPlayed:
|
case AlbumSortFrequentlyPlayed:
|
||||||
return l.newBaseIter("frequent", filter, make(map[string]string))
|
return s.newBaseIter("frequent", filter, s.prefetchCoverCB, make(map[string]string))
|
||||||
case AlbumSortRandom:
|
case AlbumSortRandom:
|
||||||
return l.newRandomIter()
|
return s.newRandomIter(filter, s.prefetchCoverCB)
|
||||||
case AlbumSortTitleAZ:
|
case AlbumSortTitleAZ:
|
||||||
return l.newBaseIter("alphabeticalByName", filter, make(map[string]string))
|
return s.newBaseIter("alphabeticalByName", filter, s.prefetchCoverCB, make(map[string]string))
|
||||||
case AlbumSortArtistAZ:
|
case AlbumSortArtistAZ:
|
||||||
return l.newBaseIter("alphabeticalByArtist", filter, make(map[string]string))
|
return s.newBaseIter("alphabeticalByArtist", filter, s.prefetchCoverCB, make(map[string]string))
|
||||||
case AlbumSortYearAscending:
|
case AlbumSortYearAscending:
|
||||||
return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "0", "toYear": "3000"})
|
return s.newBaseIter("byYear", filter, s.prefetchCoverCB, map[string]string{"fromYear": "0", "toYear": "3000"})
|
||||||
case AlbumSortYearDescending:
|
case AlbumSortYearDescending:
|
||||||
return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "3000", "toYear": "0"})
|
return s.newBaseIter("byYear", filter, s.prefetchCoverCB, map[string]string{"fromYear": "3000", "toYear": "0"})
|
||||||
default:
|
default:
|
||||||
log.Printf("Undefined album sort order: %s", sort)
|
log.Printf("Undefined album sort order: %s", sortOrder)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LibraryManager) StarredIter(filter AlbumFilter) AlbumIterator {
|
func (s *subsonicMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
|
||||||
return l.newBaseIter("starred", filter, make(map[string]string))
|
return s.newSearchIter(searchQuery, filter, s.prefetchCoverCB)
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
type baseIter struct {
|
||||||
listType string
|
listType string
|
||||||
filter AlbumFilter
|
filter mediaprovider.AlbumFilter
|
||||||
|
prefetchCB func(string)
|
||||||
serverPos int
|
serverPos int
|
||||||
l *LibraryManager
|
|
||||||
s *subsonic.Client
|
s *subsonic.Client
|
||||||
opts map[string]string
|
opts map[string]string
|
||||||
prefetched []*subsonic.AlbumID3
|
prefetched []*mediaprovider.Album
|
||||||
prefetchedPos int
|
prefetchedPos int
|
||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LibraryManager) newBaseIter(listType string, filter AlbumFilter, opts map[string]string) *baseIter {
|
func (s *subsonicMediaProvider) newBaseIter(listType string, filter mediaprovider.AlbumFilter, cb func(string), opts map[string]string) *baseIter {
|
||||||
return &baseIter{
|
return &baseIter{
|
||||||
listType: listType,
|
prefetchCB: cb,
|
||||||
filter: filter,
|
listType: listType,
|
||||||
l: l,
|
filter: filter,
|
||||||
s: l.s.Server,
|
s: s.client,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *baseIter) Next() *subsonic.AlbumID3 {
|
func (r *baseIter) Next() *mediaprovider.Album {
|
||||||
if r.done {
|
if r.done {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -165,46 +139,45 @@ func (r *baseIter) Next() *subsonic.AlbumID3 {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r.serverPos += len(albums)
|
r.serverPos += len(albums)
|
||||||
albums = sharedutil.FilterSlice(albums, r.filter.Matches)
|
albums = sharedutil.FilterSlice(albums, func(al *subsonic.AlbumID3) bool { return filterMatches(r.filter, al) })
|
||||||
r.prefetched = albums
|
r.prefetched = sharedutil.MapSlice(albums, toAlbum)
|
||||||
if len(albums) > 0 {
|
if len(albums) > 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.prefetchedPos = 1
|
r.prefetchedPos = 1
|
||||||
if r.l.PreCacheCoverFn != nil {
|
if r.prefetchCB != nil {
|
||||||
for _, album := range r.prefetched {
|
for _, album := range r.prefetched {
|
||||||
go r.l.PreCacheCoverFn(album.CoverArt)
|
go r.prefetchCB(album.CoverArtID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.prefetched[0]
|
return r.prefetched[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
type searchIter struct {
|
type searchIter struct {
|
||||||
searchIterBase
|
searchIterBase
|
||||||
|
|
||||||
l *LibraryManager
|
prefetchCB func(string)
|
||||||
filter AlbumFilter
|
filter mediaprovider.AlbumFilter
|
||||||
prefetched []*subsonic.AlbumID3
|
prefetched []*subsonic.AlbumID3
|
||||||
prefetchedPos int
|
prefetchedPos int
|
||||||
albumIDset map[string]bool
|
albumIDset map[string]bool
|
||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LibraryManager) newSearchIter(query string, filter AlbumFilter) *searchIter {
|
func (s *subsonicMediaProvider) newSearchIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchIter {
|
||||||
return &searchIter{
|
return &searchIter{
|
||||||
searchIterBase: searchIterBase{
|
searchIterBase: searchIterBase{
|
||||||
query: query,
|
query: query,
|
||||||
s: l.s.Server,
|
s: s.client,
|
||||||
},
|
},
|
||||||
l: l,
|
prefetchCB: cb,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
albumIDset: make(map[string]bool),
|
albumIDset: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *searchIter) Next() *subsonic.AlbumID3 {
|
func (s *searchIter) Next() *mediaprovider.Album {
|
||||||
if s.done {
|
if s.done {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -257,7 +230,7 @@ func (s *searchIter) Next() *subsonic.AlbumID3 {
|
|||||||
s.prefetchedPos = 0
|
s.prefetchedPos = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return a
|
return toAlbum(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -268,20 +241,21 @@ func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) {
|
|||||||
if _, have := s.albumIDset[album.ID]; have {
|
if _, have := s.albumIDset[album.ID]; have {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !s.filter.Matches(album) {
|
if !filterMatches(s.filter, album) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.prefetched = append(s.prefetched, album)
|
s.prefetched = append(s.prefetched, album)
|
||||||
if s.l.PreCacheCoverFn != nil {
|
if s.prefetchCB != nil {
|
||||||
go s.l.PreCacheCoverFn(album.CoverArt)
|
go s.prefetchCB(album.CoverArt)
|
||||||
}
|
}
|
||||||
s.albumIDset[album.ID] = true
|
s.albumIDset[album.ID] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type randomIter struct {
|
type randomIter struct {
|
||||||
|
filter mediaprovider.AlbumFilter
|
||||||
|
prefetchCB func(coverArtID string)
|
||||||
albumIDSet map[string]bool
|
albumIDSet map[string]bool
|
||||||
l *LibraryManager
|
|
||||||
s *subsonic.Client
|
s *subsonic.Client
|
||||||
prefetched []*subsonic.AlbumID3
|
prefetched []*subsonic.AlbumID3
|
||||||
prefetchedPos int
|
prefetchedPos int
|
||||||
@@ -296,44 +270,45 @@ type randomIter struct {
|
|||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LibraryManager) newRandomIter() *randomIter {
|
func (s *subsonicMediaProvider) newRandomIter(filter mediaprovider.AlbumFilter, cb func(string)) *randomIter {
|
||||||
return &randomIter{
|
return &randomIter{
|
||||||
l: l,
|
filter: filter,
|
||||||
s: l.s.Server,
|
prefetchCB: cb,
|
||||||
|
s: s.client,
|
||||||
albumIDSet: make(map[string]bool),
|
albumIDSet: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *randomIter) Next() *subsonic.AlbumID3 {
|
func (r *randomIter) Next() *mediaprovider.Album {
|
||||||
if r.done {
|
if r.done {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.prefetched == nil {
|
// repeat fetch task until we have matching results
|
||||||
|
// or we reach the end (handled via short circuit return)
|
||||||
|
for len(r.prefetched) == 0 {
|
||||||
if r.phaseTwo {
|
if r.phaseTwo {
|
||||||
for len(r.prefetched) == 0 {
|
// fetch albums from deterministic order
|
||||||
albums, err := r.s.GetAlbumList2("newest", map[string]string{"size": "20", "offset": strconv.Itoa(r.offset)})
|
albums, err := r.s.GetAlbumList2("newest", map[string]string{"size": "25", "offset": strconv.Itoa(r.offset)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Printf("error fetching albums: %s", err.Error())
|
||||||
albums = nil
|
albums = nil
|
||||||
}
|
}
|
||||||
if len(albums) == 0 {
|
if len(albums) == 0 {
|
||||||
r.done = true
|
r.done = true
|
||||||
r.albumIDSet = nil
|
r.albumIDSet = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r.offset += len(albums)
|
r.offset += len(albums)
|
||||||
for _, album := range albums {
|
for _, album := range albums {
|
||||||
if _, ok := r.albumIDSet[album.ID]; !ok {
|
if _, ok := r.albumIDSet[album.ID]; !ok && filterMatches(r.filter, album) {
|
||||||
r.prefetched = append(r.prefetched, album)
|
r.prefetched = append(r.prefetched, album)
|
||||||
if r.l.PreCacheCoverFn != nil {
|
if r.prefetchCB != nil {
|
||||||
go r.l.PreCacheCoverFn(album.CoverArt)
|
go r.prefetchCB(album.CoverArt)
|
||||||
}
|
|
||||||
r.albumIDSet[album.ID] = true
|
|
||||||
}
|
}
|
||||||
|
r.albumIDSet[album.ID] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.prefetchedPos = 0
|
|
||||||
} else {
|
} else {
|
||||||
albums, err := r.s.GetAlbumList2("random", map[string]string{"size": "25"})
|
albums, err := r.s.GetAlbumList2("random", map[string]string{"size": "25"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -345,12 +320,16 @@ func (r *randomIter) Next() *subsonic.AlbumID3 {
|
|||||||
var hitCount int
|
var hitCount int
|
||||||
for _, album := range albums {
|
for _, album := range albums {
|
||||||
if _, ok := r.albumIDSet[album.ID]; !ok {
|
if _, ok := r.albumIDSet[album.ID]; !ok {
|
||||||
|
// still need to keep track even if album is not matched
|
||||||
|
// by the filter because we need to know when to move to phase two
|
||||||
hitCount++
|
hitCount++
|
||||||
r.prefetched = append(r.prefetched, album)
|
|
||||||
if r.l.PreCacheCoverFn != nil {
|
|
||||||
go r.l.PreCacheCoverFn(album.CoverArt)
|
|
||||||
}
|
|
||||||
r.albumIDSet[album.ID] = true
|
r.albumIDSet[album.ID] = true
|
||||||
|
if filterMatches(r.filter, album) {
|
||||||
|
r.prefetched = append(r.prefetched, album)
|
||||||
|
if r.prefetchCB != nil {
|
||||||
|
go r.prefetchCB(album.CoverArt)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if successRatio := float64(hitCount) / float64(25); successRatio < 0.3 {
|
if successRatio := float64(hitCount) / float64(25); successRatio < 0.3 {
|
||||||
@@ -368,30 +347,8 @@ func (r *randomIter) Next() *subsonic.AlbumID3 {
|
|||||||
r.prefetchedPos = 0
|
r.prefetchedPos = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return a
|
return toAlbum(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package subsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type searchIterBase struct {
|
||||||
|
query string
|
||||||
|
artistOffset int
|
||||||
|
albumOffset int
|
||||||
|
songOffset int
|
||||||
|
s *subsonic.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searchIterBase) fetchResults() *subsonic.SearchResult3 {
|
||||||
|
searchOpts := map[string]string{
|
||||||
|
"artistOffset": strconv.Itoa(s.artistOffset),
|
||||||
|
"albumOffset": strconv.Itoa(s.albumOffset),
|
||||||
|
"songOffset": strconv.Itoa(s.songOffset),
|
||||||
|
}
|
||||||
|
results, err := s.s.Search3(s.query, searchOpts)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
results = nil
|
||||||
|
}
|
||||||
|
if results == nil || len(results.Album)+len(results.Artist)+len(results.Song) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
package subsonic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type subsonicMediaProvider struct {
|
||||||
|
client *subsonic.Client
|
||||||
|
prefetchCoverCB func(coverArtID string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
|
||||||
|
return &subsonicMediaProvider{client: subsonicClient}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID string)) {
|
||||||
|
s.prefetchCoverCB = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
|
||||||
|
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) DeletePlaylist(id string) error {
|
||||||
|
return s.client.DeletePlaylist(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) EditPlaylist(id, name, description string, public bool) error {
|
||||||
|
return s.client.UpdatePlaylist(id, map[string]string{
|
||||||
|
"name": name,
|
||||||
|
"comment": description,
|
||||||
|
"public": strconv.FormatBool(public),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) EditPlaylistTracks(id string, trackIDsToAdd []string, trackIndexesToRemove []int) error {
|
||||||
|
return s.client.UpdatePlaylistTracks(id, trackIDsToAdd, trackIndexesToRemove)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWithTracks, error) {
|
||||||
|
al, err := s.client.GetAlbum(albumID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
album := &mediaprovider.AlbumWithTracks{
|
||||||
|
Tracks: sharedutil.MapSlice(al.Song, toTrack),
|
||||||
|
}
|
||||||
|
fillAlbum(al, &album.Album)
|
||||||
|
return album, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetArtist(artistID string) (*mediaprovider.ArtistWithAlbums, error) {
|
||||||
|
ar, err := s.client.GetArtist(artistID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mediaprovider.ArtistWithAlbums{
|
||||||
|
Artist: mediaprovider.Artist{
|
||||||
|
ID: ar.ID,
|
||||||
|
Name: ar.Name,
|
||||||
|
Favorite: !ar.Starred.IsZero(),
|
||||||
|
AlbumCount: ar.AlbumCount,
|
||||||
|
},
|
||||||
|
Albums: sharedutil.MapSlice(ar.Album, toAlbum),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.ArtistInfo, error) {
|
||||||
|
info, err := s.client.GetArtistInfo(artistID, map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &mediaprovider.ArtistInfo{
|
||||||
|
Biography: info.Biography,
|
||||||
|
LastFMUrl: info.LastFmUrl,
|
||||||
|
ImageURL: info.LargeImageUrl,
|
||||||
|
SimilarArtists: sharedutil.MapSlice(info.SimilarArtist, toArtist),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) {
|
||||||
|
idxs, err := s.client.GetArtists(map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var artists []*mediaprovider.Artist
|
||||||
|
for _, idx := range idxs.Index {
|
||||||
|
for _, ar := range idx.Artist {
|
||||||
|
artists = append(artists, toArtistFromID3(ar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return artists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) {
|
||||||
|
params := map[string]string{}
|
||||||
|
if size > 0 {
|
||||||
|
params["size"] = strconv.Itoa(size)
|
||||||
|
}
|
||||||
|
return s.client.GetCoverArt(id, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
|
||||||
|
fav, err := s.client.GetStarred2(map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return mediaprovider.Favorites{}, err
|
||||||
|
}
|
||||||
|
return mediaprovider.Favorites{
|
||||||
|
Albums: sharedutil.MapSlice(fav.Album, toAlbum),
|
||||||
|
Artists: sharedutil.MapSlice(fav.Artist, toArtistFromID3),
|
||||||
|
Tracks: sharedutil.MapSlice(fav.Song, toTrack),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
|
||||||
|
g, err := s.client.GetGenres()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre {
|
||||||
|
return &mediaprovider.Genre{
|
||||||
|
Name: g.Name,
|
||||||
|
AlbumCount: g.AlbumCount,
|
||||||
|
TrackCount: g.SongCount,
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.PlaylistWithTracks, error) {
|
||||||
|
pl, err := s.client.GetPlaylist(playlistID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
playlist := &mediaprovider.PlaylistWithTracks{
|
||||||
|
Tracks: sharedutil.MapSlice(pl.Entry, toTrack),
|
||||||
|
}
|
||||||
|
fillPlaylist(pl, &playlist.Playlist)
|
||||||
|
return playlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) {
|
||||||
|
pl, err := s.client.GetPlaylists(map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.MapSlice(pl, toPlaylist), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]*mediaprovider.Track, error) {
|
||||||
|
opts := map[string]string{"size": strconv.Itoa(count)}
|
||||||
|
if genreName != "" {
|
||||||
|
opts["genre"] = genreName
|
||||||
|
}
|
||||||
|
tr, err := s.client.GetRandomSongs(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.MapSlice(tr, toTrack), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([]*mediaprovider.Track, error) {
|
||||||
|
tr, err := s.client.GetSimilarSongs2(artistID, map[string]string{"count": strconv.Itoa(count)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.MapSlice(tr, toTrack), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetStreamURL(trackID string) (string, error) {
|
||||||
|
u, err := s.client.GetStreamURL(trackID, map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) GetTopTracks(artist mediaprovider.Artist, count int) ([]*mediaprovider.Track, error) {
|
||||||
|
params := map[string]string{}
|
||||||
|
if count > 0 {
|
||||||
|
params["count"] = strconv.Itoa(count)
|
||||||
|
}
|
||||||
|
tr, err := s.client.GetTopSongs(artist.Name, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.MapSlice(tr, toTrack), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) ReplacePlaylistTracks(playlistID string, trackIDs []string) error {
|
||||||
|
return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"playlistId": playlistID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) Scrobble(trackID string, submission bool) error {
|
||||||
|
return s.client.Scrobble(trackID, map[string]string{
|
||||||
|
"time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
||||||
|
"submission": strconv.FormatBool(submission)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error {
|
||||||
|
subParams := subsonic.StarParameters{
|
||||||
|
AlbumIDs: params.AlbumIDs,
|
||||||
|
ArtistIDs: params.ArtistIDs,
|
||||||
|
SongIDs: params.TrackIDs,
|
||||||
|
}
|
||||||
|
if favorite {
|
||||||
|
return s.client.Star(subParams)
|
||||||
|
}
|
||||||
|
return s.client.Unstar(subParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoriteParameters, rating int) error {
|
||||||
|
// Subsonic doesn't allow bulk setting ratings.
|
||||||
|
// To not overwhelm the server with requests, set rating for
|
||||||
|
// only 5 tracks at a time concurrently
|
||||||
|
batchSize := 5
|
||||||
|
var err error
|
||||||
|
batchSetRating := func(offs int, wg *sync.WaitGroup) {
|
||||||
|
for i := 0; i < batchSize && offs+i < len(params.TrackIDs); i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int) {
|
||||||
|
newErr := s.client.SetRating(params.TrackIDs[idx], rating)
|
||||||
|
if err == nil && newErr != nil {
|
||||||
|
err = newErr
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(offs + i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numBatches := int(math.Ceil(float64(len(params.TrackIDs)) / float64(batchSize)))
|
||||||
|
for i := 0; i < numBatches; i++ {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
batchSetRating(i*batchSize, &wg)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
||||||
|
if ch == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &mediaprovider.Track{
|
||||||
|
ID: ch.ID,
|
||||||
|
CoverArtID: ch.CoverArt,
|
||||||
|
ParentID: ch.Parent,
|
||||||
|
Name: ch.Title,
|
||||||
|
Duration: ch.Duration,
|
||||||
|
TrackNumber: ch.Track,
|
||||||
|
DiscNumber: ch.DiscNumber,
|
||||||
|
Genre: ch.Genre,
|
||||||
|
ArtistIDs: []string{ch.ArtistID},
|
||||||
|
ArtistNames: []string{ch.Artist},
|
||||||
|
Album: ch.Album,
|
||||||
|
AlbumID: ch.AlbumID,
|
||||||
|
Year: ch.Year,
|
||||||
|
Rating: ch.UserRating,
|
||||||
|
Favorite: !ch.Starred.IsZero(),
|
||||||
|
PlayCount: int(ch.PlayCount),
|
||||||
|
FilePath: ch.Path,
|
||||||
|
Size: ch.Size,
|
||||||
|
BitRate: ch.BitRate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album {
|
||||||
|
if al == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
album := &mediaprovider.Album{}
|
||||||
|
fillAlbum(al, album)
|
||||||
|
return album
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillAlbum(subAlbum *subsonic.AlbumID3, album *mediaprovider.Album) {
|
||||||
|
album.ID = subAlbum.ID
|
||||||
|
album.CoverArtID = subAlbum.CoverArt
|
||||||
|
album.Name = subAlbum.Name
|
||||||
|
album.Duration = subAlbum.Duration
|
||||||
|
album.ArtistIDs = []string{subAlbum.ArtistID}
|
||||||
|
album.ArtistNames = []string{subAlbum.Artist}
|
||||||
|
album.Year = subAlbum.Year
|
||||||
|
album.TrackCount = subAlbum.SongCount
|
||||||
|
album.Genres = []string{subAlbum.Genre}
|
||||||
|
album.Favorite = !subAlbum.Starred.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
func toArtist(ar *subsonic.Artist) *mediaprovider.Artist {
|
||||||
|
if ar == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &mediaprovider.Artist{
|
||||||
|
ID: ar.ID,
|
||||||
|
Name: ar.Name,
|
||||||
|
Favorite: !ar.Starred.IsZero(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist {
|
||||||
|
if ar == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &mediaprovider.Artist{
|
||||||
|
ID: ar.ID,
|
||||||
|
Name: ar.Name,
|
||||||
|
Favorite: !ar.Starred.IsZero(),
|
||||||
|
AlbumCount: ar.AlbumCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPlaylist(pl *subsonic.Playlist) *mediaprovider.Playlist {
|
||||||
|
if pl == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
playlist := &mediaprovider.Playlist{}
|
||||||
|
fillPlaylist(pl, playlist)
|
||||||
|
return playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillPlaylist(pl *subsonic.Playlist, playlist *mediaprovider.Playlist) {
|
||||||
|
playlist.Name = pl.Name
|
||||||
|
playlist.ID = pl.ID
|
||||||
|
playlist.CoverArtID = pl.CoverArt
|
||||||
|
playlist.Description = pl.Comment
|
||||||
|
playlist.Owner = pl.Owner
|
||||||
|
playlist.Public = pl.Public
|
||||||
|
playlist.TrackCount = pl.SongCount
|
||||||
|
playlist.Duration = pl.Duration
|
||||||
|
}
|
||||||
@@ -1,62 +1,62 @@
|
|||||||
package backend
|
package subsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *LibraryManager) AllTracksIterator() TrackIterator {
|
func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider.TrackIterator {
|
||||||
return &allTracksIterator{
|
if searchQuery == "" {
|
||||||
l: l,
|
return &allTracksIterator{
|
||||||
albumIter: l.AlbumsIter(AlbumSortArtistAZ, AlbumFilter{}),
|
s: s,
|
||||||
|
albumIter: s.IterateAlbums(AlbumSortArtistAZ, mediaprovider.AlbumFilter{}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LibraryManager) SearchTracksIterator(query string) TrackIterator {
|
|
||||||
return &searchTracksIterator{
|
return &searchTracksIterator{
|
||||||
searchIterBase: searchIterBase{
|
searchIterBase: searchIterBase{
|
||||||
s: l.s.Server,
|
s: s.client,
|
||||||
query: query,
|
query: searchQuery,
|
||||||
},
|
},
|
||||||
trackIDset: make(map[string]bool),
|
trackIDset: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type allTracksIterator struct {
|
type allTracksIterator struct {
|
||||||
l *LibraryManager
|
s *subsonicMediaProvider
|
||||||
albumIter AlbumIterator
|
albumIter mediaprovider.AlbumIterator
|
||||||
curAlbum *subsonic.AlbumID3
|
curAlbum *mediaprovider.AlbumWithTracks
|
||||||
curTrackIdx int
|
curTrackIdx int
|
||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *allTracksIterator) Next() *subsonic.Child {
|
func (a *allTracksIterator) Next() *mediaprovider.Track {
|
||||||
if a.done {
|
if a.done {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch next album
|
// fetch next album
|
||||||
if a.curAlbum == nil || a.curTrackIdx >= len(a.curAlbum.Song) {
|
if a.curAlbum == nil || a.curTrackIdx >= len(a.curAlbum.Tracks) {
|
||||||
al := a.albumIter.Next()
|
al := a.albumIter.Next()
|
||||||
if al == nil {
|
if al == nil {
|
||||||
a.done = true
|
a.done = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
al, err := a.l.s.Server.GetAlbum(al.ID)
|
alWithTracks, err := a.s.GetAlbum(al.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error fetching album: %s", err.Error())
|
log.Printf("error fetching album: %s", err.Error())
|
||||||
}
|
}
|
||||||
if len(al.Song) == 0 {
|
if len(alWithTracks.Tracks) == 0 {
|
||||||
// in the unlikely case of an album with zero tracks,
|
// in the unlikely case of an album with zero tracks,
|
||||||
// just call recursively to move to next album
|
// just call recursively to move to next album
|
||||||
return a.Next()
|
return a.Next()
|
||||||
}
|
}
|
||||||
a.curAlbum = al
|
a.curAlbum = alWithTracks
|
||||||
a.curTrackIdx = 0
|
a.curTrackIdx = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
tr := a.curAlbum.Song[a.curTrackIdx]
|
tr := a.curAlbum.Tracks[a.curTrackIdx]
|
||||||
a.curTrackIdx += 1
|
a.curTrackIdx += 1
|
||||||
return tr
|
return tr
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ type searchTracksIterator struct {
|
|||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *searchTracksIterator) Next() *subsonic.Child {
|
func (s *searchTracksIterator) Next() *mediaprovider.Track {
|
||||||
if s.done {
|
if s.done {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ func (s *searchTracksIterator) Next() *subsonic.Child {
|
|||||||
s.prefetched = s.prefetched[:0]
|
s.prefetched = s.prefetched[:0]
|
||||||
s.prefetchedPos = 0
|
s.prefetchedPos = 0
|
||||||
}
|
}
|
||||||
return tr
|
return toTrack(tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// no more results
|
// no more results
|
||||||
+21
-35
@@ -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] = ©
|
pq[i] = ©
|
||||||
@@ -235,11 +228,7 @@ func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child {
|
|||||||
// this should be called to ensure the in-memory track model is updated.
|
// 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,14 +7,18 @@ 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 {
|
||||||
ServerID uuid.UUID
|
LoggedInUser string
|
||||||
Server *subsonic.Client
|
ServerID uuid.UUID
|
||||||
|
Server mediaprovider.MediaProvider
|
||||||
|
|
||||||
|
prefetchCoverCB func(string)
|
||||||
appName string
|
appName string
|
||||||
onServerConnected []func()
|
onServerConnected []func()
|
||||||
onLogout []func()
|
onLogout []func()
|
||||||
@@ -26,12 +30,21 @@ func NewServerManager(appName string) *ServerManager {
|
|||||||
return &ServerManager{appName: appName}
|
return &ServerManager{appName: appName}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServerManager) SetPrefetchAlbumCoverCallback(cb func(string)) {
|
||||||
|
s.prefetchCoverCB = cb
|
||||||
|
if s.Server != nil {
|
||||||
|
s.Server.SetPrefetchCoverCallback(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error {
|
func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error {
|
||||||
cli, err := s.testConnectionAndCreateClient(conf.ServerConnection, password)
|
cli, err := s.testConnectionAndCreateClient(conf.ServerConnection, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Server = cli
|
s.Server = subsonicMP.SubsonicMediaProvider(cli)
|
||||||
|
s.Server.SetPrefetchCoverCallback(s.prefetchCoverCB)
|
||||||
|
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 +129,7 @@ func (s *ServerManager) Logout() {
|
|||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
s.Server = nil
|
s.Server = nil
|
||||||
|
s.LoggedInUser = ""
|
||||||
s.ServerID = uuid.UUID{}
|
s.ServerID = uuid.UUID{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-32
@@ -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 {
|
||||||
@@ -35,19 +34,17 @@ type AlbumPage struct {
|
|||||||
type albumPageState struct {
|
type albumPageState struct {
|
||||||
albumID string
|
albumID string
|
||||||
cfg *backend.AlbumPageConfig
|
cfg *backend.AlbumPageConfig
|
||||||
lm *backend.LibraryManager
|
mp mediaprovider.MediaProvider
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
sm *backend.ServerManager
|
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAlbumPage(
|
func NewAlbumPage(
|
||||||
albumID string,
|
albumID string,
|
||||||
cfg *backend.AlbumPageConfig,
|
cfg *backend.AlbumPageConfig,
|
||||||
sm *backend.ServerManager,
|
|
||||||
pm *backend.PlaybackManager,
|
pm *backend.PlaybackManager,
|
||||||
lm *backend.LibraryManager,
|
mp mediaprovider.MediaProvider,
|
||||||
im *backend.ImageManager,
|
im *backend.ImageManager,
|
||||||
contr *controller.Controller,
|
contr *controller.Controller,
|
||||||
) *AlbumPage {
|
) *AlbumPage {
|
||||||
@@ -55,9 +52,8 @@ func NewAlbumPage(
|
|||||||
albumPageState: albumPageState{
|
albumPageState: albumPageState{
|
||||||
albumID: albumID,
|
albumID: albumID,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
sm: sm,
|
|
||||||
pm: pm,
|
pm: pm,
|
||||||
lm: lm,
|
mp: mp,
|
||||||
im: im,
|
im: im,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
},
|
},
|
||||||
@@ -92,11 +88,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 +112,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.mp.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 +211,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 +234,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.mp.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,18 +247,18 @@ 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 {
|
||||||
return NewAlbumPage(s.albumID, s.cfg, s.sm, s.pm, s.lm, s.im, s.contr)
|
return NewAlbumPage(s.albumID, s.cfg, s.pm, s.mp, s.im, s.contr)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-17
@@ -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"
|
||||||
@@ -23,13 +24,13 @@ type AlbumsPage struct {
|
|||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
lm *backend.LibraryManager
|
mp mediaprovider.MediaProvider
|
||||||
grid *widgets.GridView
|
grid *widgets.GridView
|
||||||
searchGrid *widgets.GridView
|
searchGrid *widgets.GridView
|
||||||
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
|
||||||
@@ -54,12 +55,12 @@ func (s *selectWidget) MinSize() fyne.Size {
|
|||||||
return fyne.NewSize(170, s.Select.MinSize().Height)
|
return fyne.NewSize(170, s.Select.MinSize().Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager) *AlbumsPage {
|
func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) *AlbumsPage {
|
||||||
a := &AlbumsPage{
|
a := &AlbumsPage{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
pm: pm,
|
pm: pm,
|
||||||
lm: lm,
|
mp: mp,
|
||||||
im: im,
|
im: im,
|
||||||
}
|
}
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
@@ -68,12 +69,12 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller,
|
|||||||
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
||||||
SizeName: theme.SizeNameHeadingText,
|
SizeName: theme.SizeNameHeadingText,
|
||||||
}
|
}
|
||||||
a.sortOrder = NewSelect(backend.AlbumSortOrders, a.onSortOrderChanged)
|
a.sortOrder = NewSelect(mp.AlbumSortOrders(), a.onSortOrderChanged)
|
||||||
if !sharedutil.SliceContains(backend.AlbumSortOrders, cfg.SortOrder) {
|
if !sharedutil.SliceContains(mp.AlbumSortOrders(), cfg.SortOrder) {
|
||||||
cfg.SortOrder = string(backend.AlbumSortRecentlyAdded)
|
cfg.SortOrder = string(mp.AlbumSortOrders()[0])
|
||||||
}
|
}
|
||||||
a.sortOrder.Selected = cfg.SortOrder
|
a.sortOrder.Selected = cfg.SortOrder
|
||||||
iter := lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), a.filter)
|
iter := mp.IterateAlbums(a.sortOrder.Selected, a.filter)
|
||||||
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im)
|
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im)
|
||||||
contr.ConnectAlbumGridActions(a.grid)
|
contr.ConnectAlbumGridActions(a.grid)
|
||||||
a.createSearchAndFilter()
|
a.createSearchAndFilter()
|
||||||
@@ -111,7 +112,7 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
|
|||||||
cfg: saved.cfg,
|
cfg: saved.cfg,
|
||||||
contr: saved.contr,
|
contr: saved.contr,
|
||||||
pm: saved.pm,
|
pm: saved.pm,
|
||||||
lm: saved.lm,
|
mp: saved.mp,
|
||||||
im: saved.im,
|
im: saved.im,
|
||||||
searchText: saved.searchText,
|
searchText: saved.searchText,
|
||||||
filter: saved.filter,
|
filter: saved.filter,
|
||||||
@@ -122,7 +123,7 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage {
|
|||||||
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
||||||
SizeName: theme.SizeNameHeadingText,
|
SizeName: theme.SizeNameHeadingText,
|
||||||
}
|
}
|
||||||
a.sortOrder = NewSelect(backend.AlbumSortOrders, nil)
|
a.sortOrder = NewSelect(a.mp.AlbumSortOrders(), nil)
|
||||||
a.sortOrder.Selected = saved.sortOrder
|
a.sortOrder.Selected = saved.sortOrder
|
||||||
a.sortOrder.OnChanged = a.onSortOrderChanged
|
a.sortOrder.OnChanged = a.onSortOrderChanged
|
||||||
a.grid = widgets.NewGridViewFromState(saved.gridState)
|
a.grid = widgets.NewGridViewFromState(saved.gridState)
|
||||||
@@ -162,7 +163,7 @@ func (a *AlbumsPage) Reload() {
|
|||||||
if a.searchText != "" {
|
if a.searchText != "" {
|
||||||
a.doSearch(a.searchText)
|
a.doSearch(a.searchText)
|
||||||
} else {
|
} else {
|
||||||
iter := a.lm.AlbumsIter(backend.AlbumSortOrder(a.sortOrder.Selected), a.filter)
|
iter := a.mp.IterateAlbums(a.sortOrder.Selected, a.filter)
|
||||||
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
||||||
a.grid.Refresh()
|
a.grid.Refresh()
|
||||||
}
|
}
|
||||||
@@ -173,7 +174,7 @@ func (a *AlbumsPage) Save() SavedPage {
|
|||||||
cfg: a.cfg,
|
cfg: a.cfg,
|
||||||
contr: a.contr,
|
contr: a.contr,
|
||||||
pm: a.pm,
|
pm: a.pm,
|
||||||
lm: a.lm,
|
mp: a.mp,
|
||||||
im: a.im,
|
im: a.im,
|
||||||
searchText: a.searchText,
|
searchText: a.searchText,
|
||||||
filter: a.filter,
|
filter: a.filter,
|
||||||
@@ -187,11 +188,12 @@ func (a *AlbumsPage) Save() SavedPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *AlbumsPage) doSearch(query string) {
|
func (a *AlbumsPage) doSearch(query string) {
|
||||||
|
iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter))
|
||||||
if a.searchGrid == nil {
|
if a.searchGrid == nil {
|
||||||
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(a.lm.SearchIter(query)), a.im)
|
a.searchGrid = widgets.NewGridView(iter, a.im)
|
||||||
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
||||||
} else {
|
} else {
|
||||||
a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(a.lm.SearchIterWithFilter(query, a.filter)))
|
a.searchGrid.Reset(iter)
|
||||||
}
|
}
|
||||||
a.container.Objects[0] = a.searchGrid
|
a.container.Objects[0] = a.searchGrid
|
||||||
a.Refresh()
|
a.Refresh()
|
||||||
@@ -199,7 +201,7 @@ func (a *AlbumsPage) doSearch(query string) {
|
|||||||
|
|
||||||
func (a *AlbumsPage) onSortOrderChanged(order string) {
|
func (a *AlbumsPage) onSortOrderChanged(order string) {
|
||||||
a.cfg.SortOrder = a.sortOrder.Selected
|
a.cfg.SortOrder = a.sortOrder.Selected
|
||||||
iter := a.lm.AlbumsIter(backend.AlbumSortOrder(order), a.filter)
|
iter := a.mp.IterateAlbums(order, a.filter)
|
||||||
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
||||||
if a.searchText == "" {
|
if a.searchText == "" {
|
||||||
a.container.Objects[0] = a.grid
|
a.container.Objects[0] = a.grid
|
||||||
@@ -214,11 +216,11 @@ func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
|
|
||||||
type savedAlbumsPage struct {
|
type savedAlbumsPage struct {
|
||||||
searchText string
|
searchText string
|
||||||
filter backend.AlbumFilter
|
filter mediaprovider.AlbumFilter
|
||||||
cfg *backend.AlbumsPageConfig
|
cfg *backend.AlbumsPageConfig
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
lm *backend.LibraryManager
|
mp mediaprovider.MediaProvider
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
sortOrder string
|
sortOrder string
|
||||||
gridState widgets.GridViewState
|
gridState widgets.GridViewState
|
||||||
|
|||||||
+24
-28
@@ -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"
|
||||||
@@ -19,8 +20,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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ fyne.Widget = (*ArtistPage)(nil)
|
var _ fyne.Widget = (*ArtistPage)(nil)
|
||||||
@@ -31,7 +30,7 @@ type artistPageState struct {
|
|||||||
|
|
||||||
cfg *backend.ArtistPageConfig
|
cfg *backend.ArtistPageConfig
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
sm *backend.ServerManager
|
mp mediaprovider.MediaProvider
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
}
|
}
|
||||||
@@ -41,7 +40,7 @@ type ArtistPage struct {
|
|||||||
|
|
||||||
artistPageState
|
artistPageState
|
||||||
|
|
||||||
artistInfo *subsonic.ArtistID3
|
artistInfo *mediaprovider.ArtistWithAlbums
|
||||||
|
|
||||||
albumGrid *widgets.GridView
|
albumGrid *widgets.GridView
|
||||||
tracklistCtr *fyne.Container
|
tracklistCtr *fyne.Container
|
||||||
@@ -50,20 +49,20 @@ type ArtistPage struct {
|
|||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller) *ArtistPage {
|
func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller) *ArtistPage {
|
||||||
activeView := 0
|
activeView := 0
|
||||||
if cfg.InitialView == "Top Tracks" {
|
if cfg.InitialView == "Top Tracks" {
|
||||||
activeView = 1
|
activeView = 1
|
||||||
}
|
}
|
||||||
return newArtistPage(artistID, cfg, pm, sm, im, contr, activeView)
|
return newArtistPage(artistID, cfg, pm, mp, im, contr, activeView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller, activeView int) *ArtistPage {
|
func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, activeView int) *ArtistPage {
|
||||||
a := &ArtistPage{artistPageState: artistPageState{
|
a := &ArtistPage{artistPageState: artistPageState{
|
||||||
artistID: artistID,
|
artistID: artistID,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
pm: pm,
|
pm: pm,
|
||||||
sm: sm,
|
mp: mp,
|
||||||
im: im,
|
im: im,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
activeView: activeView,
|
activeView: activeView,
|
||||||
@@ -118,7 +117,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)
|
||||||
@@ -129,7 +128,7 @@ func (a *ArtistPage) OnSongChange(track *subsonic.Child, lastScrobbledIfAny *sub
|
|||||||
|
|
||||||
func (a *ArtistPage) playAllTracks() {
|
func (a *ArtistPage) playAllTracks() {
|
||||||
if a.artistInfo != nil { // page loaded
|
if a.artistInfo != nil { // page loaded
|
||||||
for i, album := range a.artistInfo.Album {
|
for i, album := range a.artistInfo.Albums {
|
||||||
a.pm.LoadAlbum(album.ID, i > 0 /*append*/, false /*shuffle*/)
|
a.pm.LoadAlbum(album.ID, i > 0 /*append*/, false /*shuffle*/)
|
||||||
}
|
}
|
||||||
a.pm.PlayFromBeginning()
|
a.pm.PlayFromBeginning()
|
||||||
@@ -142,7 +141,7 @@ func (a *ArtistPage) playArtistRadio() {
|
|||||||
|
|
||||||
// should be called asynchronously
|
// should be called asynchronously
|
||||||
func (a *ArtistPage) load() {
|
func (a *ArtistPage) load() {
|
||||||
artist, err := a.sm.Server.GetArtist(a.artistID)
|
artist, err := a.mp.GetArtist(a.artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get artist: %s", err.Error())
|
log.Printf("Failed to get artist: %s", err.Error())
|
||||||
return
|
return
|
||||||
@@ -154,7 +153,7 @@ func (a *ArtistPage) load() {
|
|||||||
} else {
|
} else {
|
||||||
a.showTopTracks()
|
a.showTopTracks()
|
||||||
}
|
}
|
||||||
info, err := a.sm.Server.GetArtistInfo2(a.artistID, nil)
|
info, err := a.mp.GetArtistInfo(a.artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get artist info: %s", err.Error())
|
log.Printf("Failed to get artist info: %s", err.Error())
|
||||||
}
|
}
|
||||||
@@ -168,11 +167,11 @@ func (a *ArtistPage) showAlbumGrid() {
|
|||||||
a.activeView = 0 // if page still loading, will show discography view first
|
a.activeView = 0 // if page still loading, will show discography view first
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
model := sharedutil.MapSlice(a.artistInfo.Album, func(al *subsonic.AlbumID3) widgets.GridViewItemModel {
|
model := sharedutil.MapSlice(a.artistInfo.Albums, func(al *mediaprovider.Album) widgets.GridViewItemModel {
|
||||||
return widgets.GridViewItemModel{
|
return widgets.GridViewItemModel{
|
||||||
Name: al.Name,
|
Name: al.Name,
|
||||||
ID: al.ID,
|
ID: al.ID,
|
||||||
CoverArtID: al.CoverArt,
|
CoverArtID: al.CoverArtID,
|
||||||
Secondary: strconv.Itoa(al.Year),
|
Secondary: strconv.Itoa(al.Year),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -190,7 +189,7 @@ func (a *ArtistPage) showTopTracks() {
|
|||||||
a.activeView = 1 // if page still loading, will show tracks view first
|
a.activeView = 1 // if page still loading, will show tracks view first
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ts, err := a.sm.Server.GetTopSongs(a.artistInfo.Name, map[string]string{"count": "20"})
|
ts, err := a.mp.GetTopTracks(a.artistInfo.Artist, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error getting top songs: %s", err.Error())
|
log.Printf("error getting top songs: %s", err.Error())
|
||||||
return
|
return
|
||||||
@@ -233,7 +232,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *artistPageState) Restore() Page {
|
func (s *artistPageState) Restore() Page {
|
||||||
return newArtistPage(s.artistID, s.cfg, s.pm, s.sm, s.im, s.contr, s.activeView)
|
return newArtistPage(s.artistID, s.cfg, s.pm, s.mp, s.im, s.contr, s.activeView)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArtistPageHeader struct {
|
type ArtistPageHeader struct {
|
||||||
@@ -276,18 +275,18 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistPageHeader) Update(artist *subsonic.ArtistID3) {
|
func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums) {
|
||||||
if artist == nil {
|
if artist == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.favoriteBtn.IsFavorited = !artist.Starred.IsZero()
|
a.favoriteBtn.IsFavorited = !artist.Favorite
|
||||||
a.favoriteBtn.Refresh()
|
a.favoriteBtn.Refresh()
|
||||||
a.artistID = artist.ID
|
a.artistID = artist.ID
|
||||||
a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name
|
a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name
|
||||||
a.titleDisp.Refresh()
|
a.titleDisp.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) {
|
||||||
if info == nil {
|
if info == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -303,7 +302,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.similarArtists.RemoveAll()
|
a.similarArtists.RemoveAll()
|
||||||
for i, art := range info.SimilarArtist {
|
for i, art := range info.SimilarArtists {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
a.similarArtists.Add(widget.NewLabel("Similar Artists:"))
|
a.similarArtists.Add(widget.NewLabel("Similar Artists:"))
|
||||||
}
|
}
|
||||||
@@ -320,11 +319,11 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
|||||||
}
|
}
|
||||||
a.similarArtists.Refresh()
|
a.similarArtists.Refresh()
|
||||||
|
|
||||||
if info.LargeImageUrl != "" {
|
if info.ImageURL != "" {
|
||||||
if a.artistImage.HaveImage() {
|
if a.artistImage.HaveImage() {
|
||||||
_ = a.artistPage.im.RefreshCachedArtistImageIfExpired(a.artistID, info.LargeImageUrl)
|
_ = a.artistPage.im.RefreshCachedArtistImageIfExpired(a.artistID, info.ImageURL)
|
||||||
} else {
|
} else {
|
||||||
im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.LargeImageUrl)
|
im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.ImageURL)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
a.artistImage.SetImage(im, true /*tappable*/)
|
a.artistImage.SetImage(im, true /*tappable*/)
|
||||||
}
|
}
|
||||||
@@ -333,11 +332,8 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistPageHeader) toggleFavorited() {
|
func (a *ArtistPageHeader) toggleFavorited() {
|
||||||
if a.favoriteBtn.IsFavorited {
|
params := mediaprovider.RatingFavoriteParameters{ArtistIDs: []string{a.artistID}}
|
||||||
a.artistPage.sm.Server.Star(subsonic.StarParameters{ArtistIDs: []string{a.artistID}})
|
a.artistPage.mp.SetFavorite(params, a.favoriteBtn.IsFavorited)
|
||||||
} else {
|
|
||||||
a.artistPage.sm.Server.Unstar(subsonic.StarParameters{ArtistIDs: []string{a.artistID}})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistPageHeader) createContainer() {
|
func (a *ArtistPageHeader) createContainer() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ package browsing
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
||||||
@@ -16,8 +15,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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ fyne.Widget = (*ArtistPage)(nil)
|
var _ fyne.Widget = (*ArtistPage)(nil)
|
||||||
@@ -27,7 +24,7 @@ type ArtistsGenresPage struct {
|
|||||||
|
|
||||||
isGenresPage bool
|
isGenresPage bool
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
sm *backend.ServerManager
|
mp mediaprovider.MediaProvider
|
||||||
model []widgets.ArtistGenreListItemModel
|
model []widgets.ArtistGenreListItemModel
|
||||||
list *widgets.ArtistGenreList
|
list *widgets.ArtistGenreList
|
||||||
|
|
||||||
@@ -36,11 +33,11 @@ type ArtistsGenresPage struct {
|
|||||||
searcher *widgets.SearchEntry
|
searcher *widgets.SearchEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager) *ArtistsGenresPage {
|
func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, mp mediaprovider.MediaProvider) *ArtistsGenresPage {
|
||||||
return newArtistsGenresPage(isGenresPage, contr, sm, "")
|
return newArtistsGenresPage(isGenresPage, contr, mp, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager, searchText string) *ArtistsGenresPage {
|
func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, mp mediaprovider.MediaProvider, searchText string) *ArtistsGenresPage {
|
||||||
title := "Artists"
|
title := "Artists"
|
||||||
if isGenresPage {
|
if isGenresPage {
|
||||||
title = "Genres"
|
title = "Genres"
|
||||||
@@ -48,7 +45,7 @@ func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *b
|
|||||||
a := &ArtistsGenresPage{
|
a := &ArtistsGenresPage{
|
||||||
isGenresPage: isGenresPage,
|
isGenresPage: isGenresPage,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
sm: sm,
|
mp: mp,
|
||||||
titleDisp: widget.NewRichTextWithText(title),
|
titleDisp: widget.NewRichTextWithText(title),
|
||||||
}
|
}
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
@@ -74,13 +71,13 @@ func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *b
|
|||||||
// should be called asynchronously
|
// should be called asynchronously
|
||||||
func (a *ArtistsGenresPage) load(searchOnLoad bool) {
|
func (a *ArtistsGenresPage) load(searchOnLoad bool) {
|
||||||
if a.isGenresPage {
|
if a.isGenresPage {
|
||||||
genres, err := a.sm.Server.GetGenres()
|
genres, err := a.mp.GetGenres()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error loading genres: %v", err.Error())
|
log.Printf("error loading genres: %v", err.Error())
|
||||||
}
|
}
|
||||||
a.model = a.buildGenresListModel(genres)
|
a.model = a.buildGenresListModel(genres)
|
||||||
} else {
|
} else {
|
||||||
artists, err := a.sm.Server.GetArtists(nil)
|
artists, err := a.mp.GetArtists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error loading artists: %v", err.Error())
|
log.Printf("error loading artists: %v", err.Error())
|
||||||
}
|
}
|
||||||
@@ -129,7 +126,7 @@ func (a *ArtistsGenresPage) Save() SavedPage {
|
|||||||
return &savedArtistsGenresPage{
|
return &savedArtistsGenresPage{
|
||||||
isGenresPage: a.isGenresPage,
|
isGenresPage: a.isGenresPage,
|
||||||
contr: a.contr,
|
contr: a.contr,
|
||||||
sm: a.sm,
|
mp: a.mp,
|
||||||
searchText: a.searcher.Entry.Text,
|
searchText: a.searcher.Entry.Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,37 +134,35 @@ func (a *ArtistsGenresPage) Save() SavedPage {
|
|||||||
type savedArtistsGenresPage struct {
|
type savedArtistsGenresPage struct {
|
||||||
isGenresPage bool
|
isGenresPage bool
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
sm *backend.ServerManager
|
mp mediaprovider.MediaProvider
|
||||||
searchText string
|
searchText string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *savedArtistsGenresPage) Restore() Page {
|
func (s *savedArtistsGenresPage) Restore() Page {
|
||||||
return newArtistsGenresPage(s.isGenresPage, s.contr, s.sm, s.searchText)
|
return newArtistsGenresPage(s.isGenresPage, s.contr, s.mp, s.searchText)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsGenresPage) buildArtistListModel(artists *subsonic.ArtistsID3) []widgets.ArtistGenreListItemModel {
|
func (a *ArtistsGenresPage) buildArtistListModel(artists []*mediaprovider.Artist) []widgets.ArtistGenreListItemModel {
|
||||||
model := make([]widgets.ArtistGenreListItemModel, 0)
|
model := make([]widgets.ArtistGenreListItemModel, 0)
|
||||||
for _, idx := range artists.Index {
|
for _, artist := range artists {
|
||||||
for _, artist := range idx.Artist {
|
model = append(model, widgets.ArtistGenreListItemModel{
|
||||||
model = append(model, widgets.ArtistGenreListItemModel{
|
ID: artist.ID,
|
||||||
ID: artist.ID,
|
Name: artist.Name,
|
||||||
Name: artist.Name,
|
AlbumCount: artist.AlbumCount,
|
||||||
AlbumCount: artist.AlbumCount,
|
Favorite: artist.Favorite,
|
||||||
Favorite: artist.Starred != time.Time{},
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArtistsGenresPage) buildGenresListModel(genres []*subsonic.Genre) []widgets.ArtistGenreListItemModel {
|
func (a *ArtistsGenresPage) buildGenresListModel(genres []*mediaprovider.Genre) []widgets.ArtistGenreListItemModel {
|
||||||
model := make([]widgets.ArtistGenreListItemModel, 0)
|
model := make([]widgets.ArtistGenreListItemModel, 0)
|
||||||
for _, genre := range genres {
|
for _, genre := range genres {
|
||||||
model = append(model, widgets.ArtistGenreListItemModel{
|
model = append(model, widgets.ArtistGenreListItemModel{
|
||||||
ID: genre.Name,
|
ID: genre.Name,
|
||||||
Name: genre.Name,
|
Name: genre.Name,
|
||||||
AlbumCount: genre.AlbumCount,
|
AlbumCount: genre.AlbumCount,
|
||||||
TrackCount: genre.SongCount,
|
TrackCount: genre.TrackCount,
|
||||||
Favorite: false,
|
Favorite: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -26,10 +25,9 @@ type FavoritesPage struct {
|
|||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
sm *backend.ServerManager
|
mp mediaprovider.MediaProvider
|
||||||
lm *backend.LibraryManager
|
|
||||||
|
|
||||||
filter backend.AlbumFilter
|
filter mediaprovider.AlbumFilter
|
||||||
searchText string
|
searchText string
|
||||||
nowPlayingID string
|
nowPlayingID string
|
||||||
pendingViewSwitch bool
|
pendingViewSwitch bool
|
||||||
@@ -45,19 +43,18 @@ type FavoritesPage struct {
|
|||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Controller, sm *backend.ServerManager, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager) *FavoritesPage {
|
func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage {
|
||||||
a := &FavoritesPage{
|
a := &FavoritesPage{
|
||||||
filter: backend.AlbumFilter{ExcludeUnfavorited: true},
|
filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true},
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
pm: pm,
|
pm: pm,
|
||||||
lm: lm,
|
mp: mp,
|
||||||
sm: sm,
|
|
||||||
im: im,
|
im: im,
|
||||||
}
|
}
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
a.createHeader(0)
|
a.createHeader(0)
|
||||||
iter := lm.StarredIter(a.filter)
|
iter := mp.IterateAlbums("", a.filter)
|
||||||
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
|
a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
|
||||||
a.contr.ConnectAlbumGridActions(a.grid)
|
a.contr.ConnectAlbumGridActions(a.grid)
|
||||||
if cfg.InitialView == "Artists" {
|
if cfg.InitialView == "Artists" {
|
||||||
@@ -101,8 +98,7 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage {
|
|||||||
cfg: saved.cfg,
|
cfg: saved.cfg,
|
||||||
contr: saved.contr,
|
contr: saved.contr,
|
||||||
pm: saved.pm,
|
pm: saved.pm,
|
||||||
lm: saved.lm,
|
mp: saved.mp,
|
||||||
sm: saved.sm,
|
|
||||||
im: saved.im,
|
im: saved.im,
|
||||||
searchText: saved.searchText,
|
searchText: saved.searchText,
|
||||||
filter: saved.filter,
|
filter: saved.filter,
|
||||||
@@ -143,13 +139,13 @@ func (a *FavoritesPage) Reload() {
|
|||||||
if a.searchText != "" {
|
if a.searchText != "" {
|
||||||
a.doSearchAlbums(a.searchText)
|
a.doSearchAlbums(a.searchText)
|
||||||
} else {
|
} else {
|
||||||
iter := a.lm.StarredIter(a.filter)
|
iter := a.mp.IterateAlbums("", a.filter)
|
||||||
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
a.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
||||||
}
|
}
|
||||||
if a.tracklistCtr != nil || a.artistListCtr != nil {
|
if a.tracklistCtr != nil || a.artistListCtr != nil {
|
||||||
go func() {
|
go func() {
|
||||||
// re-fetch starred info from server
|
// re-fetch starred info from server
|
||||||
starred, err := a.sm.Server.GetStarred2(nil)
|
starred, err := a.mp.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
|
||||||
@@ -157,7 +153,7 @@ func (a *FavoritesPage) Reload() {
|
|||||||
if a.tracklistCtr != nil {
|
if a.tracklistCtr != nil {
|
||||||
// refresh favorite songs view
|
// refresh favorite songs view
|
||||||
tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist)
|
tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist)
|
||||||
tr.Tracks = starred.Song
|
tr.Tracks = starred.Tracks
|
||||||
if a.toggleBtns.ActivatedButtonIndex() == 2 {
|
if a.toggleBtns.ActivatedButtonIndex() == 2 {
|
||||||
// favorite songs view is visible
|
// favorite songs view is visible
|
||||||
tr.Refresh()
|
tr.Refresh()
|
||||||
@@ -166,7 +162,7 @@ func (a *FavoritesPage) Reload() {
|
|||||||
if a.artistListCtr != nil {
|
if a.artistListCtr != nil {
|
||||||
// refresh favorite artists view
|
// refresh favorite artists view
|
||||||
al := a.artistListCtr.Objects[0].(*widgets.ArtistGenreList)
|
al := a.artistListCtr.Objects[0].(*widgets.ArtistGenreList)
|
||||||
al.Items = buildArtistListModel(starred.Artist)
|
al.Items = buildArtistListModel(starred.Artists)
|
||||||
if a.toggleBtns.ActivatedButtonIndex() == 1 {
|
if a.toggleBtns.ActivatedButtonIndex() == 1 {
|
||||||
// favorite artists view is visible
|
// favorite artists view is visible
|
||||||
al.Refresh()
|
al.Refresh()
|
||||||
@@ -181,9 +177,8 @@ func (a *FavoritesPage) Save() SavedPage {
|
|||||||
cfg: a.cfg,
|
cfg: a.cfg,
|
||||||
contr: a.contr,
|
contr: a.contr,
|
||||||
pm: a.pm,
|
pm: a.pm,
|
||||||
sm: a.sm,
|
mp: a.mp,
|
||||||
im: a.im,
|
im: a.im,
|
||||||
lm: a.lm,
|
|
||||||
filter: a.filter,
|
filter: a.filter,
|
||||||
searchText: a.searchText,
|
searchText: a.searchText,
|
||||||
gridState: a.grid.SaveToState(),
|
gridState: a.grid.SaveToState(),
|
||||||
@@ -216,7 +211,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
|
||||||
@@ -235,7 +230,7 @@ func (a *FavoritesPage) SelectAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *FavoritesPage) doSearchAlbums(query string) {
|
func (a *FavoritesPage) doSearchAlbums(query string) {
|
||||||
iter := a.lm.SearchIterWithFilter(query, a.filter)
|
iter := a.mp.SearchAlbums(query, a.filter)
|
||||||
if a.searchGrid == nil {
|
if a.searchGrid == nil {
|
||||||
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
|
a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im)
|
||||||
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
a.contr.ConnectAlbumGridActions(a.searchGrid)
|
||||||
@@ -271,12 +266,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.mp.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 +290,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 +315,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.mp.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) {
|
||||||
@@ -355,12 +350,11 @@ type savedFavoritesPage struct {
|
|||||||
cfg *backend.FavoritesPageConfig
|
cfg *backend.FavoritesPageConfig
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
sm *backend.ServerManager
|
mp mediaprovider.MediaProvider
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-12
@@ -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"
|
||||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
@@ -22,12 +23,12 @@ type GenrePage struct {
|
|||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
lm *backend.LibraryManager
|
mp mediaprovider.MediaProvider
|
||||||
grid *widgets.GridView
|
grid *widgets.GridView
|
||||||
searchGrid *widgets.GridView
|
searchGrid *widgets.GridView
|
||||||
searcher *widgets.SearchEntry
|
searcher *widgets.SearchEntry
|
||||||
searchText string
|
searchText string
|
||||||
filter backend.AlbumFilter
|
filter mediaprovider.AlbumFilter
|
||||||
filterBtn *widgets.AlbumFilterButton
|
filterBtn *widgets.AlbumFilterButton
|
||||||
titleDisp *widget.RichText
|
titleDisp *widget.RichText
|
||||||
playRandom *widget.Button
|
playRandom *widget.Button
|
||||||
@@ -37,13 +38,13 @@ type GenrePage struct {
|
|||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGenrePage(genre string, contr *controller.Controller, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager) *GenrePage {
|
func NewGenrePage(genre string, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) *GenrePage {
|
||||||
g := &GenrePage{
|
g := &GenrePage{
|
||||||
genre: genre,
|
genre: genre,
|
||||||
filter: backend.AlbumFilter{Genres: []string{genre}},
|
filter: mediaprovider.AlbumFilter{Genres: []string{genre}},
|
||||||
contr: contr,
|
contr: contr,
|
||||||
pm: pm,
|
pm: pm,
|
||||||
lm: lm,
|
mp: mp,
|
||||||
im: im,
|
im: im,
|
||||||
}
|
}
|
||||||
g.ExtendBaseWidget(g)
|
g.ExtendBaseWidget(g)
|
||||||
@@ -53,7 +54,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba
|
|||||||
SizeName: theme.SizeNameHeadingText,
|
SizeName: theme.SizeNameHeadingText,
|
||||||
}
|
}
|
||||||
g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs)
|
g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs)
|
||||||
iter := g.lm.GenreIter(g.genre, g.filter)
|
iter := g.mp.IterateAlbums("", g.filter)
|
||||||
g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
||||||
g.contr.ConnectAlbumGridActions(g.grid)
|
g.contr.ConnectAlbumGridActions(g.grid)
|
||||||
g.createSearchAndFilter()
|
g.createSearchAndFilter()
|
||||||
@@ -90,7 +91,7 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage {
|
|||||||
genre: saved.genre,
|
genre: saved.genre,
|
||||||
contr: saved.contr,
|
contr: saved.contr,
|
||||||
pm: saved.pm,
|
pm: saved.pm,
|
||||||
lm: saved.lm,
|
mp: saved.mp,
|
||||||
im: saved.im,
|
im: saved.im,
|
||||||
searchText: saved.searchText,
|
searchText: saved.searchText,
|
||||||
filter: saved.filter,
|
filter: saved.filter,
|
||||||
@@ -124,7 +125,7 @@ func (g *GenrePage) Reload() {
|
|||||||
if g.searchText != "" {
|
if g.searchText != "" {
|
||||||
g.doSearch(g.searchText)
|
g.doSearch(g.searchText)
|
||||||
} else {
|
} else {
|
||||||
iter := g.lm.GenreIter(g.genre, g.filter)
|
iter := g.mp.IterateAlbums("", g.filter)
|
||||||
g.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
g.grid.Reset(widgets.NewGridViewAlbumIterator(iter))
|
||||||
g.grid.Refresh()
|
g.grid.Refresh()
|
||||||
}
|
}
|
||||||
@@ -137,7 +138,7 @@ func (g *GenrePage) Save() SavedPage {
|
|||||||
searchText: g.searchText,
|
searchText: g.searchText,
|
||||||
contr: g.contr,
|
contr: g.contr,
|
||||||
pm: g.pm,
|
pm: g.pm,
|
||||||
lm: g.lm,
|
mp: g.mp,
|
||||||
im: g.im,
|
im: g.im,
|
||||||
gridState: g.grid.SaveToState(),
|
gridState: g.grid.SaveToState(),
|
||||||
}
|
}
|
||||||
@@ -167,7 +168,7 @@ func (g *GenrePage) OnSearched(query string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GenrePage) doSearch(query string) {
|
func (g *GenrePage) doSearch(query string) {
|
||||||
iter := g.lm.SearchIterWithFilter(query, g.filter)
|
iter := g.mp.SearchAlbums(query, g.filter)
|
||||||
if g.searchGrid == nil {
|
if g.searchGrid == nil {
|
||||||
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im)
|
||||||
g.contr.ConnectAlbumGridActions(g.searchGrid)
|
g.contr.ConnectAlbumGridActions(g.searchGrid)
|
||||||
@@ -185,10 +186,10 @@ func (g *GenrePage) playRandomSongs() {
|
|||||||
type savedGenrePage struct {
|
type savedGenrePage struct {
|
||||||
genre string
|
genre string
|
||||||
searchText string
|
searchText string
|
||||||
filter backend.AlbumFilter
|
filter mediaprovider.AlbumFilter
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
lm *backend.LibraryManager
|
mp mediaprovider.MediaProvider
|
||||||
im *backend.ImageManager
|
im *backend.ImageManager
|
||||||
gridState widgets.GridViewState
|
gridState widgets.GridViewState
|
||||||
searchGridState widgets.GridViewState
|
searchGridState widgets.GridViewState
|
||||||
|
|||||||
@@ -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/layouts"
|
"github.com/dweymouth/supersonic/ui/layouts"
|
||||||
@@ -10,8 +11,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 NowPlayingPage struct {
|
type NowPlayingPage struct {
|
||||||
@@ -28,7 +27,6 @@ type NowPlayingPage struct {
|
|||||||
type nowPlayingPageState struct {
|
type nowPlayingPageState struct {
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
conf *backend.NowPlayingPageConfig
|
conf *backend.NowPlayingPageConfig
|
||||||
sm *backend.ServerManager
|
|
||||||
pm *backend.PlaybackManager
|
pm *backend.PlaybackManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +34,9 @@ func NewNowPlayingPage(
|
|||||||
highlightedTrackID string,
|
highlightedTrackID string,
|
||||||
contr *controller.Controller,
|
contr *controller.Controller,
|
||||||
conf *backend.NowPlayingPageConfig,
|
conf *backend.NowPlayingPageConfig,
|
||||||
sm *backend.ServerManager,
|
|
||||||
pm *backend.PlaybackManager,
|
pm *backend.PlaybackManager,
|
||||||
) *NowPlayingPage {
|
) *NowPlayingPage {
|
||||||
a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{contr: contr, conf: conf, sm: sm, pm: pm}}
|
a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{contr: contr, conf: conf, pm: pm}}
|
||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
a.tracklist = widgets.NewTracklist(nil)
|
a.tracklist = widgets.NewTracklist(nil)
|
||||||
a.tracklist.SetVisibleColumns(conf.TracklistColumns)
|
a.tracklist.SetVisibleColumns(conf.TracklistColumns)
|
||||||
@@ -83,7 +80,7 @@ func (a *NowPlayingPage) SelectAll() {
|
|||||||
a.tracklist.SelectAll()
|
a.tracklist.SelectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *NowPlayingPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) {
|
func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) {
|
||||||
if song == nil {
|
if song == nil {
|
||||||
a.nowPlayingID = ""
|
a.nowPlayingID = ""
|
||||||
} else {
|
} else {
|
||||||
@@ -118,5 +115,5 @@ func (a *NowPlayingPage) load(highlightedTrackID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *nowPlayingPageState) Restore() Page {
|
func (s *nowPlayingPageState) Restore() Page {
|
||||||
return NewNowPlayingPage("", s.contr, s.conf, s.sm, s.pm)
|
return NewNowPlayingPage("", s.contr, s.conf, s.pm)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-20
@@ -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/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"
|
||||||
@@ -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 PlaylistPage struct {
|
type PlaylistPage struct {
|
||||||
@@ -90,7 +89,7 @@ func (a *PlaylistPage) Route() controller.Route {
|
|||||||
return controller.PlaylistRoute(a.playlistID)
|
return controller.PlaylistRoute(a.playlistID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) {
|
func (a *PlaylistPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) {
|
||||||
if song == nil {
|
if song == nil {
|
||||||
a.nowPlayingID = ""
|
a.nowPlayingID = ""
|
||||||
} else {
|
} else {
|
||||||
@@ -119,7 +118,7 @@ func (a *PlaylistPage) load() {
|
|||||||
log.Printf("Failed to get playlist: %s", err.Error())
|
log.Printf("Failed to get playlist: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.tracklist.Tracks = playlist.Entry
|
a.tracklist.Tracks = playlist.Tracks
|
||||||
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
a.tracklist.SetNowPlaying(a.nowPlayingID)
|
||||||
a.tracklist.Refresh()
|
a.tracklist.Refresh()
|
||||||
a.header.Update(playlist)
|
a.header.Update(playlist)
|
||||||
@@ -148,10 +147,7 @@ func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) {
|
|||||||
for i, tr := range newTracks {
|
for i, tr := range newTracks {
|
||||||
ids[i] = tr.ID
|
ids[i] = tr.ID
|
||||||
}
|
}
|
||||||
err := a.sm.Server.CreatePlaylistWithTracks(ids, map[string]string{
|
if err := a.sm.Server.ReplacePlaylistTracks(a.playlistID, ids); err != nil {
|
||||||
"playlistId": a.playlistID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error updating playlist: %s", err.Error())
|
log.Printf("error updating playlist: %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
a.tracklist.Tracks = newTracks
|
a.tracklist.Tracks = newTracks
|
||||||
@@ -161,7 +157,7 @@ func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPage) onRemoveSelectedFromPlaylist() {
|
func (a *PlaylistPage) onRemoveSelectedFromPlaylist() {
|
||||||
a.sm.Server.UpdatePlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes())
|
a.sm.Server.EditPlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes())
|
||||||
a.tracklist.UnselectAll()
|
a.tracklist.UnselectAll()
|
||||||
go a.Reload()
|
go a.Reload()
|
||||||
}
|
}
|
||||||
@@ -170,7 +166,7 @@ type PlaylistPageHeader struct {
|
|||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
page *PlaylistPage
|
page *PlaylistPage
|
||||||
playlistInfo *subsonic.Playlist
|
playlistInfo *mediaprovider.PlaylistWithTracks
|
||||||
image *widgets.ImagePlaceholder
|
image *widgets.ImagePlaceholder
|
||||||
|
|
||||||
editButton *widget.Button
|
editButton *widget.Button
|
||||||
@@ -199,7 +195,7 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader {
|
|||||||
a.trackTimeLabel = widget.NewLabel("")
|
a.trackTimeLabel = widget.NewLabel("")
|
||||||
a.editButton = widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func() {
|
a.editButton = widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func() {
|
||||||
if a.playlistInfo != nil {
|
if a.playlistInfo != nil {
|
||||||
page.contr.DoEditPlaylistWorkflow(a.playlistInfo)
|
page.contr.DoEditPlaylistWorkflow(&a.playlistInfo.Playlist)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
a.editButton.Hidden = true
|
a.editButton.Hidden = true
|
||||||
@@ -244,18 +240,18 @@ func (a *PlaylistPageHeader) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
return widget.NewSimpleRenderer(a.container)
|
return widget.NewSimpleRenderer(a.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPageHeader) Update(playlist *subsonic.Playlist) {
|
func (a *PlaylistPageHeader) Update(playlist *mediaprovider.PlaylistWithTracks) {
|
||||||
a.playlistInfo = playlist
|
a.playlistInfo = playlist
|
||||||
a.editButton.Hidden = playlist.Owner != a.page.sm.Server.User
|
a.editButton.Hidden = playlist.Owner != a.page.sm.LoggedInUser
|
||||||
a.titleLabel.Segments[0].(*widget.TextSegment).Text = playlist.Name
|
a.titleLabel.Segments[0].(*widget.TextSegment).Text = playlist.Name
|
||||||
a.descriptionLabel.SetText(playlist.Comment)
|
a.descriptionLabel.SetText(playlist.Description)
|
||||||
a.ownerLabel.SetText(a.formatPlaylistOwnerStr(playlist))
|
a.ownerLabel.SetText(a.formatPlaylistOwnerStr(playlist))
|
||||||
a.trackTimeLabel.SetText(a.formatPlaylistTrackTimeStr(playlist))
|
a.trackTimeLabel.SetText(a.formatPlaylistTrackTimeStr(playlist))
|
||||||
a.createdAtLabel.SetText("created at TODO")
|
a.createdAtLabel.SetText("created at TODO")
|
||||||
|
|
||||||
var haveCover bool
|
var haveCover bool
|
||||||
if playlist.CoverArt != "" {
|
if playlist.CoverArtID != "" {
|
||||||
if im, err := a.page.im.GetCoverThumbnail(playlist.CoverArt); err == nil && im != nil {
|
if im, err := a.page.im.GetCoverThumbnail(playlist.CoverArtID); err == nil && im != nil {
|
||||||
a.image.SetImage(im, false /*tappable*/)
|
a.image.SetImage(im, false /*tappable*/)
|
||||||
haveCover = true
|
haveCover = true
|
||||||
}
|
}
|
||||||
@@ -268,7 +264,7 @@ func (a *PlaylistPageHeader) Update(playlist *subsonic.Playlist) {
|
|||||||
a.Refresh()
|
a.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *subsonic.Playlist) string {
|
func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *mediaprovider.PlaylistWithTracks) string {
|
||||||
pubPriv := "Public"
|
pubPriv := "Public"
|
||||||
if !p.Public {
|
if !p.Public {
|
||||||
pubPriv = "Private"
|
pubPriv = "Private"
|
||||||
@@ -276,12 +272,12 @@ func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *subsonic.Playlist) string
|
|||||||
return fmt.Sprintf("%s playlist by %s", pubPriv, p.Owner)
|
return fmt.Sprintf("%s playlist by %s", pubPriv, p.Owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *subsonic.Playlist) string {
|
func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *mediaprovider.PlaylistWithTracks) string {
|
||||||
tracks := "tracks"
|
tracks := "tracks"
|
||||||
if p.SongCount == 1 {
|
if p.TrackCount == 1 {
|
||||||
tracks = "track"
|
tracks = "track"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%d %s, %s", p.SongCount, tracks, util.SecondsToTimeString(float64(p.Duration)))
|
return fmt.Sprintf("%d %s, %s", p.TrackCount, tracks, util.SecondsToTimeString(float64(p.Duration)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playlistPageState) Restore() Page {
|
func (s *playlistPageState) Restore() Page {
|
||||||
|
|||||||
@@ -7,6 +7,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"
|
||||||
@@ -18,8 +19,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 PlaylistsPage struct {
|
type PlaylistsPage struct {
|
||||||
@@ -27,9 +26,9 @@ type PlaylistsPage struct {
|
|||||||
|
|
||||||
cfg *backend.PlaylistsPageConfig
|
cfg *backend.PlaylistsPageConfig
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
sm *backend.ServerManager
|
mp mediaprovider.MediaProvider
|
||||||
playlists []*subsonic.Playlist
|
playlists []*mediaprovider.Playlist
|
||||||
searchedPlaylists []*subsonic.Playlist
|
searchedPlaylists []*mediaprovider.Playlist
|
||||||
|
|
||||||
viewToggle *widgets.ToggleButtonGroup
|
viewToggle *widgets.ToggleButtonGroup
|
||||||
searcher *widgets.SearchEntry
|
searcher *widgets.SearchEntry
|
||||||
@@ -39,18 +38,18 @@ type PlaylistsPage struct {
|
|||||||
gridView *widgets.GridView
|
gridView *widgets.GridView
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, sm *backend.ServerManager) *PlaylistsPage {
|
func NewPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider) *PlaylistsPage {
|
||||||
activeView := 0
|
activeView := 0
|
||||||
if cfg.InitialView == "Grid" {
|
if cfg.InitialView == "Grid" {
|
||||||
activeView = 1
|
activeView = 1
|
||||||
}
|
}
|
||||||
return newPlaylistsPage(contr, cfg, sm, "", activeView)
|
return newPlaylistsPage(contr, cfg, mp, "", activeView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, sm *backend.ServerManager, searchText string, activeView int) *PlaylistsPage {
|
func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageConfig, mp mediaprovider.MediaProvider, searchText string, activeView int) *PlaylistsPage {
|
||||||
a := &PlaylistsPage{
|
a := &PlaylistsPage{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
sm: sm,
|
mp: mp,
|
||||||
contr: contr,
|
contr: contr,
|
||||||
titleDisp: widget.NewRichTextWithText("Playlists"),
|
titleDisp: widget.NewRichTextWithText("Playlists"),
|
||||||
}
|
}
|
||||||
@@ -76,7 +75,7 @@ func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) load(searchOnLoad bool) {
|
func (a *PlaylistsPage) load(searchOnLoad bool) {
|
||||||
playlists, err := a.sm.Server.GetPlaylists(nil)
|
playlists, err := a.mp.GetPlaylists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error loading playlists: %v", err.Error())
|
log.Printf("error loading playlists: %v", err.Error())
|
||||||
}
|
}
|
||||||
@@ -93,7 +92,7 @@ func (a *PlaylistsPage) createListView() {
|
|||||||
a.listView.OnNavTo = a.showPlaylistPage
|
a.listView.OnNavTo = a.showPlaylistPage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) createGridView(playlists []*subsonic.Playlist) {
|
func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) {
|
||||||
model := createPlaylistGridViewModel(playlists)
|
model := createPlaylistGridViewModel(playlists)
|
||||||
a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager)
|
a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager)
|
||||||
a.gridView.OnPlay = func(id string, shuffle bool) {
|
a.gridView.OnPlay = func(id string, shuffle bool) {
|
||||||
@@ -110,7 +109,7 @@ func (a *PlaylistsPage) createGridView(playlists []*subsonic.Playlist) {
|
|||||||
log.Printf("error loading playlist: %s", err.Error())
|
log.Printf("error loading playlist: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.contr.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(pl.Entry))
|
a.contr.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(pl.Tracks))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,17 +141,17 @@ func (a *PlaylistsPage) showGridView() {
|
|||||||
a.container.Objects[0].Refresh()
|
a.container.Objects[0].Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPlaylistGridViewModel(playlists []*subsonic.Playlist) []widgets.GridViewItemModel {
|
func createPlaylistGridViewModel(playlists []*mediaprovider.Playlist) []widgets.GridViewItemModel {
|
||||||
return sharedutil.MapSlice(playlists, func(pl *subsonic.Playlist) widgets.GridViewItemModel {
|
return sharedutil.MapSlice(playlists, func(pl *mediaprovider.Playlist) widgets.GridViewItemModel {
|
||||||
tracks := "tracks"
|
tracks := "tracks"
|
||||||
if pl.SongCount == 1 {
|
if pl.TrackCount == 1 {
|
||||||
tracks = "track"
|
tracks = "track"
|
||||||
}
|
}
|
||||||
return widgets.GridViewItemModel{
|
return widgets.GridViewItemModel{
|
||||||
Name: pl.Name,
|
Name: pl.Name,
|
||||||
ID: pl.ID,
|
ID: pl.ID,
|
||||||
CoverArtID: pl.CoverArt,
|
CoverArtID: pl.CoverArtID,
|
||||||
Secondary: fmt.Sprintf("%d %s", pl.SongCount, tracks),
|
Secondary: fmt.Sprintf("%d %s", pl.TrackCount, tracks),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -164,15 +163,15 @@ func (a *PlaylistsPage) showPlaylistPage(id string) {
|
|||||||
func (a *PlaylistsPage) onSearched(query string) {
|
func (a *PlaylistsPage) onSearched(query string) {
|
||||||
// since the playlist list is returned in full non-paginated, we will do our own
|
// since the playlist list is returned in full non-paginated, we will do our own
|
||||||
// simple search based on the name, description, and owner, rather than calling a server API
|
// simple search based on the name, description, and owner, rather than calling a server API
|
||||||
var playlists []*subsonic.Playlist
|
var playlists []*mediaprovider.Playlist
|
||||||
if query == "" {
|
if query == "" {
|
||||||
a.searchedPlaylists = nil
|
a.searchedPlaylists = nil
|
||||||
playlists = a.playlists
|
playlists = a.playlists
|
||||||
} else {
|
} else {
|
||||||
a.searchedPlaylists = sharedutil.FilterSlice(a.playlists, func(p *subsonic.Playlist) bool {
|
a.searchedPlaylists = sharedutil.FilterSlice(a.playlists, func(p *mediaprovider.Playlist) bool {
|
||||||
qLower := strings.ToLower(query)
|
qLower := strings.ToLower(query)
|
||||||
return strings.Contains(strings.ToLower(p.Name), qLower) ||
|
return strings.Contains(strings.ToLower(p.Name), qLower) ||
|
||||||
strings.Contains(strings.ToLower(p.Comment), qLower) ||
|
strings.Contains(strings.ToLower(p.Description), qLower) ||
|
||||||
strings.Contains(strings.ToLower(p.Owner), qLower)
|
strings.Contains(strings.ToLower(p.Owner), qLower)
|
||||||
})
|
})
|
||||||
playlists = a.searchedPlaylists
|
playlists = a.searchedPlaylists
|
||||||
@@ -182,7 +181,7 @@ func (a *PlaylistsPage) onSearched(query string) {
|
|||||||
|
|
||||||
// update the model for both views if initialized,
|
// update the model for both views if initialized,
|
||||||
// refresh the active view
|
// refresh the active view
|
||||||
func (a *PlaylistsPage) refreshView(playlists []*subsonic.Playlist) {
|
func (a *PlaylistsPage) refreshView(playlists []*mediaprovider.Playlist) {
|
||||||
if a.listView != nil {
|
if a.listView != nil {
|
||||||
a.listView.Playlists = playlists
|
a.listView.Playlists = playlists
|
||||||
}
|
}
|
||||||
@@ -214,7 +213,7 @@ func (a *PlaylistsPage) Save() SavedPage {
|
|||||||
return &savedPlaylistsPage{
|
return &savedPlaylistsPage{
|
||||||
contr: a.contr,
|
contr: a.contr,
|
||||||
cfg: a.cfg,
|
cfg: a.cfg,
|
||||||
sm: a.sm,
|
mp: a.mp,
|
||||||
searchText: a.searcher.Entry.Text,
|
searchText: a.searcher.Entry.Text,
|
||||||
activeView: a.viewToggle.ActivatedButtonIndex(),
|
activeView: a.viewToggle.ActivatedButtonIndex(),
|
||||||
}
|
}
|
||||||
@@ -223,13 +222,13 @@ func (a *PlaylistsPage) Save() SavedPage {
|
|||||||
type savedPlaylistsPage struct {
|
type savedPlaylistsPage struct {
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
cfg *backend.PlaylistsPageConfig
|
cfg *backend.PlaylistsPageConfig
|
||||||
sm *backend.ServerManager
|
mp mediaprovider.MediaProvider
|
||||||
searchText string
|
searchText string
|
||||||
activeView int
|
activeView int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *savedPlaylistsPage) Restore() Page {
|
func (s *savedPlaylistsPage) Restore() Page {
|
||||||
return newPlaylistsPage(s.contr, s.cfg, s.sm, s.searchText, s.activeView)
|
return newPlaylistsPage(s.contr, s.cfg, s.mp, s.searchText, s.activeView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) {
|
func (a *PlaylistsPage) buildContainer(initialView fyne.CanvasObject) {
|
||||||
@@ -247,7 +246,7 @@ func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
type PlaylistList struct {
|
type PlaylistList struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
Playlists []*subsonic.Playlist
|
Playlists []*mediaprovider.Playlist
|
||||||
OnNavTo func(string)
|
OnNavTo func(string)
|
||||||
|
|
||||||
columnsLayout *layouts.ColumnsLayout
|
columnsLayout *layouts.ColumnsLayout
|
||||||
@@ -274,9 +273,9 @@ func NewPlaylistList() *PlaylistList {
|
|||||||
row := item.(*PlaylistListRow)
|
row := item.(*PlaylistListRow)
|
||||||
row.ID = a.Playlists[id].ID
|
row.ID = a.Playlists[id].ID
|
||||||
row.nameLabel.Text = a.Playlists[id].Name
|
row.nameLabel.Text = a.Playlists[id].Name
|
||||||
row.descrptionLabel.Text = a.Playlists[id].Comment
|
row.descrptionLabel.Text = a.Playlists[id].Description
|
||||||
row.ownerLabel.Text = a.Playlists[id].Owner
|
row.ownerLabel.Text = a.Playlists[id].Owner
|
||||||
row.trackCountLabel.Text = strconv.Itoa(a.Playlists[id].SongCount)
|
row.trackCountLabel.Text = strconv.Itoa(a.Playlists[id].TrackCount)
|
||||||
row.Refresh()
|
row.Refresh()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-10
@@ -28,27 +28,27 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati
|
|||||||
func (r Router) CreatePage(rte controller.Route) Page {
|
func (r Router) CreatePage(rte controller.Route) Page {
|
||||||
switch rte.Page {
|
switch rte.Page {
|
||||||
case controller.Album:
|
case controller.Album:
|
||||||
return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.App.ServerManager, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.Controller)
|
return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
|
||||||
case controller.Albums:
|
case controller.Albums:
|
||||||
return NewAlbumsPage(&r.App.Config.AlbumsPage, r.Controller, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager)
|
return NewAlbumsPage(&r.App.Config.AlbumsPage, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
|
||||||
case controller.Artist:
|
case controller.Artist:
|
||||||
return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager, r.App.ImageManager, r.Controller)
|
return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
|
||||||
case controller.Artists:
|
case controller.Artists:
|
||||||
return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager)
|
return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager.Server)
|
||||||
case controller.Favorites:
|
case controller.Favorites:
|
||||||
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager)
|
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
|
||||||
case controller.Genre:
|
case controller.Genre:
|
||||||
return NewGenrePage(rte.Arg, r.Controller, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager)
|
return NewGenrePage(rte.Arg, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
|
||||||
case controller.Genres:
|
case controller.Genres:
|
||||||
return NewArtistsGenresPage(true, r.Controller, r.App.ServerManager)
|
return NewArtistsGenresPage(true, r.Controller, r.App.ServerManager.Server)
|
||||||
case controller.NowPlaying:
|
case controller.NowPlaying:
|
||||||
return NewNowPlayingPage(rte.Arg, r.Controller, &r.App.Config.NowPlayingPage, r.App.ServerManager, r.App.PlaybackManager)
|
return NewNowPlayingPage(rte.Arg, r.Controller, &r.App.Config.NowPlayingPage, r.App.PlaybackManager)
|
||||||
case controller.Playlist:
|
case controller.Playlist:
|
||||||
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
|
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
|
||||||
case controller.Playlists:
|
case controller.Playlists:
|
||||||
return NewPlaylistsPage(r.Controller, &r.App.Config.PlaylistsPage, r.App.ServerManager)
|
return NewPlaylistsPage(r.Controller, &r.App.Config.PlaylistsPage, r.App.ServerManager.Server)
|
||||||
case controller.Tracks:
|
case controller.Tracks:
|
||||||
return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.App.LibraryManager)
|
return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.App.ServerManager.Server)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/layouts"
|
"github.com/dweymouth/supersonic/ui/layouts"
|
||||||
@@ -12,8 +13,6 @@ 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/go-subsonic/subsonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TracksPage struct {
|
type TracksPage struct {
|
||||||
@@ -37,11 +36,11 @@ type tracksPageState struct {
|
|||||||
searchText string
|
searchText string
|
||||||
contr *controller.Controller
|
contr *controller.Controller
|
||||||
conf *backend.TracksPageConfig
|
conf *backend.TracksPageConfig
|
||||||
lm *backend.LibraryManager
|
mp mediaprovider.MediaProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, lm *backend.LibraryManager) *TracksPage {
|
func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, mp mediaprovider.MediaProvider) *TracksPage {
|
||||||
t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, lm: lm}}
|
t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, mp: mp}}
|
||||||
t.ExtendBaseWidget(t)
|
t.ExtendBaseWidget(t)
|
||||||
|
|
||||||
t.tracklist = widgets.NewTracklist(nil)
|
t.tracklist = widgets.NewTracklist(nil)
|
||||||
@@ -79,12 +78,12 @@ func (t *TracksPage) Route() controller.Route {
|
|||||||
|
|
||||||
func (t *TracksPage) Reload() {
|
func (t *TracksPage) Reload() {
|
||||||
t.tracklist.Clear()
|
t.tracklist.Clear()
|
||||||
iter := t.lm.AllTracksIterator()
|
iter := t.mp.IterateTracks("")
|
||||||
// loads asynchronously
|
// loads asynchronously
|
||||||
t.loader = widgets.NewTracklistLoader(t.tracklist, iter)
|
t.loader = widgets.NewTracklistLoader(t.tracklist, iter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TracksPage) OnSongChange(track *subsonic.Child, lastScrobbledIfAny *subsonic.Child) {
|
func (t *TracksPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track) {
|
||||||
t.nowPlayingID = sharedutil.TrackIDOrEmptyStr(track)
|
t.nowPlayingID = sharedutil.TrackIDOrEmptyStr(track)
|
||||||
t.tracklist.SetNowPlaying(t.nowPlayingID)
|
t.tracklist.SetNowPlaying(t.nowPlayingID)
|
||||||
if t.searchTracklist != nil {
|
if t.searchTracklist != nil {
|
||||||
@@ -130,7 +129,7 @@ func (t *TracksPage) doSearch(query string) {
|
|||||||
} else {
|
} else {
|
||||||
t.searchTracklist.Clear()
|
t.searchTracklist.Clear()
|
||||||
}
|
}
|
||||||
iter := t.lm.SearchTracksIterator(query)
|
iter := t.mp.IterateTracks(query)
|
||||||
t.searchLoader = widgets.NewTracklistLoader(t.searchTracklist, iter)
|
t.searchLoader = widgets.NewTracklistLoader(t.searchTracklist, iter)
|
||||||
t.container.Objects[0].(*fyne.Container).Objects[0] = t.searchTracklist
|
t.container.Objects[0].(*fyne.Container).Objects[0] = t.searchTracklist
|
||||||
t.Refresh()
|
t.Refresh()
|
||||||
@@ -146,7 +145,7 @@ func (t *TracksPage) Save() SavedPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *tracksPageState) Restore() Page {
|
func (s *tracksPageState) Restore() Page {
|
||||||
t := NewTracksPage(s.contr, s.conf, s.lm)
|
t := NewTracksPage(s.contr, s.conf, s.mp)
|
||||||
t.searchText = s.searchText
|
t.searchText = s.searchText
|
||||||
if t.searchText != "" {
|
if t.searchText != "" {
|
||||||
t.searcher.Entry.Text = t.searchText
|
t.searcher.Entry.Text = t.searchText
|
||||||
|
|||||||
+51
-83
@@ -3,12 +3,10 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"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/sharedutil"
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
"github.com/dweymouth/supersonic/ui/dialogs"
|
"github.com/dweymouth/supersonic/ui/dialogs"
|
||||||
@@ -20,8 +18,6 @@ import (
|
|||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NavigationHandler func(Route)
|
type NavigationHandler func(Route)
|
||||||
@@ -94,14 +90,14 @@ func (m *Controller) ShowPopUpImage(img image.Image) {
|
|||||||
|
|
||||||
func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) {
|
func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) {
|
||||||
tracklist.OnAddToPlaylist = m.DoAddTracksToPlaylistWorkflow
|
tracklist.OnAddToPlaylist = m.DoAddTracksToPlaylistWorkflow
|
||||||
tracklist.OnAddToQueue = func(tracks []*subsonic.Child) {
|
tracklist.OnAddToQueue = func(tracks []*mediaprovider.Track) {
|
||||||
m.App.PlaybackManager.LoadTracks(tracks, true, false)
|
m.App.PlaybackManager.LoadTracks(tracks, true, false)
|
||||||
}
|
}
|
||||||
tracklist.OnPlayTrackAt = func(idx int) {
|
tracklist.OnPlayTrackAt = func(idx int) {
|
||||||
m.App.PlaybackManager.LoadTracks(tracklist.Tracks, false, false)
|
m.App.PlaybackManager.LoadTracks(tracklist.Tracks, false, false)
|
||||||
m.App.PlaybackManager.PlayTrackAt(idx)
|
m.App.PlaybackManager.PlayTrackAt(idx)
|
||||||
}
|
}
|
||||||
tracklist.OnPlaySelection = func(tracks []*subsonic.Child, shuffle bool) {
|
tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) {
|
||||||
m.App.PlaybackManager.LoadTracks(tracks, false, shuffle)
|
m.App.PlaybackManager.LoadTracks(tracks, false, shuffle)
|
||||||
m.App.PlaybackManager.PlayFromBeginning()
|
m.App.PlaybackManager.PlayFromBeginning()
|
||||||
}
|
}
|
||||||
@@ -132,12 +128,14 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
|
|||||||
m.NavigateTo(ArtistRoute(artistID))
|
m.NavigateTo(ArtistRoute(artistID))
|
||||||
}
|
}
|
||||||
grid.OnAddToPlaylist = func(albumID string) {
|
grid.OnAddToPlaylist = func(albumID string) {
|
||||||
album, err := m.App.ServerManager.Server.GetAlbum(albumID)
|
go func() {
|
||||||
if err != nil {
|
album, err := m.App.ServerManager.Server.GetAlbum(albumID)
|
||||||
log.Printf("error loading album: %s", err.Error())
|
if err != nil {
|
||||||
return
|
log.Printf("error loading album: %s", err.Error())
|
||||||
}
|
return
|
||||||
m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Song))
|
}
|
||||||
|
m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Tracks))
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,37 +171,41 @@ func (m *Controller) PromptForFirstServer() {
|
|||||||
// Depending on the results of that dialog, potentially create a new playlist
|
// Depending on the results of that dialog, potentially create a new playlist
|
||||||
// Add tracks to the user-specified playlist
|
// Add tracks to the user-specified playlist
|
||||||
func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
||||||
pls, err := m.App.LibraryManager.GetUserOwnedPlaylists()
|
go func() {
|
||||||
if err != nil {
|
pls, err := m.App.ServerManager.Server.GetPlaylists()
|
||||||
// TODO: surface this error to user
|
pls = sharedutil.FilterSlice(pls, func(pl *mediaprovider.Playlist) bool {
|
||||||
log.Printf("error getting user-owned playlists: %s", err.Error())
|
return pl.Owner == m.App.ServerManager.LoggedInUser
|
||||||
return
|
})
|
||||||
}
|
if err != nil {
|
||||||
plNames := make([]string, 0, len(pls))
|
// TODO: surface this error to user
|
||||||
for _, pl := range pls {
|
log.Printf("error getting user-owned playlists: %s", err.Error())
|
||||||
plNames = append(plNames, pl.Name)
|
return
|
||||||
}
|
|
||||||
|
|
||||||
dlg := dialogs.NewAddToPlaylistDialog("Add to Playlist", plNames)
|
|
||||||
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
|
||||||
m.ClosePopUpOnEscape(pop)
|
|
||||||
dlg.OnCanceled = pop.Hide
|
|
||||||
dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) {
|
|
||||||
pop.Hide()
|
|
||||||
m.doModalClosed()
|
|
||||||
if playlistChoice < 0 {
|
|
||||||
m.App.ServerManager.Server.CreatePlaylistWithTracks(
|
|
||||||
trackIDs, map[string]string{"name": newPlaylistName})
|
|
||||||
} else {
|
|
||||||
m.App.ServerManager.Server.UpdatePlaylistTracks(
|
|
||||||
pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/)
|
|
||||||
}
|
}
|
||||||
}
|
plNames := make([]string, 0, len(pls))
|
||||||
m.haveModal = true
|
for _, pl := range pls {
|
||||||
pop.Show()
|
plNames = append(plNames, pl.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
dlg := dialogs.NewAddToPlaylistDialog("Add to Playlist", plNames)
|
||||||
|
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
||||||
|
m.ClosePopUpOnEscape(pop)
|
||||||
|
dlg.OnCanceled = pop.Hide
|
||||||
|
dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) {
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
if playlistChoice < 0 {
|
||||||
|
go m.App.ServerManager.Server.CreatePlaylist(newPlaylistName, trackIDs)
|
||||||
|
} else {
|
||||||
|
go m.App.ServerManager.Server.EditPlaylistTracks(
|
||||||
|
pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.haveModal = true
|
||||||
|
pop.Show()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) {
|
func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
|
||||||
dlg := dialogs.NewEditPlaylistDialog(playlist)
|
dlg := dialogs.NewEditPlaylistDialog(playlist)
|
||||||
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
||||||
m.ClosePopUpOnEscape(pop)
|
m.ClosePopUpOnEscape(pop)
|
||||||
@@ -234,11 +236,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) {
|
|||||||
pop.Hide()
|
pop.Hide()
|
||||||
m.doModalClosed()
|
m.doModalClosed()
|
||||||
go func() {
|
go func() {
|
||||||
err := m.App.ServerManager.Server.UpdatePlaylist(playlist.ID, map[string]string{
|
err := m.App.ServerManager.Server.EditPlaylist(playlist.ID, dlg.Name, dlg.Description, dlg.IsPublic)
|
||||||
"name": dlg.Name,
|
|
||||||
"comment": dlg.Description,
|
|
||||||
"public": strconv.FormatBool(dlg.IsPublic),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error updating playlist: %s", err.Error())
|
log.Printf("error updating playlist: %s", err.Error())
|
||||||
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
} else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID {
|
||||||
@@ -405,49 +403,19 @@ func (c *Controller) doModalClosed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) SetTrackFavorites(trackIDs []string, favorite bool) {
|
func (c *Controller) SetTrackFavorites(trackIDs []string, favorite bool) {
|
||||||
s := c.App.ServerManager.Server
|
go c.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{
|
||||||
if favorite {
|
TrackIDs: trackIDs,
|
||||||
go s.Star(subsonic.StarParameters{SongIDs: trackIDs})
|
}, favorite)
|
||||||
} else {
|
|
||||||
go s.Unstar(subsonic.StarParameters{SongIDs: trackIDs})
|
|
||||||
}
|
|
||||||
for _, id := range trackIDs {
|
for _, id := range trackIDs {
|
||||||
c.App.PlaybackManager.OnTrackFavoriteStatusChanged(id, favorite)
|
c.App.PlaybackManager.OnTrackFavoriteStatusChanged(id, favorite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) SetTrackRatings(trackIDs []string, rating int) {
|
func (c *Controller) SetTrackRatings(trackIDs []string, rating int) {
|
||||||
// Subsonic doesn't allow bulk setting ratings.
|
go c.App.ServerManager.Server.SetRating(mediaprovider.RatingFavoriteParameters{
|
||||||
// To not overwhelm the server with requests, set rating for
|
TrackIDs: trackIDs,
|
||||||
// only 5 tracks at a time concurrently
|
}, rating)
|
||||||
batchSize := 5
|
|
||||||
batchSetRating := func(offs int, wg *sync.WaitGroup) {
|
|
||||||
for i := 0; i < batchSize && offs+i < len(trackIDs); i++ {
|
|
||||||
if wg != nil {
|
|
||||||
wg.Add(1)
|
|
||||||
}
|
|
||||||
go func(idx int) {
|
|
||||||
c.App.ServerManager.Server.SetRating(trackIDs[idx], rating)
|
|
||||||
if wg != nil {
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
}(offs + i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(trackIDs) <= 5 {
|
|
||||||
// one batch only - no need to use wait group
|
|
||||||
batchSetRating(0, nil)
|
|
||||||
} else {
|
|
||||||
go func() {
|
|
||||||
numBatches := int(math.Ceil(float64(len(trackIDs)) / float64(batchSize)))
|
|
||||||
for i := 0; i < numBatches; i++ {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
batchSetRating(i*batchSize, &wg)
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify PlaybackManager of rating change to update
|
// Notify PlaybackManager of rating change to update
|
||||||
// the in-memory track models
|
// the in-memory track models
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/data/binding"
|
"fyne.io/fyne/v2/data/binding"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EditPlaylistDialog struct {
|
type EditPlaylistDialog struct {
|
||||||
@@ -23,11 +23,11 @@ type EditPlaylistDialog struct {
|
|||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEditPlaylistDialog(playlist *subsonic.Playlist) *EditPlaylistDialog {
|
func NewEditPlaylistDialog(playlist *mediaprovider.Playlist) *EditPlaylistDialog {
|
||||||
e := &EditPlaylistDialog{
|
e := &EditPlaylistDialog{
|
||||||
IsPublic: playlist.Public,
|
IsPublic: playlist.Public,
|
||||||
Name: playlist.Name,
|
Name: playlist.Name,
|
||||||
Description: playlist.Comment,
|
Description: playlist.Description,
|
||||||
}
|
}
|
||||||
e.ExtendBaseWidget(e)
|
e.ExtendBaseWidget(e)
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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()
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
+30
-9
@@ -6,17 +6,38 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"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"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const batchFetchSize = 6
|
const batchFetchSize = 6
|
||||||
|
|
||||||
|
type BatchingIterator struct {
|
||||||
|
iter mediaprovider.AlbumIterator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBatchingIterator(iter mediaprovider.AlbumIterator) BatchingIterator {
|
||||||
|
return BatchingIterator{iter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BatchingIterator) NextN(n int) []*mediaprovider.Album {
|
||||||
|
results := make([]*mediaprovider.Album, 0, n)
|
||||||
|
i := 0
|
||||||
|
for i < n {
|
||||||
|
album := b.iter.Next()
|
||||||
|
if album == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
results = append(results, album)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
type ImageFetcher interface {
|
type ImageFetcher interface {
|
||||||
GetCoverThumbnailFromCache(string) (image.Image, bool)
|
GetCoverThumbnailFromCache(string) (image.Image, bool)
|
||||||
GetCoverThumbnail(string) (image.Image, error)
|
GetCoverThumbnail(string) (image.Image, error)
|
||||||
@@ -27,24 +48,24 @@ type GridViewIterator interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type gridViewAlbumIterator struct {
|
type gridViewAlbumIterator struct {
|
||||||
iter *backend.BatchingIterator
|
iter BatchingIterator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
|
func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
|
||||||
albums := g.iter.NextN(n)
|
albums := g.iter.NextN(n)
|
||||||
return sharedutil.MapSlice(albums, func(al *subsonic.AlbumID3) GridViewItemModel {
|
return sharedutil.MapSlice(albums, func(al *mediaprovider.Album) GridViewItemModel {
|
||||||
return GridViewItemModel{
|
return GridViewItemModel{
|
||||||
Name: al.Name,
|
Name: al.Name,
|
||||||
ID: al.ID,
|
ID: al.ID,
|
||||||
CoverArtID: al.CoverArt,
|
CoverArtID: al.CoverArtID,
|
||||||
Secondary: al.Artist,
|
Secondary: al.ArtistNames[0],
|
||||||
SecondaryID: al.ArtistID,
|
SecondaryID: al.ArtistIDs[0],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGridViewAlbumIterator(iter backend.AlbumIterator) GridViewIterator {
|
func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator {
|
||||||
return gridViewAlbumIterator{iter: backend.NewBatchingIterator(iter)}
|
return gridViewAlbumIterator{iter: NewBatchingIterator(iter)}
|
||||||
}
|
}
|
||||||
|
|
||||||
type GridView struct {
|
type GridView struct {
|
||||||
|
|||||||
+30
-35
@@ -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,15 +607,15 @@ 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.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon
|
|
||||||
} else {
|
|
||||||
t.isFavorite = true
|
t.isFavorite = true
|
||||||
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon
|
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon
|
||||||
|
} else {
|
||||||
|
t.isFavorite = false
|
||||||
|
t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
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)]
|
||||||
|
|||||||
@@ -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++ {
|
||||||
|
|||||||
Reference in New Issue
Block a user