diff --git a/backend/albumiterator.go b/backend/albumiterator.go deleted file mode 100644 index 70c7b05..0000000 --- a/backend/albumiterator.go +++ /dev/null @@ -1,397 +0,0 @@ -package backend - -import ( - "log" - "strconv" - "strings" - - "github.com/dweymouth/go-subsonic/subsonic" - "github.com/dweymouth/supersonic/sharedutil" -) - -type AlbumSortOrder string - -const ( - AlbumSortRecentlyAdded AlbumSortOrder = "Recently Added" - AlbumSortRecentlyPlayed AlbumSortOrder = "Recently Played" - AlbumSortFrequentlyPlayed AlbumSortOrder = "Frequently Played" - AlbumSortRandom AlbumSortOrder = "Random" - AlbumSortTitleAZ AlbumSortOrder = "Title (A-Z)" - AlbumSortArtistAZ AlbumSortOrder = "Artist (A-Z)" - AlbumSortYearAscending AlbumSortOrder = "Year (ascending)" - AlbumSortYearDescending AlbumSortOrder = "Year (descending)" -) - -var ( - AlbumSortOrders []string = []string{ - string(AlbumSortRecentlyAdded), - string(AlbumSortRecentlyPlayed), - string(AlbumSortFrequentlyPlayed), - string(AlbumSortRandom), - string(AlbumSortTitleAZ), - string(AlbumSortArtistAZ), - string(AlbumSortYearAscending), - string(AlbumSortYearDescending), - } -) - -type AlbumFilter struct { - MinYear int - MaxYear int // 0 == unset/match any - Genres []string // len(0) == unset/match any - - ExcludeFavorited bool // mut. exc. with ExcludeUnfavorited - ExcludeUnfavorited bool // mut. exc. with ExcludeFavorited -} - -func (f *AlbumFilter) Matches(album *subsonic.AlbumID3) bool { - if album == nil { - return false - } - if f.ExcludeFavorited && !album.Starred.IsZero() { - return false - } - if f.ExcludeUnfavorited && album.Starred.IsZero() { - return false - } - if y := album.Year; y < f.MinYear || (f.MaxYear > 0 && y > f.MaxYear) { - return false - } - if len(f.Genres) == 0 { - return true - } - for _, g := range f.Genres { - if strings.EqualFold(g, album.Genre) { - return true - } - } - return false -} - -func (f *AlbumFilter) IsEmpty() bool { - return !f.ExcludeFavorited && !f.ExcludeUnfavorited && - f.MinYear == 0 && f.MaxYear == 0 && len(f.Genres) == 0 -} - -func (l *LibraryManager) AlbumsIter(sort AlbumSortOrder, filter AlbumFilter) AlbumIterator { - switch sort { - case AlbumSortRecentlyAdded: - return l.newBaseIter("newest", filter, make(map[string]string)) - case AlbumSortRecentlyPlayed: - return l.newBaseIter("recent", filter, make(map[string]string)) - case AlbumSortFrequentlyPlayed: - return l.newBaseIter("frequent", filter, make(map[string]string)) - case AlbumSortRandom: - return l.newRandomIter() - case AlbumSortTitleAZ: - return l.newBaseIter("alphabeticalByName", filter, make(map[string]string)) - case AlbumSortArtistAZ: - return l.newBaseIter("alphabeticalByArtist", filter, make(map[string]string)) - case AlbumSortYearAscending: - return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "0", "toYear": "3000"}) - case AlbumSortYearDescending: - return l.newBaseIter("byYear", filter, map[string]string{"fromYear": "3000", "toYear": "0"}) - default: - log.Printf("Undefined album sort order: %s", sort) - return nil - } -} - -func (l *LibraryManager) StarredIter(filter AlbumFilter) AlbumIterator { - return l.newBaseIter("starred", filter, make(map[string]string)) -} - -func (l *LibraryManager) GenreIter(genre string, filter AlbumFilter) AlbumIterator { - return l.newBaseIter("byGenre", filter, map[string]string{"genre": genre}) -} - -func (l *LibraryManager) SearchIter(query string) AlbumIterator { - return l.newSearchIter(query, AlbumFilter{}) -} - -func (l *LibraryManager) SearchIterWithFilter(query string, filter AlbumFilter) AlbumIterator { - return l.newSearchIter(query, filter) -} - -func (l *LibraryManager) GetAlbum(id string) (*subsonic.AlbumID3, error) { - a, err := l.s.Server.GetAlbum(id) - if err != nil { - return nil, err - } - return a, nil -} - -type baseIter struct { - listType string - filter AlbumFilter - serverPos int - l *LibraryManager - s *subsonic.Client - opts map[string]string - prefetched []*subsonic.AlbumID3 - prefetchedPos int - done bool -} - -func (l *LibraryManager) newBaseIter(listType string, filter AlbumFilter, opts map[string]string) *baseIter { - return &baseIter{ - listType: listType, - filter: filter, - l: l, - s: l.s.Server, - opts: opts, - } -} - -func (r *baseIter) Next() *subsonic.AlbumID3 { - if r.done { - return nil - } - if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) { - a := r.prefetched[r.prefetchedPos] - r.prefetchedPos++ - return a - } - r.prefetched = nil - for { // keep fetching until we are done or have mathcing results - r.opts["offset"] = strconv.Itoa(r.serverPos) - albums, err := r.s.GetAlbumList2(r.listType, r.opts) - if err != nil { - log.Printf("error fetching albums: %s", err.Error()) - albums = nil - } - if len(albums) == 0 { - r.done = true - return nil - } - r.serverPos += len(albums) - albums = sharedutil.FilterSlice(albums, r.filter.Matches) - r.prefetched = albums - if len(albums) > 0 { - break - } - } - r.prefetchedPos = 1 - if r.l.PreCacheCoverFn != nil { - for _, album := range r.prefetched { - go r.l.PreCacheCoverFn(album.CoverArt) - } - } - - return r.prefetched[0] -} - -type searchIter struct { - searchIterBase - - l *LibraryManager - filter AlbumFilter - prefetched []*subsonic.AlbumID3 - prefetchedPos int - albumIDset map[string]bool - done bool -} - -func (l *LibraryManager) newSearchIter(query string, filter AlbumFilter) *searchIter { - return &searchIter{ - searchIterBase: searchIterBase{ - query: query, - s: l.s.Server, - }, - l: l, - filter: filter, - albumIDset: make(map[string]bool), - } -} - -func (s *searchIter) Next() *subsonic.AlbumID3 { - if s.done { - return nil - } - - // prefetch more search results from server - if s.prefetched == nil { - results := s.searchIterBase.fetchResults() - if results == nil { - s.done = true - s.albumIDset = nil - return nil - } - - // add results from albums search - s.addNewAlbums(results.Album) - s.albumOffset += len(results.Album) - - // add results from artists search - for _, artist := range results.Artist { - artist, err := s.s.GetArtist(artist.ID) - if err != nil || artist == nil { - log.Printf("error fetching artist: %s", err.Error()) - } else { - s.addNewAlbums(artist.Album) - } - } - s.artistOffset += len(results.Artist) - - // add results from songs search - for _, song := range results.Song { - if song.AlbumID == "" { - continue - } - album, err := s.s.GetAlbum(song.AlbumID) - if err != nil || album == nil { - log.Printf("error fetching album: %s", err.Error()) - } else { - s.addNewAlbums([]*subsonic.AlbumID3{album}) - } - } - s.songOffset += len(results.Song) - } - - // return from prefetched results - if len(s.prefetched) > 0 { - a := s.prefetched[s.prefetchedPos] - s.prefetchedPos++ - if s.prefetchedPos == len(s.prefetched) { - s.prefetched = nil - s.prefetchedPos = 0 - } - - return a - } - - return nil -} - -func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) { - for _, album := range al { - if _, have := s.albumIDset[album.ID]; have { - continue - } - if !s.filter.Matches(album) { - continue - } - s.prefetched = append(s.prefetched, album) - if s.l.PreCacheCoverFn != nil { - go s.l.PreCacheCoverFn(album.CoverArt) - } - s.albumIDset[album.ID] = true - } -} - -type randomIter struct { - albumIDSet map[string]bool - l *LibraryManager - s *subsonic.Client - prefetched []*subsonic.AlbumID3 - prefetchedPos int - // Random iter works in two phases - phase 1 by requesting random - // albums from the server. Since the Subsonic API provides no way - // of paginating a single random sort, we may get albums back twice. - // We use albumIDSet to keep track of which albums have already been returned. - // Once we start getting back too many already-returned albums, - // switch to requesting more albums from a deterministic sort order. - phaseTwo bool - offset int - done bool -} - -func (l *LibraryManager) newRandomIter() *randomIter { - return &randomIter{ - l: l, - s: l.s.Server, - albumIDSet: make(map[string]bool), - } -} - -func (r *randomIter) Next() *subsonic.AlbumID3 { - if r.done { - return nil - } - - if r.prefetched == nil { - if r.phaseTwo { - for len(r.prefetched) == 0 { - albums, err := r.s.GetAlbumList2("newest", map[string]string{"size": "20", "offset": strconv.Itoa(r.offset)}) - if err != nil { - log.Println(err) - albums = nil - } - if len(albums) == 0 { - r.done = true - r.albumIDSet = nil - return nil - } - r.offset += len(albums) - for _, album := range albums { - if _, ok := r.albumIDSet[album.ID]; !ok { - r.prefetched = append(r.prefetched, album) - if r.l.PreCacheCoverFn != nil { - go r.l.PreCacheCoverFn(album.CoverArt) - } - r.albumIDSet[album.ID] = true - } - } - } - r.prefetchedPos = 0 - } else { - albums, err := r.s.GetAlbumList2("random", map[string]string{"size": "25"}) - if err != nil { - log.Println(err) - r.done = true - r.albumIDSet = nil - return nil - } - var hitCount int - for _, album := range albums { - if _, ok := r.albumIDSet[album.ID]; !ok { - hitCount++ - r.prefetched = append(r.prefetched, album) - if r.l.PreCacheCoverFn != nil { - go r.l.PreCacheCoverFn(album.CoverArt) - } - r.albumIDSet[album.ID] = true - } - } - if successRatio := float64(hitCount) / float64(25); successRatio < 0.3 { - r.phaseTwo = true - } - } - } - - // return from prefetched results - if len(r.prefetched) > 0 { - a := r.prefetched[r.prefetchedPos] - r.prefetchedPos++ - if r.prefetchedPos == len(r.prefetched) { - r.prefetched = nil - r.prefetchedPos = 0 - } - - return a - } - - return nil -} - -type BatchingIterator struct { - iter AlbumIterator -} - -func NewBatchingIterator(iter AlbumIterator) *BatchingIterator { - return &BatchingIterator{iter} -} - -func (b *BatchingIterator) NextN(n int) []*subsonic.AlbumID3 { - results := make([]*subsonic.AlbumID3, 0, n) - i := 0 - for i < n { - album := b.iter.Next() - if album == nil { - break - } - results = append(results, album) - i++ - } - return results -} diff --git a/backend/imagemanager.go b/backend/imagemanager.go index a330b2d..9644e5a 100644 --- a/backend/imagemanager.go +++ b/backend/imagemanager.go @@ -18,6 +18,8 @@ import ( const CachedImageValidTime = 24 * time.Hour +const coverArtThumbnailSize = 300 + type ImageManager struct { s *ServerManager baseCacheDir string @@ -72,7 +74,7 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error) if i.cachedFullSizeCoverID == coverID { return i.cachedFullSizeCover, nil } - im, err := i.s.Server.GetCoverArt(coverID, nil) + im, err := i.s.Server.GetCoverArt(coverID, 0) if err != nil { return nil, err } @@ -143,7 +145,7 @@ func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(coverID string, ttl ti } func (i *ImageManager) fetchAndCacheCoverFromServer(coverID string, ttl time.Duration) (image.Image, error) { - img, err := i.s.Server.GetCoverArt(coverID, map[string]string{"size": "300"}) + img, err := i.s.Server.GetCoverArt(coverID, coverArtThumbnailSize) if err != nil { return nil, err } diff --git a/backend/librarymanager.go b/backend/librarymanager.go index 69b1092..5f4919d 100644 --- a/backend/librarymanager.go +++ b/backend/librarymanager.go @@ -23,17 +23,3 @@ func NewLibraryManager(s *ServerManager) *LibraryManager { s: s, } } - -func (l *LibraryManager) GetUserOwnedPlaylists() ([]*subsonic.Playlist, error) { - pl, err := l.s.Server.GetPlaylists(nil) - userPl := make([]*subsonic.Playlist, 0) - if err != nil { - return nil, err - } - for _, p := range pl { - if p.Owner == l.s.Server.User { - userPl = append(userPl, p) - } - } - return userPl, nil -} diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index c428f15..936f345 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -26,9 +26,9 @@ type RatingFavoriteParameters struct { } type Favorites struct { - Albums []Album - Artists []Artist - Tracks []Track + Albums []*Album + Artists []*Artist + Tracks []*Track } type MediaProvider interface { @@ -48,19 +48,19 @@ type MediaProvider interface { IterateTracks(searchQuery string) TrackIterator - GetRandomTracks(genre string, count int) ([]Track, error) + GetRandomTracks(genre string, count int) ([]*Track, error) - GetSimilarTracks(artistID string, count int) ([]Track, error) + GetSimilarTracks(artistID string, count int) ([]*Track, error) - GetArtists() ([]Artist, error) + GetArtists() ([]*Artist, error) - GetGenres() ([]Genre, error) + GetGenres() ([]*Genre, error) GetFavorites() (Favorites, error) GetStreamURL(trackID string) (string, error) - SetFavorite(params RatingFavoriteParameters) error + SetFavorite(params RatingFavoriteParameters, favorite bool) error SetRating(params RatingFavoriteParameters, rating int) error diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index 9aa00f9..bfa4e18 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -15,7 +15,7 @@ type Album struct { type AlbumWithTracks struct { Album - Tracks []Track + Tracks []*Track } type Artist struct { @@ -26,14 +26,14 @@ type Artist struct { type ArtistWithAlbums struct { Artist - Albums []Album + Albums []*Album } type ArtistInfo struct { Biography string LastFMUrl string ImageURL string - SimilarArtists []Artist + SimilarArtists []*Artist } type Genre struct { @@ -54,11 +54,14 @@ type Track struct { ArtistIDs []string ArtistNames []string Album string + AlbumID string + Year int Rating int Favorite bool Size int64 PlayCount int FilePath string + BitRate int } type Playlist struct { @@ -73,5 +76,5 @@ type Playlist struct { type PlaylistWithTracks struct { Playlist - Tracks []Track + Tracks []*Track } diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go index 4e8a4db..f198e1f 100644 --- a/backend/mediaprovider/subsonic/albumiterator.go +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -78,7 +78,7 @@ type baseIter struct { serverPos int s *subsonic.Client opts map[string]string - prefetched []mediaprovider.Album + prefetched []*mediaprovider.Album prefetchedPos int done bool } @@ -99,7 +99,7 @@ func (r *baseIter) Next() *mediaprovider.Album { if r.prefetched != nil && r.prefetchedPos < len(r.prefetched) { a := r.prefetched[r.prefetchedPos] r.prefetchedPos++ - return &a + return a } r.prefetched = nil for { // keep fetching until we are done or have mathcing results @@ -129,8 +129,7 @@ func (r *baseIter) Next() *mediaprovider.Album { } */ - ret := r.prefetched[0] - return &ret + return r.prefetched[0] } type searchIter struct { @@ -207,8 +206,7 @@ func (s *searchIter) Next() *mediaprovider.Album { s.prefetchedPos = 0 } - al := toAlbum(a) - return &al + return toAlbum(a) } return nil @@ -323,8 +321,7 @@ func (r *randomIter) Next() *mediaprovider.Album { r.prefetchedPos = 0 } - al := toAlbum(a) - return &al + return toAlbum(a) } return nil diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 876e79d..5d48469 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -2,7 +2,6 @@ package subsonic import ( "image" - "log" "math" "strconv" "sync" @@ -115,12 +114,12 @@ func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A }, nil } -func (s *subsonicMediaProvider) GetArtists() ([]mediaprovider.Artist, error) { +func (s *subsonicMediaProvider) GetArtists() ([]*mediaprovider.Artist, error) { idxs, err := s.client.GetArtists(map[string]string{}) if err != nil { return nil, err } - var artists []mediaprovider.Artist + var artists []*mediaprovider.Artist for _, idx := range idxs.Index { for _, ar := range idx.Artist { artists = append(artists, toArtistFromID3(ar)) @@ -130,7 +129,11 @@ func (s *subsonicMediaProvider) GetArtists() ([]mediaprovider.Artist, error) { } func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) { - return s.client.GetCoverArt(id, map[string]string{"size": strconv.Itoa(size)}) + params := map[string]string{} + if size > 0 { + params["size"] = strconv.Itoa(size) + } + return s.client.GetCoverArt(id, params) } func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) { @@ -145,13 +148,13 @@ func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) }, nil } -func (s *subsonicMediaProvider) GetGenres() ([]mediaprovider.Genre, error) { +func (s *subsonicMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) { g, err := s.client.GetGenres() if err != nil { return nil, err } - return sharedutil.MapSlice(g, func(g *subsonic.Genre) mediaprovider.Genre { - return mediaprovider.Genre{ + return sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre { + return &mediaprovider.Genre{ Name: g.Name, AlbumCount: g.AlbumCount, TrackCount: g.SongCount, @@ -178,7 +181,7 @@ func (s *subsonicMediaProvider) GetPlaylists() ([]mediaprovider.Playlist, error) return sharedutil.MapSlice(pl, toPlaylist), nil } -func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]mediaprovider.Track, error) { +func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]*mediaprovider.Track, error) { opts := map[string]string{"size": strconv.Itoa(count)} if genreName != "" { opts["genre"] = genreName @@ -190,7 +193,7 @@ func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([] return sharedutil.MapSlice(tr, toTrack), nil } -func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([]mediaprovider.Track, error) { +func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([]*mediaprovider.Track, error) { tr, err := s.client.GetSimilarSongs2(artistID, map[string]string{"count": strconv.Itoa(count)}) if err != nil { return nil, err @@ -216,12 +219,16 @@ func (s *subsonicMediaProvider) Scrobble(trackID string, submission bool) error "submission": strconv.FormatBool(submission)}) } -func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters) error { - return s.client.Star(subsonic.StarParameters{ +func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error { + subParams := subsonic.StarParameters{ AlbumIDs: params.AlbumIDs, ArtistIDs: params.ArtistIDs, SongIDs: params.TrackIDs, - }) + } + if favorite { + return s.client.Star(subParams) + } + return s.client.Unstar(subParams) } func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoriteParameters, rating int) error { @@ -263,12 +270,11 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar return err } -func toTrack(ch *subsonic.Child) mediaprovider.Track { +func toTrack(ch *subsonic.Child) *mediaprovider.Track { if ch == nil { - log.Println("subsonicMediaProvider: toTrack called on nil track") - return mediaprovider.Track{} + return nil } - return mediaprovider.Track{ + return &mediaprovider.Track{ ID: ch.ID, CoverArtID: ch.CoverArt, ParentID: ch.Parent, @@ -280,19 +286,21 @@ func toTrack(ch *subsonic.Child) mediaprovider.Track { ArtistIDs: []string{ch.ArtistID}, ArtistNames: []string{ch.Artist}, Album: ch.Album, + AlbumID: ch.AlbumID, + Year: ch.Year, Rating: ch.UserRating, Favorite: !ch.Starred.IsZero(), PlayCount: int(ch.PlayCount), FilePath: ch.Path, + BitRate: ch.BitRate, } } -func toAlbum(al *subsonic.AlbumID3) mediaprovider.Album { +func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album { if al == nil { - log.Println("subsonicMediaProvider: toAlbum called on nil album") - return mediaprovider.Album{} + return nil } - return mediaprovider.Album{ + return &mediaprovider.Album{ ID: al.ID, CoverArtID: al.CoverArt, Name: al.Name, @@ -306,23 +314,21 @@ func toAlbum(al *subsonic.AlbumID3) mediaprovider.Album { } } -func toArtist(ar *subsonic.Artist) mediaprovider.Artist { +func toArtist(ar *subsonic.Artist) *mediaprovider.Artist { if ar == nil { - log.Println("subsonicMediaProvider: toArtist called on nil artist") - return mediaprovider.Artist{} + return nil } - return mediaprovider.Artist{ + return &mediaprovider.Artist{ ID: ar.ID, Name: ar.Name, } } -func toArtistFromID3(ar *subsonic.ArtistID3) mediaprovider.Artist { +func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist { if ar == nil { - log.Println("subsonicMediaProvider: toArtistFromID3 called on nil artistID3") - return mediaprovider.Artist{} + return nil } - return mediaprovider.Artist{ + return &mediaprovider.Artist{ ID: ar.ID, Name: ar.Name, AlbumCount: ar.AlbumCount, diff --git a/backend/mediaprovider/subsonic/trackiterator.go b/backend/mediaprovider/subsonic/trackiterator.go index 4930749..98f9481 100644 --- a/backend/mediaprovider/subsonic/trackiterator.go +++ b/backend/mediaprovider/subsonic/trackiterator.go @@ -58,7 +58,7 @@ func (a *allTracksIterator) Next() *mediaprovider.Track { tr := a.curAlbum.Tracks[a.curTrackIdx] a.curTrackIdx += 1 - return &tr + return tr } type searchTracksIterator struct { @@ -109,8 +109,7 @@ func (s *searchTracksIterator) Next() *mediaprovider.Track { s.prefetched = s.prefetched[:0] s.prefetchedPos = 0 } - track := toTrack(tr) - return &track + return toTrack(tr) } // no more results diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index f70e089..230f7a8 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -3,14 +3,12 @@ package backend import ( "context" "log" - "strconv" "time" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/sharedutil" - - "github.com/dweymouth/go-subsonic/subsonic" ) const ( @@ -33,14 +31,14 @@ type PlaybackManager struct { curTrackTime float64 callbacksDisabled bool - playQueue []*subsonic.Child + playQueue []*mediaprovider.Track nowPlayingIdx int64 // to pass to onSongChange listeners; clear once listeners have been called - lastScrobbled *subsonic.Child + lastScrobbled *mediaprovider.Track scrobbleCfg *ScrobbleConfig - onSongChange []func(nowPlaying *subsonic.Child, justScrobbledIfAny *subsonic.Child) + onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64) } @@ -111,7 +109,7 @@ func (p *PlaybackManager) DisableCallbacks() { } // Gets the curently playing song, if any. -func (p *PlaybackManager) NowPlaying() *subsonic.Child { +func (p *PlaybackManager) NowPlaying() *mediaprovider.Track { if len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped { return nil } @@ -119,7 +117,7 @@ func (p *PlaybackManager) NowPlaying() *subsonic.Child { } // Sets a callback that is notified whenever a new song begins playing. -func (p *PlaybackManager) OnSongChange(cb func(nowPlaying *subsonic.Child, justScrobbledIfAny *subsonic.Child)) { +func (p *PlaybackManager) OnSongChange(cb func(nowPlaying *mediaprovider.Track, justScrobbledIfAny *mediaprovider.Track)) { p.onSongChange = append(p.onSongChange, cb) } @@ -134,7 +132,7 @@ func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle if err != nil { return err } - return p.LoadTracks(album.Song, appendToQueue, shuffle) + return p.LoadTracks(album.Tracks, appendToQueue, shuffle) } // Loads the specified playlist into the play queue. @@ -143,10 +141,10 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool, sh if err != nil { return err } - return p.LoadTracks(playlist.Entry, appendToQueue, shuffle) + return p.LoadTracks(playlist.Tracks, appendToQueue, shuffle) } -func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue, shuffle bool) error { +func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, appendToQueue, shuffle bool) error { if !appendToQueue { p.player.Stop() p.nowPlayingIdx = 0 @@ -157,11 +155,11 @@ func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue, sh util.ShuffleSlice(nums) } for _, i := range nums { - url, err := p.sm.Server.GetStreamURL(tracks[i].ID, map[string]string{}) + url, err := p.sm.Server.GetStreamURL(tracks[i].ID) if err != nil { return err } - p.player.AppendFile(url.String()) + p.player.AppendFile(url) // ensure a deep copy of the track info so that we can maintain our own state // (tracking play count increases, favorite, and rating) without messing up // other views' track models @@ -200,11 +198,7 @@ func (p *PlaybackManager) PlayTrackAt(idx int) error { } func (p *PlaybackManager) PlayRandomSongs(genreName string) { - params := map[string]string{"size": "100"} - if genreName != "" { - params["genre"] = genreName - } - if songs, err := p.sm.Server.GetRandomSongs(params); err != nil { + if songs, err := p.sm.Server.GetRandomTracks(genreName, 100); err != nil { log.Printf("error getting random songs: %s", err.Error()) } else { p.LoadTracks(songs, false, false) @@ -213,8 +207,7 @@ func (p *PlaybackManager) PlayRandomSongs(genreName string) { } func (p *PlaybackManager) PlaySimilarSongs(id string) { - params := map[string]string{"size": "100"} - if songs, err := p.sm.Server.GetSimilarSongs2(id, params); err != nil { + if songs, err := p.sm.Server.GetSimilarTracks(id, 100); err != nil { log.Printf("error getting similar songs: %s", err.Error()) } else { p.LoadTracks(songs, false, false) @@ -222,8 +215,8 @@ func (p *PlaybackManager) PlaySimilarSongs(id string) { } } -func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child { - pq := make([]*subsonic.Child, len(p.playQueue)) +func (p *PlaybackManager) GetPlayQueue() []*mediaprovider.Track { + pq := make([]*mediaprovider.Track, len(p.playQueue)) for i, tr := range p.playQueue { copy := *tr pq[i] = © @@ -235,11 +228,7 @@ func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child { // this should be called to ensure the in-memory track model is updated. func (p *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) { if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil { - if fav { - tr.Starred = time.Now() - } else { - tr.Starred = time.Time{} - } + tr.Favorite = fav } } @@ -247,13 +236,13 @@ func (p *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) { // this should be called to ensure the in-memory track model is updated. func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) { if tr := sharedutil.FindTrackByID(id, p.playQueue); tr != nil { - tr.UserRating = rating + tr.Rating = rating } } // trackIdxs must be sorted func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) { - newQueue := make([]*subsonic.Child, 0, len(p.playQueue)-len(trackIdxs)) + newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIdxs)) rmCount := 0 rmIdx := 0 for i, tr := range p.playQueue { @@ -310,10 +299,10 @@ func (p *PlaybackManager) checkScrobble(playDur time.Duration) { playDur.Seconds() >= float64(p.scrobbleCfg.ThresholdTimeSeconds) if timeThresholdMet || pcnt >= float64(p.scrobbleCfg.ThresholdPercent) { song := p.playQueue[p.nowPlayingIdx] - log.Printf("Scrobbling %q", song.Title) + log.Printf("Scrobbling %q", song.Name) song.PlayCount += 1 p.lastScrobbled = song - go p.sm.Server.Scrobble(song.ID, map[string]string{"time": strconv.FormatInt(time.Now().Unix()*1000, 10)}) + go p.sm.Server.Scrobble(song.ID, true) } } @@ -322,10 +311,7 @@ func (p *PlaybackManager) sendNowPlayingScrobble() { return } song := p.playQueue[p.nowPlayingIdx] - go p.sm.Server.Scrobble(song.ID, map[string]string{ - "time": strconv.FormatInt(time.Now().Unix()*1000, 10), - "submission": "false", - }) + go p.sm.Server.Scrobble(song.ID, false) } func (p *PlaybackManager) invokeOnSongChangeCallbacks() { diff --git a/backend/servermanager.go b/backend/servermanager.go index eb9f1b3..1698700 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -7,13 +7,16 @@ import ( "time" "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" + subsonicMP "github.com/dweymouth/supersonic/backend/mediaprovider/subsonic" "github.com/google/uuid" "github.com/zalando/go-keyring" ) type ServerManager struct { - ServerID uuid.UUID - Server *subsonic.Client + LoggedInUser string + ServerID uuid.UUID + Server mediaprovider.MediaProvider appName string onServerConnected []func() @@ -31,7 +34,8 @@ func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) err if err != nil { return err } - s.Server = cli + s.Server = subsonicMP.SubsonicMediaProvider(cli) + s.LoggedInUser = conf.Username s.ServerID = conf.ID for _, cb := range s.onServerConnected { cb() @@ -116,6 +120,7 @@ func (s *ServerManager) Logout() { cb() } s.Server = nil + s.LoggedInUser = "" s.ServerID = uuid.UUID{} } } diff --git a/backend/trackiterator.go b/backend/trackiterator.go deleted file mode 100644 index ef9fc98..0000000 --- a/backend/trackiterator.go +++ /dev/null @@ -1,140 +0,0 @@ -package backend - -import ( - "log" - - "github.com/dweymouth/go-subsonic/subsonic" -) - -func (l *LibraryManager) AllTracksIterator() TrackIterator { - return &allTracksIterator{ - l: l, - albumIter: l.AlbumsIter(AlbumSortArtistAZ, AlbumFilter{}), - } -} - -func (l *LibraryManager) SearchTracksIterator(query string) TrackIterator { - return &searchTracksIterator{ - searchIterBase: searchIterBase{ - s: l.s.Server, - query: query, - }, - trackIDset: make(map[string]bool), - } -} - -type allTracksIterator struct { - l *LibraryManager - albumIter AlbumIterator - curAlbum *subsonic.AlbumID3 - curTrackIdx int - done bool -} - -func (a *allTracksIterator) Next() *subsonic.Child { - if a.done { - return nil - } - - // fetch next album - if a.curAlbum == nil || a.curTrackIdx >= len(a.curAlbum.Song) { - al := a.albumIter.Next() - if al == nil { - a.done = true - return nil - } - al, err := a.l.s.Server.GetAlbum(al.ID) - if err != nil { - log.Printf("error fetching album: %s", err.Error()) - } - if len(al.Song) == 0 { - // in the unlikely case of an album with zero tracks, - // just call recursively to move to next album - return a.Next() - } - a.curAlbum = al - a.curTrackIdx = 0 - } - - tr := a.curAlbum.Song[a.curTrackIdx] - a.curTrackIdx += 1 - return tr -} - -type searchTracksIterator struct { - searchIterBase - - prefetched []*subsonic.Child - prefetchedPos int - trackIDset map[string]bool - done bool -} - -func (s *searchTracksIterator) Next() *subsonic.Child { - if s.done { - return nil - } - - // prefetch more search results from server - if len(s.prefetched) == 0 { - results := s.searchIterBase.fetchResults() - - if results != nil { - // add results from songs search - s.addNewTracks(results.Song) - s.songOffset += len(results.Song) - - // add results from artists search - for _, artist := range results.Artist { - artist, err := s.s.GetArtist(artist.ID) - if err != nil { - log.Printf("error fetching artist: %s", err.Error()) - } else { - s.addNewTracksFromAlbums(artist.Album) - } - } - s.artistOffset += len(results.Artist) - - // add results from albums search - s.addNewTracksFromAlbums(results.Album) - s.albumOffset += len(results.Album) - } - } - - // return from prefetched results - if len(s.prefetched) > 0 { - tr := s.prefetched[s.prefetchedPos] - s.prefetchedPos++ - if s.prefetchedPos == len(s.prefetched) { - s.prefetched = s.prefetched[:0] - s.prefetchedPos = 0 - } - return tr - } - - // no more results - s.done = true - s.prefetched = nil - s.trackIDset = nil - return nil -} - -func (s *searchTracksIterator) addNewTracks(tracks []*subsonic.Child) { - for _, tr := range tracks { - if _, have := s.trackIDset[tr.ID]; have { - continue - } - s.prefetched = append(s.prefetched, tr) - s.trackIDset[tr.ID] = true - } -} - -func (s *searchTracksIterator) addNewTracksFromAlbums(albums []*subsonic.AlbumID3) { - for _, al := range albums { - if album, err := s.s.GetAlbum(al.ID); err != nil { - log.Printf("error fetching album: %s", err.Error()) - } else { - s.addNewTracks(album.Song) - } - } -} diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index d07098c..d01268c 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -4,7 +4,7 @@ import ( "math" "sort" - "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" ) func SliceContains[T comparable](ts []T, t T) bool { @@ -40,7 +40,7 @@ func MapSlice[T any, U any](ts []T, f func(T) U) []U { return result } -func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child { +func FindTrackByID(id string, tracks []*mediaprovider.Track) *mediaprovider.Track { for _, tr := range tracks { if id == tr.ID { return tr @@ -49,15 +49,15 @@ func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child { return nil } -func TrackIDOrEmptyStr(track *subsonic.Child) string { +func TrackIDOrEmptyStr(track *mediaprovider.Track) string { if track == nil { return "" } return track.ID } -func TracksToIDs(tracks []*subsonic.Child) []string { - return MapSlice(tracks, func(tr *subsonic.Child) string { +func TracksToIDs(tracks []*mediaprovider.Track) []string { + return MapSlice(tracks, func(tr *mediaprovider.Track) string { return tr.ID }) } @@ -73,8 +73,8 @@ const ( // Reorder tracks and return a new track slice. // idxToMove must contain only valid indexes into tracks, and no repeats -func ReorderTracks(tracks []*subsonic.Child, idxToMove []int, op TrackReorderOp) []*subsonic.Child { - newTracks := make([]*subsonic.Child, len(tracks)) +func ReorderTracks(tracks []*mediaprovider.Track, idxToMove []int, op TrackReorderOp) []*mediaprovider.Track { + newTracks := make([]*mediaprovider.Track, len(tracks)) switch op { case MoveToTop: topIdx := 0 diff --git a/sharedutil/sharedutil_test.go b/sharedutil/sharedutil_test.go index 0aa2fe7..4e7332f 100644 --- a/sharedutil/sharedutil_test.go +++ b/sharedutil/sharedutil_test.go @@ -3,11 +3,11 @@ package sharedutil import ( "testing" - "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" ) func Test_ReorderTracks(t *testing.T) { - tracks := []*subsonic.Child{ + tracks := []*mediaprovider.Track{ {ID: "a"}, // 0 {ID: "b"}, // 1 {ID: "c"}, // 2 @@ -18,7 +18,7 @@ func Test_ReorderTracks(t *testing.T) { // test MoveToTop: idxToMove := []int{0, 2, 3, 5} - want := []*subsonic.Child{ + want := []*mediaprovider.Track{ {ID: "a"}, {ID: "c"}, {ID: "d"}, @@ -33,7 +33,7 @@ func Test_ReorderTracks(t *testing.T) { // test MoveToBottom: idxToMove = []int{0, 2, 5} - want = []*subsonic.Child{ + want = []*mediaprovider.Track{ {ID: "b"}, {ID: "d"}, {ID: "e"}, @@ -48,7 +48,7 @@ func Test_ReorderTracks(t *testing.T) { // test MoveUp: idxToMove = []int{0, 1, 3, 5} - want = []*subsonic.Child{ + want = []*mediaprovider.Track{ {ID: "a"}, {ID: "b"}, {ID: "d"}, @@ -63,7 +63,7 @@ func Test_ReorderTracks(t *testing.T) { // test MoveDown: idxToMove = []int{2, 4, 5} - want = []*subsonic.Child{ + want = []*mediaprovider.Track{ {ID: "a"}, {ID: "b"}, {ID: "d"}, @@ -77,7 +77,7 @@ func Test_ReorderTracks(t *testing.T) { } } -func tracklistsEqual(t *testing.T, a, b []*subsonic.Child) bool { +func tracklistsEqual(t *testing.T, a, b []*mediaprovider.Track) bool { t.Helper() if len(a) != len(b) { return false diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 4bc2c51..429b969 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -7,6 +7,7 @@ import ( "time" "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" @@ -15,7 +16,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic/subsonic" ) type BottomPanel struct { @@ -69,7 +69,7 @@ func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel contr.NavigateTo(controller.AlbumRoute(bp.playbackManager.NowPlaying().AlbumID)) }) bp.NowPlaying.OnArtistNameTapped(func() { - contr.NavigateTo(controller.ArtistRoute(bp.playbackManager.NowPlaying().ArtistID)) + contr.NavigateTo(controller.ArtistRoute(bp.playbackManager.NowPlaying().ArtistIDs[0])) }) bp.NowPlaying.OnTrackNameTapped(func() { contr.NavigateTo(controller.NowPlayingRoute(bp.playbackManager.NowPlaying().ID)) @@ -108,11 +108,11 @@ func (bp *BottomPanel) SetPlaybackManager(pm *backend.PlaybackManager) { }) } -func (bp *BottomPanel) onSongChange(song *subsonic.Child, _ *subsonic.Child) { +func (bp *BottomPanel) onSongChange(song, _ *mediaprovider.Track) { if song == nil { bp.NowPlaying.Update("", "", false, "", nil) } else { - bp.coverArtID = song.CoverArt + bp.coverArtID = song.CoverArtID var im image.Image if bp.ImageManager != nil { // set image to expire not long after the length of the song @@ -120,9 +120,9 @@ func (bp *BottomPanel) onSongChange(song *subsonic.Child, _ *subsonic.Child) { // be in cache for the next song if it's from the same album, or // if the user navigates to the album page for the track imgTTLSec := song.Duration + 30 - im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArt, time.Duration(imgTTLSec)*time.Second) + im, _ = bp.ImageManager.GetCoverThumbnailWithTTL(song.CoverArtID, time.Duration(imgTTLSec)*time.Second) } - bp.NowPlaying.Update(song.Title, song.Artist, song.ArtistID != "", song.Album, im) + bp.NowPlaying.Update(song.Name, song.ArtistNames[0], song.ArtistIDs[0] != "", song.Album, im) } } diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 32d1d60..fc722ff 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -5,6 +5,7 @@ import ( "log" "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" @@ -17,8 +18,6 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) type AlbumPage struct { @@ -92,11 +91,11 @@ func (a *AlbumPage) Route() controller.Route { return controller.AlbumRoute(a.albumID) } -func (a *AlbumPage) OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) { - if song == nil { +func (a *AlbumPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track) { + if track == nil { a.nowPlayingID = "" } else { - a.nowPlayingID = song.ID + a.nowPlayingID = track.ID } a.tracklist.SetNowPlaying(a.nowPlayingID) a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny)) @@ -116,14 +115,14 @@ func (a *AlbumPage) SelectAll() { // should be called asynchronously func (a *AlbumPage) load() { - album, err := a.lm.GetAlbum(a.albumID) + album, err := a.sm.Server.GetAlbum(a.albumID) if err != nil { log.Printf("Failed to get album: %s", err.Error()) return } a.header.Update(album, a.im) - a.tracklist.ShowDiscNumber = album.Song[0].DiscNumber != album.Song[len(album.Song)-1].DiscNumber - a.tracklist.Tracks = album.Song + a.tracklist.ShowDiscNumber = album.Tracks[0].DiscNumber != album.Tracks[len(album.Tracks)-1].DiscNumber + a.tracklist.Tracks = album.Tracks a.tracklist.SetNowPlaying(a.nowPlayingID) } @@ -215,20 +214,20 @@ func (a *AlbumPageHeader) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(a.container) } -func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageManager) { +func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backend.ImageManager) { a.albumID = album.ID - a.coverID = album.CoverArt - a.artistID = album.ArtistID + a.coverID = album.CoverArtID + a.artistID = album.ArtistIDs[0] a.titleLabel.Segments[0].(*widget.TextSegment).Text = album.Name - a.artistLabel.SetText(album.Artist) - a.genre = album.Genre - a.genreLabel.SetText(album.Genre) + a.artistLabel.SetText(album.ArtistNames[0]) + a.genre = album.Genres[0] + a.genreLabel.SetText(album.Genres[0]) a.miscLabel.SetText(formatMiscLabelStr(album)) - a.toggleFavButton.IsFavorited = !album.Starred.IsZero() + a.toggleFavButton.IsFavorited = album.Favorite a.Refresh() go func() { - if cover, err := im.GetCoverThumbnail(album.CoverArt); err == nil { + if cover, err := im.GetCoverThumbnail(album.CoverArtID); err == nil { a.cover.Image.Image = cover a.cover.Refresh() } else { @@ -238,11 +237,8 @@ func (a *AlbumPageHeader) Update(album *subsonic.AlbumID3, im *backend.ImageMana } func (a *AlbumPageHeader) toggleFavorited() { - if a.toggleFavButton.IsFavorited { - a.page.sm.Server.Star(subsonic.StarParameters{AlbumIDs: []string{a.albumID}}) - } else { - a.page.sm.Server.Unstar(subsonic.StarParameters{AlbumIDs: []string{a.albumID}}) - } + params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}} + a.page.sm.Server.SetFavorite(params, a.toggleFavButton.IsFavorited) } func (a *AlbumPageHeader) showPopUpCover() { @@ -254,16 +250,16 @@ func (a *AlbumPageHeader) showPopUpCover() { a.page.contr.ShowPopUpImage(cover) } -func formatMiscLabelStr(a *subsonic.AlbumID3) string { +func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string { var discs string - if discCount := a.Song[len(a.Song)-1].DiscNumber; discCount > 1 { + if discCount := a.Tracks[len(a.Tracks)-1].DiscNumber; discCount > 1 { discs = fmt.Sprintf("%d discs · ", discCount) } tracks := "tracks" - if a.SongCount == 1 { + if a.TrackCount == 1 { tracks = "track" } - return fmt.Sprintf("%d · %d %s · %s%s", a.Year, a.SongCount, tracks, discs, util.SecondsToTimeString(float64(a.Duration))) + return fmt.Sprintf("%d · %d %s · %s%s", a.Year, a.TrackCount, tracks, discs, util.SecondsToTimeString(float64(a.Duration))) } func (s *albumPageState) Restore() Page { diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 11e2c25..1f49d44 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -2,6 +2,7 @@ package browsing import ( "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/util" @@ -29,7 +30,7 @@ type AlbumsPage struct { searcher *widgets.SearchEntry filterBtn *widgets.AlbumFilterButton searchText string - filter backend.AlbumFilter + filter mediaprovider.AlbumFilter titleDisp *widget.RichText sortOrder *selectWidget container *fyne.Container diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 2a038c2..860646e 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" @@ -118,7 +119,7 @@ func (a *ArtistPage) Save() SavedPage { var _ CanShowNowPlaying = (*ArtistPage)(nil) -func (a *ArtistPage) OnSongChange(track *subsonic.Child, lastScrobbledIfAny *subsonic.Child) { +func (a *ArtistPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track) { a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(track) if a.tracklistCtr != nil { tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go index 92aa1e5..e9fa79c 100644 --- a/ui/browsing/browsingpane.go +++ b/ui/browsing/browsingpane.go @@ -2,6 +2,7 @@ package browsing import ( "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" @@ -11,8 +12,6 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) type Page interface { @@ -37,7 +36,7 @@ type CanSelectAll interface { } type CanShowNowPlaying interface { - OnSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) + OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) } type BrowsingPane struct { @@ -160,7 +159,7 @@ func (b *BrowsingPane) doSetPage(p Page) bool { return true } -func (b *BrowsingPane) onSongChange(song *subsonic.Child, lastScrobbledIfAny *subsonic.Child) { +func (b *BrowsingPane) onSongChange(song, lastScrobbledIfAny *mediaprovider.Track) { if b.curPage == nil { return } diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index db69f7e..cb47f9e 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -4,6 +4,7 @@ import ( "log" "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" @@ -15,8 +16,6 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) type FavoritesPage struct { @@ -216,7 +215,7 @@ func (a *FavoritesPage) OnSearched(query string) { var _ CanShowNowPlaying = (*FavoritesPage)(nil) -func (a *FavoritesPage) OnSongChange(song *subsonic.Child, _ *subsonic.Child) { +func (a *FavoritesPage) OnSongChange(song, _ *mediaprovider.Track) { a.nowPlayingID = "" if song != nil { a.nowPlayingID = song.ID @@ -271,12 +270,12 @@ func (a *FavoritesPage) onShowFavoriteArtists() { a.createContainer(layout.NewSpacer()) } go func() { - s, err := a.sm.Server.GetStarred2(nil) + fav, err := a.sm.Server.GetFavorites() if err != nil { log.Printf("error getting starred items: %s", err.Error()) return } - model := buildArtistListModel(s.Artist) + model := buildArtistListModel(fav.Artists) artistList := widgets.NewArtistGenreList(model) artistList.ShowAlbumCount = true artistList.OnNavTo = func(artistID string) { @@ -295,7 +294,7 @@ func (a *FavoritesPage) onShowFavoriteArtists() { } } -func buildArtistListModel(artists []*subsonic.ArtistID3) []widgets.ArtistGenreListItemModel { +func buildArtistListModel(artists []*mediaprovider.Artist) []widgets.ArtistGenreListItemModel { model := make([]widgets.ArtistGenreListItemModel, 0) for _, ar := range artists { model = append(model, widgets.ArtistGenreListItemModel{ @@ -320,12 +319,12 @@ func (a *FavoritesPage) onShowFavoriteSongs() { a.createContainer(layout.NewSpacer()) } go func() { - s, err := a.sm.Server.GetStarred2(nil) + fav, err := a.sm.Server.GetFavorites() if err != nil { log.Printf("error getting starred items: %s", err.Error()) return } - tracklist := widgets.NewTracklist(s.Song) + tracklist := widgets.NewTracklist(fav.Tracks) tracklist.AutoNumber = true tracklist.SetVisibleColumns(a.cfg.TracklistColumns) tracklist.OnVisibleColumnsChanged = func(cols []string) { @@ -360,7 +359,7 @@ type savedFavoritesPage struct { lm *backend.LibraryManager gridState widgets.GridViewState searchGridState widgets.GridViewState - filter backend.AlbumFilter + filter mediaprovider.AlbumFilter searchText string activeToggleBtn int } diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 0abb904..03baf35 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/ui/browsing" "github.com/dweymouth/supersonic/ui/controller" @@ -15,7 +16,6 @@ import ( "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic/subsonic" ) var ( @@ -81,12 +81,12 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) m.Window.SetContent(m.container) m.Window.Resize(size) - app.PlaybackManager.OnSongChange(func(song *subsonic.Child, _ *subsonic.Child) { + app.PlaybackManager.OnSongChange(func(song, _ *mediaprovider.Track) { if song == nil { m.Window.SetTitle(appName) return } - m.Window.SetTitle(fmt.Sprintf("%s – %s · %s", song.Title, song.Artist, appName)) + m.Window.SetTitle(fmt.Sprintf("%s – %s · %s", song.Name, song.ArtistNames[0], appName)) }) app.ServerManager.OnServerConnected(func() { m.BrowsingPane.EnableNavigationButtons() diff --git a/ui/widgets/albumfilterbutton.go b/ui/widgets/albumfilterbutton.go index 0fc59b2..e1cb468 100644 --- a/ui/widgets/albumfilterbutton.go +++ b/ui/widgets/albumfilterbutton.go @@ -9,7 +9,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" ) @@ -21,11 +21,11 @@ type AlbumFilterButton struct { GenreDisabled bool FavoriteDisabled bool - filter *backend.AlbumFilter + filter *mediaprovider.AlbumFilter dialog *widget.PopUp } -func NewAlbumFilterButton(filter *backend.AlbumFilter) *AlbumFilterButton { +func NewAlbumFilterButton(filter *mediaprovider.AlbumFilter) *AlbumFilterButton { a := &AlbumFilterButton{ filter: filter, Button: widget.Button{ diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 0bcd29d..3b455a7 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -5,8 +5,8 @@ import ( "log" "strconv" "sync" - "time" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/os" @@ -18,7 +18,6 @@ import ( "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic/subsonic" ) const ( @@ -40,7 +39,7 @@ type Tracklist struct { // Tracks is the set of tracks displayed by the widget. // Direct access to this is not thread-safe but OK for // views that only load tracks into the widget once at page load. - Tracks []*subsonic.Child + Tracks []*mediaprovider.Track // AutoNumber sets whether to auto-number the tracks 1..N in display order, // or to use the number from the track's metadata @@ -60,8 +59,8 @@ type Tracklist struct { // user action callbacks OnPlayTrackAt func(int) - OnPlaySelection func(tracks []*subsonic.Child, shuffle bool) - OnAddToQueue func(trackIDs []*subsonic.Child) + OnPlaySelection func(tracks []*mediaprovider.Track, shuffle bool) + OnAddToQueue func(trackIDs []*mediaprovider.Track) OnAddToPlaylist func(trackIDs []string) OnSetFavorite func(trackIDs []string, fav bool) OnSetRating func(trackIDs []string, rating int) @@ -85,7 +84,7 @@ type Tracklist struct { container *fyne.Container } -func NewTracklist(tracks []*subsonic.Child) *Tracklist { +func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { t := &Tracklist{Tracks: tracks, visibleColumns: make([]bool, 12)} t.ExtendBaseWidget(t) @@ -144,7 +143,7 @@ func (t *Tracklist) buildHeader() { } // Gets the track at the given index. Thread-safe. -func (t *Tracklist) TrackAt(idx int) *subsonic.Child { +func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() if idx >= len(t.Tracks) { @@ -216,7 +215,7 @@ func (t *Tracklist) Clear() { } // Append more tracks to the tracklist. Thread-safe. -func (t *Tracklist) AppendTracks(trs []*subsonic.Child) { +func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) { t.tracksMutex.Lock() defer t.tracksMutex.Unlock() t.Tracks = append(t.Tracks, trs...) @@ -330,16 +329,12 @@ func (t *Tracklist) onSetFavorite(trackID string, fav bool) { t.tracksMutex.RLock() tr := sharedutil.FindTrackByID(trackID, t.Tracks) t.tracksMutex.RUnlock() - t.onSetFavorites([]*subsonic.Child{tr}, fav, false) + t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false) } -func (t *Tracklist) onSetFavorites(tracks []*subsonic.Child, fav bool, needRefresh bool) { +func (t *Tracklist) onSetFavorites(tracks []*mediaprovider.Track, fav bool, needRefresh bool) { for _, tr := range tracks { - if fav { - tr.Starred = time.Now() - } else { - tr.Starred = time.Time{} - } + tr.Favorite = fav } if needRefresh { t.Refresh() @@ -355,12 +350,12 @@ func (t *Tracklist) onSetRating(trackID string, rating int) { t.tracksMutex.RLock() tr := sharedutil.FindTrackByID(trackID, t.Tracks) t.tracksMutex.RUnlock() - t.onSetRatings([]*subsonic.Child{tr}, rating, false) + t.onSetRatings([]*mediaprovider.Track{tr}, rating, false) } -func (t *Tracklist) onSetRatings(tracks []*subsonic.Child, rating int, needRefresh bool) { +func (t *Tracklist) onSetRatings(tracks []*mediaprovider.Track, rating int, needRefresh bool) { for _, tr := range tracks { - tr.UserRating = rating + tr.Rating = rating } if needRefresh { t.Refresh() @@ -383,9 +378,9 @@ func (t *Tracklist) onAlbumTapped(albumID string) { } } -func (t *Tracklist) selectedTracks() []*subsonic.Child { +func (t *Tracklist) selectedTracks() []*mediaprovider.Track { sel := t.selectionMgr.GetSelection() - tracks := make([]*subsonic.Child, 0, len(sel)) + tracks := make([]*mediaprovider.Track, 0, len(sel)) t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() for _, idx := range sel { @@ -484,7 +479,7 @@ type TrackRow struct { albumID string isPlaying bool isFavorite bool - playCount int64 + playCount int num *widget.RichText name *widget.RichText @@ -543,37 +538,37 @@ func newTrailingAlignRichText() *widget.RichText { return rt } -func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) { +func (t *TrackRow) Update(tr *mediaprovider.Track, rowNum int) { // Update info that can change if this row is bound to - // a new track (*subsonic.Child) + // a new track (*mediaprovider.Track) if tr.ID != t.trackID { if t.Focused { fyne.CurrentApp().Driver().CanvasForObject(t).Focus(nil) t.Focused = false } t.trackID = tr.ID - t.artistID = tr.ArtistID + t.artistID = tr.ArtistIDs[0] t.albumID = tr.AlbumID - t.name.Segments[0].(*widget.TextSegment).Text = tr.Title - t.artist.SetText(tr.Artist) - t.artist.Disabled = tr.ArtistID == "" + t.name.Segments[0].(*widget.TextSegment).Text = tr.Name + t.artist.SetText(tr.ArtistNames[0]) + t.artist.Disabled = tr.ArtistIDs[0] == "" t.album.SetText(tr.Album) t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration)) t.year.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Year) t.plays.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(int(tr.PlayCount)) t.bitrate.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.BitRate) t.size.Segments[0].(*widget.TextSegment).Text = util.BytesToSizeString(tr.Size) - t.path.Segments[0].(*widget.TextSegment).Text = tr.Path + t.path.Segments[0].(*widget.TextSegment).Text = tr.FilePath } // Update track num if needed - // (which can change based on bound *subsonic.Child or tracklist.AutoNumber) + // (which can change based on bound *mediaprovider.Track or tracklist.AutoNumber) if t.trackNum != rowNum { discNum := -1 var str string if rowNum < 0 { - rowNum = tr.Track + rowNum = tr.TrackNumber if t.tracklist.ShowDiscNumber { discNum = tr.DiscNumber } @@ -612,7 +607,7 @@ func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) { } // Render favorite column - if tr.Starred.IsZero() { + if tr.Favorite { t.isFavorite = false t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon } else { @@ -620,7 +615,7 @@ func (t *TrackRow) Update(tr *subsonic.Child, rowNum int) { t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon } - t.rating.Rating = tr.UserRating + t.rating.Rating = tr.Rating // Show only columns configured to be visible t.artist.Hidden = !t.tracklist.visibleColumns[ColNumber(ColumnArtist)] diff --git a/ui/widgets/tracklistloader.go b/ui/widgets/tracklistloader.go index 5bf6a64..5997281 100644 --- a/ui/widgets/tracklistloader.go +++ b/ui/widgets/tracklistloader.go @@ -1,25 +1,23 @@ package widgets import ( - "github.com/dweymouth/supersonic/backend" - - "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" ) // Component that manages lazily loading more tracks into a Tracklist // as the user scrolls near the bottom. type TracklistLoader struct { tracklist *Tracklist - iter backend.TrackIterator + iter mediaprovider.TrackIterator - trackBuffer []*subsonic.Child + trackBuffer []*mediaprovider.Track fetching bool done bool len int highestShown int } -func NewTracklistLoader(tracklist *Tracklist, iter backend.TrackIterator) TracklistLoader { +func NewTracklistLoader(tracklist *Tracklist, iter mediaprovider.TrackIterator) TracklistLoader { t := TracklistLoader{ tracklist: tracklist, iter: iter, @@ -44,7 +42,7 @@ func (t *TracklistLoader) loadMoreTracks(num int) { // repeat fetch task as long as user has scrolled near bottom for !t.done && t.highestShown >= t.len-25 { if t.trackBuffer == nil { - t.trackBuffer = make([]*subsonic.Child, 0, num) + t.trackBuffer = make([]*mediaprovider.Track, 0, num) } t.trackBuffer = t.trackBuffer[:0] for i := 0; i < num; i++ {