From 7d293af45bfc80a03fed47feb5414282dce7f935 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 13 May 2023 17:31:39 -0700 Subject: [PATCH 01/11] begin defining a generic MediaProvider interface and model --- backend/mediaprovider/mediaprovider.go | 64 ++++++++++++++++++++++++++ backend/mediaprovider/model.go | 61 ++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 backend/mediaprovider/mediaprovider.go create mode 100644 backend/mediaprovider/model.go diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go new file mode 100644 index 0000000..a8e61c2 --- /dev/null +++ b/backend/mediaprovider/mediaprovider.go @@ -0,0 +1,64 @@ +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 { + GetAlbum(albumID string) (*AlbumWithTracks, error) + + GetArtist(artistID string) (*Artist, error) + + GetPlaylist(playlistID string) (*PlaylistWithTracks, error) + + GetCoverArt(coverArtID string, size int) (image.Image, error) + + AlbumSortOrders() []string + + IterateAlbums(sortOrder string, searchQuery string, filter AlbumFilter) AlbumIterator + + IterateTracks(searchQuery string) TrackIterator + + GetRandomTracks(genre string, count int) ([]Track, error) + + GetSimilarTracks(artistID string, count int) ([]Track, error) + + GetArtists() ([]Artist, error) + + GetGenres() ([]Genre, error) + + GetFavorites() (Favorites, error) + + GetPlaylists() ([]Playlist, error) + + SetFavorite(params RatingFavoriteParameters) error + + SetRating(params RatingFavoriteParameters) error +} diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go new file mode 100644 index 0000000..64f7a50 --- /dev/null +++ b/backend/mediaprovider/model.go @@ -0,0 +1,61 @@ +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 + Albums []Album +} + +type Genre struct { + Name string + AlbumCount int + TrackCount int +} + +type Track struct { + ID string + CoverArtID string + ParentID string + Name string + Duration int + Genre string + ArtistIDs []string + ArtistNames []string + Rating int + Favorite bool + Size int64 + PlayCount int64 + FilePath string +} + +type Playlist struct { + ID string + CoverArtID string + Name string + Description string + Public bool + Owner string +} + +type PlaylistWithTracks struct { + Playlist + Tracks []Track +} From c4436a4887362a830b874c2220015ebb4bef7ee7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 14 May 2023 11:38:06 -0700 Subject: [PATCH 02/11] more work on generic MediaProvider interface --- backend/mediaprovider/mediaprovider.go | 20 ++++++++++++++++--- backend/mediaprovider/model.go | 19 +++++++++++++++--- .../subsonic/subsonicmediaprovider.go | 0 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 backend/mediaprovider/subsonic/subsonicmediaprovider.go diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index a8e61c2..126e7d0 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -34,7 +34,9 @@ type Favorites struct { type MediaProvider interface { GetAlbum(albumID string) (*AlbumWithTracks, error) - GetArtist(artistID string) (*Artist, error) + GetArtist(artistID string) (*ArtistWithAlbums, error) + + GetArtistInfo(artistID string) (*ArtistInfo, error) GetPlaylist(playlistID string) (*PlaylistWithTracks, error) @@ -56,9 +58,21 @@ type MediaProvider interface { GetFavorites() (Favorites, error) - GetPlaylists() ([]Playlist, error) - SetFavorite(params RatingFavoriteParameters) error SetRating(params RatingFavoriteParameters) error + + GetPlaylists() ([]Playlist, error) + + CreatePlaylist(name string, trackIDs []string) error + + EditPlaylist(id, name, description string, public bool) error + + EditPlaylistTracks(id string, trackIDsToAdd, trackIDsToRemove []string) error + + ReplacePlaylistTracks(id string, trackIDs []string) error + + DeletePlaylist(id string) error + + Scrobble(trackID string, submission bool) error } diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index 64f7a50..366a691 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -19,11 +19,23 @@ type AlbumWithTracks struct { } type Artist struct { - ID string - Name string + ID string + Name string + 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 @@ -39,10 +51,11 @@ type Track struct { Genre string ArtistIDs []string ArtistNames []string + Album string Rating int Favorite bool Size int64 - PlayCount int64 + PlayCount int FilePath string } diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go new file mode 100644 index 0000000..e69de29 From e34497ec24e22e303a1f83a5e3d7d7aa54e90851 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 14 May 2023 17:48:06 -0700 Subject: [PATCH 03/11] bunch of work on Subsonic mediaprovider adapter --- backend/mediaprovider/mediaprovider.go | 2 +- backend/mediaprovider/model.go | 3 + .../mediaprovider/subsonic/albumiterator.go | 355 ++++++++++++++++++ .../mediaprovider/subsonic/searchiterbase.go | 33 ++ .../subsonic/subsonicmediaprovider.go | 272 ++++++++++++++ 5 files changed, 664 insertions(+), 1 deletion(-) create mode 100644 backend/mediaprovider/subsonic/albumiterator.go create mode 100644 backend/mediaprovider/subsonic/searchiterbase.go diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 126e7d0..d9244e5 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -68,7 +68,7 @@ type MediaProvider interface { EditPlaylist(id, name, description string, public bool) error - EditPlaylistTracks(id string, trackIDsToAdd, trackIDsToRemove []string) error + EditPlaylistTracks(id string, trackIDsToAdd []string, trackIndexesToRemove []int) error ReplacePlaylistTracks(id string, trackIDs []string) error diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index 366a691..9aa00f9 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -48,6 +48,8 @@ type Track struct { ParentID string Name string Duration int + TrackNumber int + DiscNumber int Genre string ArtistIDs []string ArtistNames []string @@ -66,6 +68,7 @@ type Playlist struct { Description string Public bool Owner string + TrackCount int } type PlaylistWithTracks struct { diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go new file mode 100644 index 0000000..4e8a4db --- /dev/null +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -0,0 +1,355 @@ +package subsonic + +import ( + "log" + "strconv" + "strings" + + "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/sharedutil" +) + +func filterMatches(f mediaprovider.AlbumFilter, 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 filterIsEmpty(f mediaprovider.AlbumFilter) bool { + return !f.ExcludeFavorited && !f.ExcludeUnfavorited && + f.MinYear == 0 && f.MaxYear == 0 && len(f.Genres) == 0 +} + +func (s *subsonicMediaProvider) IterateAlbums(sortOrder, searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { + if searchQuery != "" { + return s.newSearchIter(searchQuery, filter) + } + if sortOrder == "" && len(filter.Genres) == 1 { + return s.newBaseIter("byGenre", filter, map[string]string{"genre": filter.Genres[0]}) + } + if sortOrder == "" && filter.ExcludeUnfavorited { + return s.newBaseIter("starred", filter, make(map[string]string)) + } + switch sortOrder { + case AlbumSortRecentlyAdded: + return s.newBaseIter("newest", filter, make(map[string]string)) + case AlbumSortRecentlyPlayed: + return s.newBaseIter("recent", filter, make(map[string]string)) + case AlbumSortFrequentlyPlayed: + return s.newBaseIter("frequent", filter, make(map[string]string)) + case AlbumSortRandom: + return s.newRandomIter() + case AlbumSortTitleAZ: + return s.newBaseIter("alphabeticalByName", filter, make(map[string]string)) + case AlbumSortArtistAZ: + return s.newBaseIter("alphabeticalByArtist", filter, make(map[string]string)) + case AlbumSortYearAscending: + return s.newBaseIter("byYear", filter, map[string]string{"fromYear": "0", "toYear": "3000"}) + case AlbumSortYearDescending: + return s.newBaseIter("byYear", filter, map[string]string{"fromYear": "3000", "toYear": "0"}) + default: + log.Printf("Undefined album sort order: %s", sortOrder) + return nil + } +} + +type baseIter struct { + listType string + filter mediaprovider.AlbumFilter + serverPos int + s *subsonic.Client + opts map[string]string + prefetched []mediaprovider.Album + prefetchedPos int + done bool +} + +func (s *subsonicMediaProvider) newBaseIter(listType string, filter mediaprovider.AlbumFilter, opts map[string]string) *baseIter { + return &baseIter{ + listType: listType, + filter: filter, + s: s.client, + opts: opts, + } +} + +func (r *baseIter) Next() *mediaprovider.Album { + 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, func(al *subsonic.AlbumID3) bool { return filterMatches(r.filter, al) }) + r.prefetched = sharedutil.MapSlice(albums, toAlbum) + if len(albums) > 0 { + break + } + } + r.prefetchedPos = 1 + /* + if r.l.PreCacheCoverFn != nil { + for _, album := range r.prefetched { + go r.l.PreCacheCoverFn(album.CoverArt) + } + } + */ + + ret := r.prefetched[0] + return &ret +} + +type searchIter struct { + searchIterBase + + filter mediaprovider.AlbumFilter + prefetched []*subsonic.AlbumID3 + prefetchedPos int + albumIDset map[string]bool + done bool +} + +func (s *subsonicMediaProvider) newSearchIter(query string, filter mediaprovider.AlbumFilter) *searchIter { + return &searchIter{ + searchIterBase: searchIterBase{ + query: query, + s: s.client, + }, + filter: filter, + albumIDset: make(map[string]bool), + } +} + +func (s *searchIter) Next() *mediaprovider.Album { + 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 + } + + al := toAlbum(a) + return &al + } + + return nil +} + +func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) { + for _, album := range al { + if _, have := s.albumIDset[album.ID]; have { + continue + } + if filterMatches(s.filter, 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 + 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 (s *subsonicMediaProvider) newRandomIter() *randomIter { + return &randomIter{ + s: s.client, + albumIDSet: make(map[string]bool), + } +} + +func (r *randomIter) Next() *mediaprovider.Album { + 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 + } + + al := toAlbum(a) + return &al + } + + 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/mediaprovider/subsonic/searchiterbase.go b/backend/mediaprovider/subsonic/searchiterbase.go new file mode 100644 index 0000000..b423dda --- /dev/null +++ b/backend/mediaprovider/subsonic/searchiterbase.go @@ -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 +} diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index e69de29..0023591 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -0,0 +1,272 @@ +package subsonic + +import ( + "image" + "log" + "strconv" + + "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/sharedutil" +) + +const ( + AlbumSortRecentlyAdded string = "Recently Added" + AlbumSortRecentlyPlayed string = "Recently Played" + AlbumSortFrequentlyPlayed string = "Frequently Played" + AlbumSortRandom string = "Random" + AlbumSortTitleAZ string = "Title (A-Z)" + AlbumSortArtistAZ string = "Artist (A-Z)" + AlbumSortYearAscending string = "Year (ascending)" + AlbumSortYearDescending string = "Year (descending)" +) + +type subsonicMediaProvider struct { + client *subsonic.Client +} + +func SubsonicMediaProvider(subsonicClient *subsonic.Client) *subsonicMediaProvider /*mediaprovider.MediaProvider*/ { + return &subsonicMediaProvider{client: subsonicClient} +} + +func (s *subsonicMediaProvider) AlbumSortOrders() []string { + return []string{ + AlbumSortRecentlyAdded, + AlbumSortRecentlyPlayed, + AlbumSortFrequentlyPlayed, + AlbumSortRandom, + AlbumSortTitleAZ, + AlbumSortArtistAZ, + AlbumSortYearAscending, + AlbumSortYearDescending, + } +} + +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 + } + return &mediaprovider.AlbumWithTracks{ + Album: mediaprovider.Album{ + ID: al.ID, + Name: al.Name, + ArtistIDs: []string{al.ArtistID}, + CoverArtID: al.CoverArt, + ArtistNames: []string{al.Artist}, + Genres: []string{al.Genre}, + Year: al.Year, + TrackCount: al.SongCount, + Favorite: !al.Starred.IsZero(), + Duration: al.Duration, + }, + Tracks: sharedutil.MapSlice(al.Song, s.toTrack), + }, 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, + 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) { + return s.client.GetCoverArt(id, map[string]string{"size": strconv.Itoa(size)}) +} + +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, s.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 + } + return &mediaprovider.PlaylistWithTracks{ + Playlist: toPlaylist(pl), + Tracks: sharedutil.MapSlice(pl.Entry, s.toTrack), + }, 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, s.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, s.toTrack), nil +} + +func (s *subsonicMediaProvider) toTrack(ch *subsonic.Child) mediaprovider.Track { + if ch == nil { + log.Println("subsonicMediaProvider: toTrack called on nil track") + return mediaprovider.Track{} + } + 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, + Rating: ch.UserRating, + Favorite: !ch.Starred.IsZero(), + PlayCount: int(ch.PlayCount), + FilePath: ch.Path, + } +} + +func toAlbum(al *subsonic.AlbumID3) mediaprovider.Album { + if al == nil { + log.Println("subsonicMediaProvider: toAlbum called on nil album") + return mediaprovider.Album{} + } + return mediaprovider.Album{ + ID: al.ID, + CoverArtID: al.CoverArt, + Name: al.Name, + Duration: al.Duration, + ArtistIDs: []string{al.ArtistID}, + ArtistNames: []string{al.Artist}, + Year: al.Year, + Genres: []string{al.Genre}, + TrackCount: al.SongCount, + Favorite: !al.Starred.IsZero(), + } +} + +func toArtist(ar *subsonic.Artist) mediaprovider.Artist { + if ar == nil { + log.Println("subsonicMediaProvider: toArtist called on nil artist") + return mediaprovider.Artist{} + } + return mediaprovider.Artist{ + ID: ar.ID, + Name: ar.Name, + } +} + +func toArtistFromID3(ar *subsonic.ArtistID3) mediaprovider.Artist { + if ar == nil { + log.Println("subsonicMediaProvider: toArtistFromID3 called on nil artistID3") + return mediaprovider.Artist{} + } + return mediaprovider.Artist{ + ID: ar.ID, + Name: ar.Name, + AlbumCount: ar.AlbumCount, + } +} + +func toPlaylist(pl *subsonic.Playlist) mediaprovider.Playlist { + return mediaprovider.Playlist{ + Name: pl.Name, + Description: pl.Comment, + Owner: pl.Owner, + Public: pl.Public, + TrackCount: pl.SongCount, + } +} From 8984d4ab3938efde2a2b51b1da1f9c6ec73d7929 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 14 May 2023 17:55:44 -0700 Subject: [PATCH 04/11] add trackiterator to subsonic adapter --- .../subsonic/subsonicmediaprovider.go | 12 +- .../mediaprovider/subsonic/trackiterator.go | 141 ++++++++++++++++++ 2 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 backend/mediaprovider/subsonic/trackiterator.go diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 0023591..90009be 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -80,7 +80,7 @@ func (s *subsonicMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWi Favorite: !al.Starred.IsZero(), Duration: al.Duration, }, - Tracks: sharedutil.MapSlice(al.Song, s.toTrack), + Tracks: sharedutil.MapSlice(al.Song, toTrack), }, nil } @@ -138,7 +138,7 @@ func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) return mediaprovider.Favorites{ Albums: sharedutil.MapSlice(fav.Album, toAlbum), Artists: sharedutil.MapSlice(fav.Artist, toArtistFromID3), - Tracks: sharedutil.MapSlice(fav.Song, s.toTrack), + Tracks: sharedutil.MapSlice(fav.Song, toTrack), }, nil } @@ -163,7 +163,7 @@ func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.P } return &mediaprovider.PlaylistWithTracks{ Playlist: toPlaylist(pl), - Tracks: sharedutil.MapSlice(pl.Entry, s.toTrack), + Tracks: sharedutil.MapSlice(pl.Entry, toTrack), }, nil } @@ -184,7 +184,7 @@ func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([] if err != nil { return nil, err } - return sharedutil.MapSlice(tr, s.toTrack), nil + return sharedutil.MapSlice(tr, toTrack), nil } func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([]mediaprovider.Track, error) { @@ -192,10 +192,10 @@ func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([] if err != nil { return nil, err } - return sharedutil.MapSlice(tr, s.toTrack), nil + return sharedutil.MapSlice(tr, toTrack), nil } -func (s *subsonicMediaProvider) 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{} diff --git a/backend/mediaprovider/subsonic/trackiterator.go b/backend/mediaprovider/subsonic/trackiterator.go new file mode 100644 index 0000000..4930749 --- /dev/null +++ b/backend/mediaprovider/subsonic/trackiterator.go @@ -0,0 +1,141 @@ +package subsonic + +import ( + "log" + + "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" +) + +func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider.TrackIterator { + if searchQuery == "" { + return &allTracksIterator{ + s: s, + albumIter: s.IterateAlbums(AlbumSortArtistAZ, "", mediaprovider.AlbumFilter{}), + } + } + return &searchTracksIterator{ + searchIterBase: searchIterBase{ + s: s.client, + query: searchQuery, + }, + trackIDset: make(map[string]bool), + } +} + +type allTracksIterator struct { + s *subsonicMediaProvider + albumIter mediaprovider.AlbumIterator + curAlbum *mediaprovider.AlbumWithTracks + curTrackIdx int + done bool +} + +func (a *allTracksIterator) Next() *mediaprovider.Track { + if a.done { + return nil + } + + // fetch next album + if a.curAlbum == nil || a.curTrackIdx >= len(a.curAlbum.Tracks) { + al := a.albumIter.Next() + if al == nil { + a.done = true + return nil + } + alWithTracks, err := a.s.GetAlbum(al.ID) + if err != nil { + log.Printf("error fetching album: %s", err.Error()) + } + if len(alWithTracks.Tracks) == 0 { + // in the unlikely case of an album with zero tracks, + // just call recursively to move to next album + return a.Next() + } + a.curAlbum = alWithTracks + a.curTrackIdx = 0 + } + + tr := a.curAlbum.Tracks[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() *mediaprovider.Track { + 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 + } + track := toTrack(tr) + return &track + } + + // 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) + } + } +} From 0ebbf621b1b1e56b1cbce983f7a84ff7fc9a603d Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 14 May 2023 18:59:38 -0700 Subject: [PATCH 05/11] finish mediaprovider interface for subsonic --- backend/mediaprovider/mediaprovider.go | 4 +- .../subsonic/subsonicmediaprovider.go | 70 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index d9244e5..c428f15 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -58,9 +58,11 @@ type MediaProvider interface { GetFavorites() (Favorites, error) + GetStreamURL(trackID string) (string, error) + SetFavorite(params RatingFavoriteParameters) error - SetRating(params RatingFavoriteParameters) error + SetRating(params RatingFavoriteParameters, rating int) error GetPlaylists() ([]Playlist, error) diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 90009be..876e79d 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -3,7 +3,10 @@ package subsonic import ( "image" "log" + "math" "strconv" + "sync" + "time" "github.com/dweymouth/go-subsonic/subsonic" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -25,7 +28,7 @@ type subsonicMediaProvider struct { client *subsonic.Client } -func SubsonicMediaProvider(subsonicClient *subsonic.Client) *subsonicMediaProvider /*mediaprovider.MediaProvider*/ { +func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider { return &subsonicMediaProvider{client: subsonicClient} } @@ -195,6 +198,71 @@ func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([] 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) 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) error { + return s.client.Star(subsonic.StarParameters{ + AlbumIDs: params.AlbumIDs, + ArtistIDs: params.ArtistIDs, + SongIDs: params.TrackIDs, + }) +} + +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++ { + if wg != nil { + wg.Add(1) + } + go func(idx int) { + newErr := s.client.SetRating(params.TrackIDs[idx], rating) + if err == nil && newErr != nil { + err = newErr + } + if wg != nil { + wg.Done() + } + }(offs + i) + } + } + + if len(params.TrackIDs) <= 5 { + // one batch only - no need to use wait group + batchSetRating(0, nil) + } else { + go func() { + 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 { log.Println("subsonicMediaProvider: toTrack called on nil track") From 85042fcbd40728e9e42a2fb9172b560b04ab082c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 14 May 2023 19:31:04 -0700 Subject: [PATCH 06/11] begin the refactoring - doesnt compile --- backend/albumiterator.go | 397 ------------------ backend/imagemanager.go | 6 +- backend/librarymanager.go | 14 - backend/mediaprovider/mediaprovider.go | 16 +- backend/mediaprovider/model.go | 11 +- .../mediaprovider/subsonic/albumiterator.go | 13 +- .../subsonic/subsonicmediaprovider.go | 62 +-- .../mediaprovider/subsonic/trackiterator.go | 5 +- backend/playbackmanager.go | 56 +-- backend/servermanager.go | 11 +- backend/trackiterator.go | 140 ------ sharedutil/sharedutil.go | 14 +- sharedutil/sharedutil_test.go | 14 +- ui/bottompanel.go | 12 +- ui/browsing/albumpage.go | 46 +- ui/browsing/albumspage.go | 3 +- ui/browsing/artistpage.go | 3 +- ui/browsing/browsingpane.go | 7 +- ui/browsing/favoritespage.go | 17 +- ui/mainwindow.go | 6 +- ui/widgets/albumfilterbutton.go | 6 +- ui/widgets/tracklist.go | 59 ++- ui/widgets/tracklistloader.go | 12 +- 23 files changed, 183 insertions(+), 747 deletions(-) delete mode 100644 backend/albumiterator.go delete mode 100644 backend/trackiterator.go 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++ { From fd106560d09b8567bc040cc8e6ddd96666383e89 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 14 May 2023 20:50:06 -0700 Subject: [PATCH 07/11] more refactoring - compiles now but not extensively tested --- backend/app.go | 8 +- backend/config.go | 2 +- backend/librarymanager.go | 25 ------- backend/mediaprovider/mediaprovider.go | 4 +- backend/mediaprovider/model.go | 2 + .../subsonic/subsonicmediaprovider.go | 41 ++++++++-- ui/browsing/albumpage.go | 15 ++-- ui/browsing/albumspage.go | 33 +++++---- ui/browsing/artistpage.go | 35 ++++----- ui/browsing/artistsgenrespage.go | 28 +++---- ui/browsing/favoritespage.go | 23 +++--- ui/browsing/genrepage.go | 25 ++++--- ui/browsing/nowplayingpage.go | 5 +- ui/browsing/playlistpage.go | 36 ++++----- ui/browsing/playlistspage.go | 37 +++++----- ui/browsing/router.go | 10 +-- ui/browsing/trackspage.go | 17 ++--- ui/controller/controller.go | 74 +++++-------------- ui/dialogs/editplaylistdialog.go | 6 +- ui/widgets/gridview.go | 39 +++++++--- 20 files changed, 216 insertions(+), 249 deletions(-) delete mode 100644 backend/librarymanager.go diff --git a/backend/app.go b/backend/app.go index 5e94281..0813583 100644 --- a/backend/app.go +++ b/backend/app.go @@ -25,7 +25,6 @@ type App struct { Config *Config ServerManager *ServerManager ImageManager *ImageManager - LibraryManager *LibraryManager PlaybackManager *PlaybackManager Player *player.Player UpdateChecker UpdateChecker @@ -63,11 +62,10 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A a.ServerManager = NewServerManager(appName) 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.LibraryManager.PreCacheCoverFn = func(coverID string) { - _, _ = a.ImageManager.GetCoverThumbnail(coverID) - } + //a.LibraryManager.PreCacheCoverFn = func(coverID string) { + //_, _ = a.ImageManager.GetCoverThumbnail(coverID) + //} return a, nil } diff --git a/backend/config.go b/backend/config.go index 91851db..b071fd2 100644 --- a/backend/config.go +++ b/backend/config.go @@ -124,7 +124,7 @@ func DefaultConfig(appVersionTag string) *Config { TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"}, }, AlbumsPage: AlbumsPageConfig{ - SortOrder: string(AlbumSortRecentlyAdded), + SortOrder: string("Recently Added"), }, ArtistPage: ArtistPageConfig{ InitialView: "Discography", diff --git a/backend/librarymanager.go b/backend/librarymanager.go deleted file mode 100644 index 5f4919d..0000000 --- a/backend/librarymanager.go +++ /dev/null @@ -1,25 +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, - } -} diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 936f345..5839bdf 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -60,11 +60,13 @@ type MediaProvider interface { 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) + GetPlaylists() ([]*Playlist, error) CreatePlaylist(name string, trackIDs []string) error diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index bfa4e18..d4e2d36 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -21,6 +21,7 @@ type AlbumWithTracks struct { type Artist struct { ID string Name string + Favorite bool AlbumCount int } @@ -71,6 +72,7 @@ type Playlist struct { Description string Public bool Owner string + Duration int TrackCount int } diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 5d48469..4eee99a 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -95,6 +95,7 @@ func (s *subsonicMediaProvider) GetArtist(artistID string) (*mediaprovider.Artis Artist: mediaprovider.Artist{ ID: ar.ID, Name: ar.Name, + Favorite: !ar.Starred.IsZero(), AlbumCount: ar.AlbumCount, }, Albums: sharedutil.MapSlice(ar.Album, toAlbum), @@ -168,12 +169,21 @@ func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.P return nil, err } return &mediaprovider.PlaylistWithTracks{ - Playlist: toPlaylist(pl), - Tracks: sharedutil.MapSlice(pl.Entry, toTrack), + Playlist: mediaprovider.Playlist{ + ID: pl.ID, + CoverArtID: pl.CoverArt, + Name: pl.Name, + Description: pl.Comment, + TrackCount: pl.SongCount, + Public: pl.Public, + Owner: pl.Owner, + Duration: pl.Duration, + }, + Tracks: sharedutil.MapSlice(pl.Entry, toTrack), }, nil } -func (s *subsonicMediaProvider) GetPlaylists() ([]mediaprovider.Playlist, error) { +func (s *subsonicMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) { pl, err := s.client.GetPlaylists(map[string]string{}) if err != nil { return nil, err @@ -209,6 +219,18 @@ func (s *subsonicMediaProvider) GetStreamURL(trackID string) (string, error) { 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}) } @@ -319,8 +341,9 @@ func toArtist(ar *subsonic.Artist) *mediaprovider.Artist { return nil } return &mediaprovider.Artist{ - ID: ar.ID, - Name: ar.Name, + ID: ar.ID, + Name: ar.Name, + Favorite: !ar.Starred.IsZero(), } } @@ -331,16 +354,20 @@ func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist { return &mediaprovider.Artist{ ID: ar.ID, Name: ar.Name, + Favorite: !ar.Starred.IsZero(), AlbumCount: ar.AlbumCount, } } -func toPlaylist(pl *subsonic.Playlist) mediaprovider.Playlist { - return mediaprovider.Playlist{ +func toPlaylist(pl *subsonic.Playlist) *mediaprovider.Playlist { + return &mediaprovider.Playlist{ Name: pl.Name, + ID: pl.ID, + CoverArtID: pl.CoverArt, Description: pl.Comment, Owner: pl.Owner, Public: pl.Public, TrackCount: pl.SongCount, + Duration: pl.Duration, } } diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index fc722ff..333b612 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -34,19 +34,17 @@ type AlbumPage struct { type albumPageState struct { albumID string cfg *backend.AlbumPageConfig - lm *backend.LibraryManager + mp mediaprovider.MediaProvider pm *backend.PlaybackManager im *backend.ImageManager - sm *backend.ServerManager contr *controller.Controller } func NewAlbumPage( albumID string, cfg *backend.AlbumPageConfig, - sm *backend.ServerManager, pm *backend.PlaybackManager, - lm *backend.LibraryManager, + mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller, ) *AlbumPage { @@ -54,9 +52,8 @@ func NewAlbumPage( albumPageState: albumPageState{ albumID: albumID, cfg: cfg, - sm: sm, pm: pm, - lm: lm, + mp: mp, im: im, contr: contr, }, @@ -115,7 +112,7 @@ func (a *AlbumPage) SelectAll() { // should be called asynchronously func (a *AlbumPage) load() { - album, err := a.sm.Server.GetAlbum(a.albumID) + album, err := a.mp.GetAlbum(a.albumID) if err != nil { log.Printf("Failed to get album: %s", err.Error()) return @@ -238,7 +235,7 @@ func (a *AlbumPageHeader) Update(album *mediaprovider.AlbumWithTracks, im *backe func (a *AlbumPageHeader) toggleFavorited() { params := mediaprovider.RatingFavoriteParameters{AlbumIDs: []string{a.albumID}} - a.page.sm.Server.SetFavorite(params, a.toggleFavButton.IsFavorited) + a.page.mp.SetFavorite(params, a.toggleFavButton.IsFavorited) } func (a *AlbumPageHeader) showPopUpCover() { @@ -263,5 +260,5 @@ func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string { } 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) } diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 1f49d44..52e9b5b 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -24,7 +24,7 @@ type AlbumsPage struct { contr *controller.Controller pm *backend.PlaybackManager im *backend.ImageManager - lm *backend.LibraryManager + mp mediaprovider.MediaProvider grid *widgets.GridView searchGrid *widgets.GridView searcher *widgets.SearchEntry @@ -55,12 +55,12 @@ func (s *selectWidget) MinSize() fyne.Size { 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{ cfg: cfg, contr: contr, pm: pm, - lm: lm, + mp: mp, im: im, } a.ExtendBaseWidget(a) @@ -69,12 +69,12 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ SizeName: theme.SizeNameHeadingText, } - a.sortOrder = NewSelect(backend.AlbumSortOrders, a.onSortOrderChanged) - if !sharedutil.SliceContains(backend.AlbumSortOrders, cfg.SortOrder) { - cfg.SortOrder = string(backend.AlbumSortRecentlyAdded) + a.sortOrder = NewSelect(mp.AlbumSortOrders(), a.onSortOrderChanged) + if !sharedutil.SliceContains(mp.AlbumSortOrders(), cfg.SortOrder) { + cfg.SortOrder = string(mp.AlbumSortOrders()[0]) } 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) contr.ConnectAlbumGridActions(a.grid) a.createSearchAndFilter() @@ -112,7 +112,7 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { cfg: saved.cfg, contr: saved.contr, pm: saved.pm, - lm: saved.lm, + mp: saved.mp, im: saved.im, searchText: saved.searchText, filter: saved.filter, @@ -123,7 +123,7 @@ func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ SizeName: theme.SizeNameHeadingText, } - a.sortOrder = NewSelect(backend.AlbumSortOrders, nil) + a.sortOrder = NewSelect(a.mp.AlbumSortOrders(), nil) a.sortOrder.Selected = saved.sortOrder a.sortOrder.OnChanged = a.onSortOrderChanged a.grid = widgets.NewGridViewFromState(saved.gridState) @@ -163,7 +163,7 @@ func (a *AlbumsPage) Reload() { if a.searchText != "" { a.doSearch(a.searchText) } 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.Refresh() } @@ -174,7 +174,7 @@ func (a *AlbumsPage) Save() SavedPage { cfg: a.cfg, contr: a.contr, pm: a.pm, - lm: a.lm, + mp: a.mp, im: a.im, searchText: a.searchText, filter: a.filter, @@ -188,11 +188,12 @@ func (a *AlbumsPage) Save() SavedPage { } func (a *AlbumsPage) doSearch(query string) { + iter := widgets.NewGridViewAlbumIterator(a.mp.IterateAlbums("", query, a.filter)) 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) } else { - a.searchGrid.Reset(widgets.NewGridViewAlbumIterator(a.lm.SearchIterWithFilter(query, a.filter))) + a.searchGrid.Reset(iter) } a.container.Objects[0] = a.searchGrid a.Refresh() @@ -200,7 +201,7 @@ func (a *AlbumsPage) doSearch(query string) { func (a *AlbumsPage) onSortOrderChanged(order string) { 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)) if a.searchText == "" { a.container.Objects[0] = a.grid @@ -215,11 +216,11 @@ func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer { type savedAlbumsPage struct { searchText string - filter backend.AlbumFilter + filter mediaprovider.AlbumFilter cfg *backend.AlbumsPageConfig contr *controller.Controller pm *backend.PlaybackManager - lm *backend.LibraryManager + mp mediaprovider.MediaProvider im *backend.ImageManager sortOrder string gridState widgets.GridViewState diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 860646e..86f0b41 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -20,8 +20,6 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) var _ fyne.Widget = (*ArtistPage)(nil) @@ -42,7 +40,7 @@ type ArtistPage struct { artistPageState - artistInfo *subsonic.ArtistID3 + artistInfo *mediaprovider.ArtistWithAlbums albumGrid *widgets.GridView tracklistCtr *fyne.Container @@ -130,7 +128,7 @@ func (a *ArtistPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track func (a *ArtistPage) playAllTracks() { 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.PlayFromBeginning() @@ -155,7 +153,7 @@ func (a *ArtistPage) load() { } else { a.showTopTracks() } - info, err := a.sm.Server.GetArtistInfo2(a.artistID, nil) + info, err := a.sm.Server.GetArtistInfo(a.artistID) if err != nil { log.Printf("Failed to get artist info: %s", err.Error()) } @@ -169,11 +167,11 @@ func (a *ArtistPage) showAlbumGrid() { a.activeView = 0 // if page still loading, will show discography view first 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{ Name: al.Name, ID: al.ID, - CoverArtID: al.CoverArt, + CoverArtID: al.CoverArtID, Secondary: strconv.Itoa(al.Year), } }) @@ -191,7 +189,7 @@ func (a *ArtistPage) showTopTracks() { a.activeView = 1 // if page still loading, will show tracks view first return } - ts, err := a.sm.Server.GetTopSongs(a.artistInfo.Name, map[string]string{"count": "20"}) + ts, err := a.sm.Server.GetTopTracks(a.artistInfo.Artist, 20) if err != nil { log.Printf("error getting top songs: %s", err.Error()) return @@ -277,18 +275,18 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader { return a } -func (a *ArtistPageHeader) Update(artist *subsonic.ArtistID3) { +func (a *ArtistPageHeader) Update(artist *mediaprovider.ArtistWithAlbums) { if artist == nil { return } - a.favoriteBtn.IsFavorited = !artist.Starred.IsZero() + a.favoriteBtn.IsFavorited = !artist.Favorite a.favoriteBtn.Refresh() a.artistID = artist.ID a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name a.titleDisp.Refresh() } -func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) { +func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) { if info == nil { return } @@ -304,7 +302,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) { } a.similarArtists.RemoveAll() - for i, art := range info.SimilarArtist { + for i, art := range info.SimilarArtists { if i == 0 { a.similarArtists.Add(widget.NewLabel("Similar Artists:")) } @@ -321,11 +319,11 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) { } a.similarArtists.Refresh() - if info.LargeImageUrl != "" { + if info.ImageURL != "" { if a.artistImage.HaveImage() { - _ = a.artistPage.im.RefreshCachedArtistImageIfExpired(a.artistID, info.LargeImageUrl) + _ = a.artistPage.im.RefreshCachedArtistImageIfExpired(a.artistID, info.ImageURL) } else { - im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.LargeImageUrl) + im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.ImageURL) if err == nil { a.artistImage.SetImage(im, true /*tappable*/) } @@ -334,11 +332,8 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) { } func (a *ArtistPageHeader) toggleFavorited() { - if a.favoriteBtn.IsFavorited { - a.artistPage.sm.Server.Star(subsonic.StarParameters{ArtistIDs: []string{a.artistID}}) - } else { - a.artistPage.sm.Server.Unstar(subsonic.StarParameters{ArtistIDs: []string{a.artistID}}) - } + params := mediaprovider.RatingFavoriteParameters{ArtistIDs: []string{a.artistID}} + a.artistPage.sm.Server.SetFavorite(params, a.favoriteBtn.IsFavorited) } func (a *ArtistPageHeader) createContainer() { diff --git a/ui/browsing/artistsgenrespage.go b/ui/browsing/artistsgenrespage.go index c694b3b..8a7078f 100644 --- a/ui/browsing/artistsgenrespage.go +++ b/ui/browsing/artistsgenrespage.go @@ -3,9 +3,9 @@ package browsing import ( "log" "strings" - "time" "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" @@ -16,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" ) var _ fyne.Widget = (*ArtistPage)(nil) @@ -80,7 +78,7 @@ func (a *ArtistsGenresPage) load(searchOnLoad bool) { } a.model = a.buildGenresListModel(genres) } else { - artists, err := a.sm.Server.GetArtists(nil) + artists, err := a.sm.Server.GetArtists() if err != nil { log.Printf("error loading artists: %v", err.Error()) } @@ -145,29 +143,27 @@ func (s *savedArtistsGenresPage) Restore() Page { return newArtistsGenresPage(s.isGenresPage, s.contr, s.sm, 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) - for _, idx := range artists.Index { - for _, artist := range idx.Artist { - model = append(model, widgets.ArtistGenreListItemModel{ - ID: artist.ID, - Name: artist.Name, - AlbumCount: artist.AlbumCount, - Favorite: artist.Starred != time.Time{}, - }) - } + for _, artist := range artists { + model = append(model, widgets.ArtistGenreListItemModel{ + ID: artist.ID, + Name: artist.Name, + AlbumCount: artist.AlbumCount, + Favorite: artist.Favorite, + }) } 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) for _, genre := range genres { model = append(model, widgets.ArtistGenreListItemModel{ ID: genre.Name, Name: genre.Name, AlbumCount: genre.AlbumCount, - TrackCount: genre.SongCount, + TrackCount: genre.TrackCount, Favorite: false, }) } diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index cb47f9e..9a8ed65 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -26,9 +26,8 @@ type FavoritesPage struct { pm *backend.PlaybackManager im *backend.ImageManager sm *backend.ServerManager - lm *backend.LibraryManager - filter backend.AlbumFilter + filter mediaprovider.AlbumFilter searchText string nowPlayingID string pendingViewSwitch bool @@ -44,19 +43,18 @@ type FavoritesPage struct { 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, sm *backend.ServerManager, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage { a := &FavoritesPage{ - filter: backend.AlbumFilter{ExcludeUnfavorited: true}, + filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true}, cfg: cfg, contr: contr, pm: pm, - lm: lm, sm: sm, im: im, } a.ExtendBaseWidget(a) a.createHeader(0) - iter := lm.StarredIter(a.filter) + iter := sm.Server.IterateAlbums("", "", a.filter) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.grid) if cfg.InitialView == "Artists" { @@ -100,7 +98,6 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { cfg: saved.cfg, contr: saved.contr, pm: saved.pm, - lm: saved.lm, sm: saved.sm, im: saved.im, searchText: saved.searchText, @@ -142,13 +139,13 @@ func (a *FavoritesPage) Reload() { if a.searchText != "" { a.doSearchAlbums(a.searchText) } else { - iter := a.lm.StarredIter(a.filter) + iter := a.sm.Server.IterateAlbums("", "", a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) } if a.tracklistCtr != nil || a.artistListCtr != nil { go func() { // re-fetch starred info from server - starred, err := a.sm.Server.GetStarred2(nil) + starred, err := a.sm.Server.GetFavorites() if err != nil { log.Printf("error getting starred items: %s", err.Error()) return @@ -156,7 +153,7 @@ func (a *FavoritesPage) Reload() { if a.tracklistCtr != nil { // refresh favorite songs view tr := a.tracklistCtr.Objects[0].(*widgets.Tracklist) - tr.Tracks = starred.Song + tr.Tracks = starred.Tracks if a.toggleBtns.ActivatedButtonIndex() == 2 { // favorite songs view is visible tr.Refresh() @@ -165,7 +162,7 @@ func (a *FavoritesPage) Reload() { if a.artistListCtr != nil { // refresh favorite artists view al := a.artistListCtr.Objects[0].(*widgets.ArtistGenreList) - al.Items = buildArtistListModel(starred.Artist) + al.Items = buildArtistListModel(starred.Artists) if a.toggleBtns.ActivatedButtonIndex() == 1 { // favorite artists view is visible al.Refresh() @@ -182,7 +179,6 @@ func (a *FavoritesPage) Save() SavedPage { pm: a.pm, sm: a.sm, im: a.im, - lm: a.lm, filter: a.filter, searchText: a.searchText, gridState: a.grid.SaveToState(), @@ -234,7 +230,7 @@ func (a *FavoritesPage) SelectAll() { } func (a *FavoritesPage) doSearchAlbums(query string) { - iter := a.lm.SearchIterWithFilter(query, a.filter) + iter := a.sm.Server.IterateAlbums("", query, a.filter) if a.searchGrid == nil { a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.searchGrid) @@ -356,7 +352,6 @@ type savedFavoritesPage struct { pm *backend.PlaybackManager sm *backend.ServerManager im *backend.ImageManager - lm *backend.LibraryManager gridState widgets.GridViewState searchGridState widgets.GridViewState filter mediaprovider.AlbumFilter diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index f53de6e..e8c2f10 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.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" myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" @@ -22,12 +23,12 @@ type GenrePage struct { contr *controller.Controller im *backend.ImageManager pm *backend.PlaybackManager - lm *backend.LibraryManager + mp mediaprovider.MediaProvider grid *widgets.GridView searchGrid *widgets.GridView searcher *widgets.SearchEntry searchText string - filter backend.AlbumFilter + filter mediaprovider.AlbumFilter filterBtn *widgets.AlbumFilterButton titleDisp *widget.RichText playRandom *widget.Button @@ -37,13 +38,13 @@ type GenrePage struct { 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{ genre: genre, - filter: backend.AlbumFilter{Genres: []string{genre}}, + filter: mediaprovider.AlbumFilter{Genres: []string{genre}}, contr: contr, pm: pm, - lm: lm, + mp: mp, im: im, } g.ExtendBaseWidget(g) @@ -53,7 +54,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba SizeName: theme.SizeNameHeadingText, } 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.contr.ConnectAlbumGridActions(g.grid) g.createSearchAndFilter() @@ -90,7 +91,7 @@ func restoreGenrePage(saved *savedGenrePage) *GenrePage { genre: saved.genre, contr: saved.contr, pm: saved.pm, - lm: saved.lm, + mp: saved.mp, im: saved.im, searchText: saved.searchText, filter: saved.filter, @@ -124,7 +125,7 @@ func (g *GenrePage) Reload() { if g.searchText != "" { g.doSearch(g.searchText) } else { - iter := g.lm.GenreIter(g.genre, g.filter) + iter := g.mp.IterateAlbums("", "", g.filter) g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) g.grid.Refresh() } @@ -137,7 +138,7 @@ func (g *GenrePage) Save() SavedPage { searchText: g.searchText, contr: g.contr, pm: g.pm, - lm: g.lm, + mp: g.mp, im: g.im, gridState: g.grid.SaveToState(), } @@ -167,7 +168,7 @@ func (g *GenrePage) OnSearched(query string) { } func (g *GenrePage) doSearch(query string) { - iter := g.lm.SearchIterWithFilter(query, g.filter) + iter := g.mp.IterateAlbums("", query, g.filter) if g.searchGrid == nil { g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.contr.ConnectAlbumGridActions(g.searchGrid) @@ -185,10 +186,10 @@ func (g *GenrePage) playRandomSongs() { type savedGenrePage struct { genre string searchText string - filter backend.AlbumFilter + filter mediaprovider.AlbumFilter contr *controller.Controller pm *backend.PlaybackManager - lm *backend.LibraryManager + mp mediaprovider.MediaProvider im *backend.ImageManager gridState widgets.GridViewState searchGridState widgets.GridViewState diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index f7c4e08..03ea70b 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.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/layouts" @@ -10,8 +11,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) type NowPlayingPage struct { @@ -83,7 +82,7 @@ func (a *NowPlayingPage) 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 { a.nowPlayingID = "" } else { diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 343925b..15d411c 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -5,6 +5,7 @@ import ( "log" "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" @@ -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 PlaylistPage struct { @@ -90,7 +89,7 @@ func (a *PlaylistPage) Route() controller.Route { 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 { a.nowPlayingID = "" } else { @@ -119,7 +118,7 @@ func (a *PlaylistPage) load() { log.Printf("Failed to get playlist: %s", err.Error()) return } - a.tracklist.Tracks = playlist.Entry + a.tracklist.Tracks = playlist.Tracks a.tracklist.SetNowPlaying(a.nowPlayingID) a.tracklist.Refresh() a.header.Update(playlist) @@ -148,10 +147,7 @@ func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { for i, tr := range newTracks { ids[i] = tr.ID } - err := a.sm.Server.CreatePlaylistWithTracks(ids, map[string]string{ - "playlistId": a.playlistID, - }) - if err != nil { + if err := a.sm.Server.ReplacePlaylistTracks(a.playlistID, ids); err != nil { log.Printf("error updating playlist: %s", err.Error()) } else { a.tracklist.Tracks = newTracks @@ -161,7 +157,7 @@ func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { } 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() go a.Reload() } @@ -170,7 +166,7 @@ type PlaylistPageHeader struct { widget.BaseWidget page *PlaylistPage - playlistInfo *subsonic.Playlist + playlistInfo *mediaprovider.PlaylistWithTracks image *widgets.ImagePlaceholder editButton *widget.Button @@ -199,7 +195,7 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { a.trackTimeLabel = widget.NewLabel("") a.editButton = widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func() { if a.playlistInfo != nil { - page.contr.DoEditPlaylistWorkflow(a.playlistInfo) + page.contr.DoEditPlaylistWorkflow(&a.playlistInfo.Playlist) } }) a.editButton.Hidden = true @@ -244,18 +240,18 @@ func (a *PlaylistPageHeader) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(a.container) } -func (a *PlaylistPageHeader) Update(playlist *subsonic.Playlist) { +func (a *PlaylistPageHeader) Update(playlist *mediaprovider.PlaylistWithTracks) { 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.descriptionLabel.SetText(playlist.Comment) + a.descriptionLabel.SetText(playlist.Description) a.ownerLabel.SetText(a.formatPlaylistOwnerStr(playlist)) a.trackTimeLabel.SetText(a.formatPlaylistTrackTimeStr(playlist)) a.createdAtLabel.SetText("created at TODO") var haveCover bool - if playlist.CoverArt != "" { - if im, err := a.page.im.GetCoverThumbnail(playlist.CoverArt); err == nil && im != nil { + if playlist.CoverArtID != "" { + if im, err := a.page.im.GetCoverThumbnail(playlist.CoverArtID); err == nil && im != nil { a.image.SetImage(im, false /*tappable*/) haveCover = true } @@ -268,7 +264,7 @@ func (a *PlaylistPageHeader) Update(playlist *subsonic.Playlist) { a.Refresh() } -func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *subsonic.Playlist) string { +func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *mediaprovider.PlaylistWithTracks) string { pubPriv := "Public" if !p.Public { pubPriv = "Private" @@ -276,12 +272,12 @@ func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *subsonic.Playlist) string 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" - if p.SongCount == 1 { + if p.TrackCount == 1 { 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 { diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index c99d3b5..2ebbf52 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -7,6 +7,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" @@ -18,8 +19,6 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) type PlaylistsPage struct { @@ -28,8 +27,8 @@ type PlaylistsPage struct { cfg *backend.PlaylistsPageConfig contr *controller.Controller sm *backend.ServerManager - playlists []*subsonic.Playlist - searchedPlaylists []*subsonic.Playlist + playlists []*mediaprovider.Playlist + searchedPlaylists []*mediaprovider.Playlist viewToggle *widgets.ToggleButtonGroup searcher *widgets.SearchEntry @@ -76,7 +75,7 @@ func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageCo } func (a *PlaylistsPage) load(searchOnLoad bool) { - playlists, err := a.sm.Server.GetPlaylists(nil) + playlists, err := a.sm.Server.GetPlaylists() if err != nil { log.Printf("error loading playlists: %v", err.Error()) } @@ -93,7 +92,7 @@ func (a *PlaylistsPage) createListView() { a.listView.OnNavTo = a.showPlaylistPage } -func (a *PlaylistsPage) createGridView(playlists []*subsonic.Playlist) { +func (a *PlaylistsPage) createGridView(playlists []*mediaprovider.Playlist) { model := createPlaylistGridViewModel(playlists) a.gridView = widgets.NewFixedGridView(model, a.contr.App.ImageManager) 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()) 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() } -func createPlaylistGridViewModel(playlists []*subsonic.Playlist) []widgets.GridViewItemModel { - return sharedutil.MapSlice(playlists, func(pl *subsonic.Playlist) widgets.GridViewItemModel { +func createPlaylistGridViewModel(playlists []*mediaprovider.Playlist) []widgets.GridViewItemModel { + return sharedutil.MapSlice(playlists, func(pl *mediaprovider.Playlist) widgets.GridViewItemModel { tracks := "tracks" - if pl.SongCount == 1 { + if pl.TrackCount == 1 { tracks = "track" } return widgets.GridViewItemModel{ Name: pl.Name, ID: pl.ID, - CoverArtID: pl.CoverArt, - Secondary: fmt.Sprintf("%d %s", pl.SongCount, tracks), + CoverArtID: pl.CoverArtID, + Secondary: fmt.Sprintf("%d %s", pl.TrackCount, tracks), } }) } @@ -164,15 +163,15 @@ func (a *PlaylistsPage) showPlaylistPage(id string) { func (a *PlaylistsPage) onSearched(query string) { // 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 - var playlists []*subsonic.Playlist + var playlists []*mediaprovider.Playlist if query == "" { a.searchedPlaylists = nil playlists = a.playlists } 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) 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) }) playlists = a.searchedPlaylists @@ -182,7 +181,7 @@ func (a *PlaylistsPage) onSearched(query string) { // update the model for both views if initialized, // refresh the active view -func (a *PlaylistsPage) refreshView(playlists []*subsonic.Playlist) { +func (a *PlaylistsPage) refreshView(playlists []*mediaprovider.Playlist) { if a.listView != nil { a.listView.Playlists = playlists } @@ -247,7 +246,7 @@ func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer { type PlaylistList struct { widget.BaseWidget - Playlists []*subsonic.Playlist + Playlists []*mediaprovider.Playlist OnNavTo func(string) columnsLayout *layouts.ColumnsLayout @@ -274,9 +273,9 @@ func NewPlaylistList() *PlaylistList { row := item.(*PlaylistListRow) row.ID = a.Playlists[id].ID 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.trackCountLabel.Text = strconv.Itoa(a.Playlists[id].SongCount) + row.trackCountLabel.Text = strconv.Itoa(a.Playlists[id].TrackCount) row.Refresh() }, ) diff --git a/ui/browsing/router.go b/ui/browsing/router.go index f737cf4..10474d3 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -28,17 +28,17 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati func (r Router) CreatePage(rte controller.Route) Page { switch rte.Page { 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: - 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: return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager, r.App.ImageManager, r.Controller) case controller.Artists: return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager) 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, r.App.PlaybackManager, r.App.ImageManager) 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: return NewArtistsGenresPage(true, r.Controller, r.App.ServerManager) case controller.NowPlaying: @@ -48,7 +48,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Playlists: return NewPlaylistsPage(r.Controller, &r.App.Config.PlaylistsPage, r.App.ServerManager) 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 } diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index dc86e46..d8a94bc 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.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/layouts" @@ -12,8 +13,6 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) type TracksPage struct { @@ -37,11 +36,11 @@ type tracksPageState struct { searchText string contr *controller.Controller conf *backend.TracksPageConfig - lm *backend.LibraryManager + mp mediaprovider.MediaProvider } -func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, lm *backend.LibraryManager) *TracksPage { - t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, lm: lm}} +func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, mp mediaprovider.MediaProvider) *TracksPage { + t := &TracksPage{tracksPageState: tracksPageState{contr: contr, conf: conf, mp: mp}} t.ExtendBaseWidget(t) t.tracklist = widgets.NewTracklist(nil) @@ -79,12 +78,12 @@ func (t *TracksPage) Route() controller.Route { func (t *TracksPage) Reload() { t.tracklist.Clear() - iter := t.lm.AllTracksIterator() + iter := t.mp.IterateTracks("") // loads asynchronously 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.tracklist.SetNowPlaying(t.nowPlayingID) if t.searchTracklist != nil { @@ -130,7 +129,7 @@ func (t *TracksPage) doSearch(query string) { } else { t.searchTracklist.Clear() } - iter := t.lm.SearchTracksIterator(query) + iter := t.mp.IterateTracks(query) t.searchLoader = widgets.NewTracklistLoader(t.searchTracklist, iter) t.container.Objects[0].(*fyne.Container).Objects[0] = t.searchTracklist t.Refresh() @@ -146,7 +145,7 @@ func (t *TracksPage) Save() SavedPage { } 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 if t.searchText != "" { t.searcher.Entry.Text = t.searchText diff --git a/ui/controller/controller.go b/ui/controller/controller.go index f231983..98eaef3 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -3,12 +3,10 @@ package controller import ( "image" "log" - "math" - "strconv" - "sync" "time" "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/dialogs" @@ -20,8 +18,6 @@ import ( "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" - - "github.com/dweymouth/go-subsonic/subsonic" ) type NavigationHandler func(Route) @@ -94,14 +90,14 @@ func (m *Controller) ShowPopUpImage(img image.Image) { func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) { tracklist.OnAddToPlaylist = m.DoAddTracksToPlaylistWorkflow - tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { + tracklist.OnAddToQueue = func(tracks []*mediaprovider.Track) { m.App.PlaybackManager.LoadTracks(tracks, true, false) } tracklist.OnPlayTrackAt = func(idx int) { m.App.PlaybackManager.LoadTracks(tracklist.Tracks, false, false) 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.PlayFromBeginning() } @@ -137,7 +133,7 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { log.Printf("error loading album: %s", err.Error()) return } - m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Song)) + m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Tracks)) } } @@ -173,7 +169,10 @@ func (m *Controller) PromptForFirstServer() { // Depending on the results of that dialog, potentially create a new playlist // Add tracks to the user-specified playlist func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { - pls, err := m.App.LibraryManager.GetUserOwnedPlaylists() + pls, err := m.App.ServerManager.Server.GetPlaylists() + pls = sharedutil.FilterSlice(pls, func(pl *mediaprovider.Playlist) bool { + return pl.Owner == m.App.ServerManager.LoggedInUser + }) if err != nil { // TODO: surface this error to user log.Printf("error getting user-owned playlists: %s", err.Error()) @@ -192,10 +191,9 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { pop.Hide() m.doModalClosed() if playlistChoice < 0 { - m.App.ServerManager.Server.CreatePlaylistWithTracks( - trackIDs, map[string]string{"name": newPlaylistName}) + m.App.ServerManager.Server.CreatePlaylist(newPlaylistName, trackIDs) } else { - m.App.ServerManager.Server.UpdatePlaylistTracks( + m.App.ServerManager.Server.EditPlaylistTracks( pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/) } } @@ -203,7 +201,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { pop.Show() } -func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) { +func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) { dlg := dialogs.NewEditPlaylistDialog(playlist) pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas()) m.ClosePopUpOnEscape(pop) @@ -234,11 +232,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) { pop.Hide() m.doModalClosed() go func() { - err := m.App.ServerManager.Server.UpdatePlaylist(playlist.ID, map[string]string{ - "name": dlg.Name, - "comment": dlg.Description, - "public": strconv.FormatBool(dlg.IsPublic), - }) + err := m.App.ServerManager.Server.EditPlaylist(playlist.ID, dlg.Name, dlg.Description, dlg.IsPublic) if err != nil { log.Printf("error updating playlist: %s", err.Error()) } else if rte := m.CurPageFunc(); rte.Page == Playlist && rte.Arg == playlist.ID { @@ -405,49 +399,19 @@ func (c *Controller) doModalClosed() { } func (c *Controller) SetTrackFavorites(trackIDs []string, favorite bool) { - s := c.App.ServerManager.Server - if favorite { - go s.Star(subsonic.StarParameters{SongIDs: trackIDs}) - } else { - go s.Unstar(subsonic.StarParameters{SongIDs: trackIDs}) - } + c.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{ + TrackIDs: trackIDs, + }, favorite) + for _, id := range trackIDs { c.App.PlaybackManager.OnTrackFavoriteStatusChanged(id, favorite) } } func (c *Controller) SetTrackRatings(trackIDs []string, rating int) { - // 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 - 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() - } - }() - } + c.App.ServerManager.Server.SetRating(mediaprovider.RatingFavoriteParameters{ + TrackIDs: trackIDs, + }, rating) // Notify PlaybackManager of rating change to update // the in-memory track models diff --git a/ui/dialogs/editplaylistdialog.go b/ui/dialogs/editplaylistdialog.go index 20e09a9..43a3a74 100644 --- a/ui/dialogs/editplaylistdialog.go +++ b/ui/dialogs/editplaylistdialog.go @@ -6,7 +6,7 @@ import ( "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic/subsonic" + "github.com/dweymouth/supersonic/backend/mediaprovider" ) type EditPlaylistDialog struct { @@ -23,11 +23,11 @@ type EditPlaylistDialog struct { container *fyne.Container } -func NewEditPlaylistDialog(playlist *subsonic.Playlist) *EditPlaylistDialog { +func NewEditPlaylistDialog(playlist *mediaprovider.Playlist) *EditPlaylistDialog { e := &EditPlaylistDialog{ IsPublic: playlist.Public, Name: playlist.Name, - Description: playlist.Comment, + Description: playlist.Description, } e.ExtendBaseWidget(e) diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index 4d04f0e..5c82b8d 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -6,17 +6,38 @@ import ( "log" "sync" - "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/sharedutil" "fyne.io/fyne/v2" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic/subsonic" ) 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 { GetCoverThumbnailFromCache(string) (image.Image, bool) GetCoverThumbnail(string) (image.Image, error) @@ -27,24 +48,24 @@ type GridViewIterator interface { } type gridViewAlbumIterator struct { - iter *backend.BatchingIterator + iter BatchingIterator } func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel { 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{ Name: al.Name, ID: al.ID, - CoverArtID: al.CoverArt, - Secondary: al.Artist, - SecondaryID: al.ArtistID, + CoverArtID: al.CoverArtID, + Secondary: al.ArtistNames[0], + SecondaryID: al.ArtistIDs[0], } }) } -func NewGridViewAlbumIterator(iter backend.AlbumIterator) GridViewIterator { - return gridViewAlbumIterator{iter: backend.NewBatchingIterator(iter)} +func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator { + return gridViewAlbumIterator{iter: NewBatchingIterator(iter)} } type GridView struct { From f926ed3fb9eda5923d6011a1f89ac36a38918ac0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 15 May 2023 17:05:18 -0700 Subject: [PATCH 08/11] fix some bugs and restructure a bit more --- backend/mediaprovider/mediaprovider.go | 4 +- .../mediaprovider/subsonic/albumiterator.go | 64 ++++---- .../subsonic/subsonicmediaprovider.go | 138 +++++++----------- .../mediaprovider/subsonic/trackiterator.go | 2 +- ui/browsing/albumspage.go | 8 +- ui/browsing/artistpage.go | 20 +-- ui/browsing/artistsgenrespage.go | 21 ++- ui/browsing/favoritespage.go | 24 +-- ui/browsing/genrepage.go | 6 +- ui/browsing/nowplayingpage.go | 6 +- ui/browsing/playlistspage.go | 18 +-- ui/browsing/router.go | 12 +- ui/controller/controller.go | 78 +++++----- ui/widgets/tracklist.go | 6 +- 14 files changed, 183 insertions(+), 224 deletions(-) diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 5839bdf..c8e6aaa 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -44,10 +44,12 @@ type MediaProvider interface { AlbumSortOrders() []string - IterateAlbums(sortOrder string, searchQuery string, filter AlbumFilter) AlbumIterator + 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) diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go index f198e1f..6ed86a2 100644 --- a/backend/mediaprovider/subsonic/albumiterator.go +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -10,6 +10,30 @@ import ( "github.com/dweymouth/supersonic/sharedutil" ) +const ( + AlbumSortRecentlyAdded string = "Recently Added" + AlbumSortRecentlyPlayed string = "Recently Played" + AlbumSortFrequentlyPlayed string = "Frequently Played" + AlbumSortRandom string = "Random" + AlbumSortTitleAZ string = "Title (A-Z)" + AlbumSortArtistAZ string = "Artist (A-Z)" + AlbumSortYearAscending string = "Year (ascending)" + AlbumSortYearDescending string = "Year (descending)" +) + +func (s *subsonicMediaProvider) AlbumSortOrders() []string { + return []string{ + AlbumSortRecentlyAdded, + AlbumSortRecentlyPlayed, + AlbumSortFrequentlyPlayed, + AlbumSortRandom, + AlbumSortTitleAZ, + AlbumSortArtistAZ, + AlbumSortYearAscending, + AlbumSortYearDescending, + } +} + func filterMatches(f mediaprovider.AlbumFilter, album *subsonic.AlbumID3) bool { if album == nil { return false @@ -34,15 +58,7 @@ func filterMatches(f mediaprovider.AlbumFilter, album *subsonic.AlbumID3) bool { return false } -func filterIsEmpty(f mediaprovider.AlbumFilter) bool { - return !f.ExcludeFavorited && !f.ExcludeUnfavorited && - f.MinYear == 0 && f.MaxYear == 0 && len(f.Genres) == 0 -} - -func (s *subsonicMediaProvider) IterateAlbums(sortOrder, searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { - if searchQuery != "" { - return s.newSearchIter(searchQuery, filter) - } +func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { if sortOrder == "" && len(filter.Genres) == 1 { return s.newBaseIter("byGenre", filter, map[string]string{"genre": filter.Genres[0]}) } @@ -72,6 +88,10 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder, searchQuery string, fil } } +func (s *subsonicMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { + return s.newSearchIter(searchQuery, filter) +} + type baseIter struct { listType string filter mediaprovider.AlbumFilter @@ -217,7 +237,7 @@ func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) { if _, have := s.albumIDset[album.ID]; have { continue } - if filterMatches(s.filter, album) { + if !filterMatches(s.filter, album) { continue } s.prefetched = append(s.prefetched, album) @@ -326,27 +346,3 @@ func (r *randomIter) Next() *mediaprovider.Album { 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/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 4eee99a..170da6f 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -12,17 +12,6 @@ import ( "github.com/dweymouth/supersonic/sharedutil" ) -const ( - AlbumSortRecentlyAdded string = "Recently Added" - AlbumSortRecentlyPlayed string = "Recently Played" - AlbumSortFrequentlyPlayed string = "Frequently Played" - AlbumSortRandom string = "Random" - AlbumSortTitleAZ string = "Title (A-Z)" - AlbumSortArtistAZ string = "Artist (A-Z)" - AlbumSortYearAscending string = "Year (ascending)" - AlbumSortYearDescending string = "Year (descending)" -) - type subsonicMediaProvider struct { client *subsonic.Client } @@ -31,19 +20,6 @@ func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaP return &subsonicMediaProvider{client: subsonicClient} } -func (s *subsonicMediaProvider) AlbumSortOrders() []string { - return []string{ - AlbumSortRecentlyAdded, - AlbumSortRecentlyPlayed, - AlbumSortFrequentlyPlayed, - AlbumSortRandom, - AlbumSortTitleAZ, - AlbumSortArtistAZ, - AlbumSortYearAscending, - AlbumSortYearDescending, - } -} - func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error { return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name}) } @@ -69,21 +45,11 @@ func (s *subsonicMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWi if err != nil { return nil, err } - return &mediaprovider.AlbumWithTracks{ - Album: mediaprovider.Album{ - ID: al.ID, - Name: al.Name, - ArtistIDs: []string{al.ArtistID}, - CoverArtID: al.CoverArt, - ArtistNames: []string{al.Artist}, - Genres: []string{al.Genre}, - Year: al.Year, - TrackCount: al.SongCount, - Favorite: !al.Starred.IsZero(), - Duration: al.Duration, - }, + album := &mediaprovider.AlbumWithTracks{ Tracks: sharedutil.MapSlice(al.Song, toTrack), - }, nil + } + fillAlbum(al, &album.Album) + return album, nil } func (s *subsonicMediaProvider) GetArtist(artistID string) (*mediaprovider.ArtistWithAlbums, error) { @@ -168,19 +134,11 @@ func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.P if err != nil { return nil, err } - return &mediaprovider.PlaylistWithTracks{ - Playlist: mediaprovider.Playlist{ - ID: pl.ID, - CoverArtID: pl.CoverArt, - Name: pl.Name, - Description: pl.Comment, - TrackCount: pl.SongCount, - Public: pl.Public, - Owner: pl.Owner, - Duration: pl.Duration, - }, + playlist := &mediaprovider.PlaylistWithTracks{ Tracks: sharedutil.MapSlice(pl.Entry, toTrack), - }, nil + } + fillPlaylist(pl, &playlist.Playlist) + return playlist, nil } func (s *subsonicMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) { @@ -261,34 +219,24 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar var err error batchSetRating := func(offs int, wg *sync.WaitGroup) { for i := 0; i < batchSize && offs+i < len(params.TrackIDs); i++ { - if wg != nil { - wg.Add(1) - } + wg.Add(1) go func(idx int) { newErr := s.client.SetRating(params.TrackIDs[idx], rating) if err == nil && newErr != nil { err = newErr } - if wg != nil { - wg.Done() - } + wg.Done() }(offs + i) } } - if len(params.TrackIDs) <= 5 { - // one batch only - no need to use wait group - batchSetRating(0, nil) - } else { - go func() { - 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() - } - }() + 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 } @@ -314,6 +262,7 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track { Favorite: !ch.Starred.IsZero(), PlayCount: int(ch.PlayCount), FilePath: ch.Path, + Size: ch.Size, BitRate: ch.BitRate, } } @@ -322,18 +271,22 @@ func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album { if al == nil { return nil } - return &mediaprovider.Album{ - ID: al.ID, - CoverArtID: al.CoverArt, - Name: al.Name, - Duration: al.Duration, - ArtistIDs: []string{al.ArtistID}, - ArtistNames: []string{al.Artist}, - Year: al.Year, - Genres: []string{al.Genre}, - TrackCount: al.SongCount, - Favorite: !al.Starred.IsZero(), - } + 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 { @@ -360,14 +313,21 @@ func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist { } func toPlaylist(pl *subsonic.Playlist) *mediaprovider.Playlist { - return &mediaprovider.Playlist{ - Name: pl.Name, - ID: pl.ID, - CoverArtID: pl.CoverArt, - Description: pl.Comment, - Owner: pl.Owner, - Public: pl.Public, - TrackCount: pl.SongCount, - Duration: pl.Duration, + 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 } diff --git a/backend/mediaprovider/subsonic/trackiterator.go b/backend/mediaprovider/subsonic/trackiterator.go index 98f9481..3758368 100644 --- a/backend/mediaprovider/subsonic/trackiterator.go +++ b/backend/mediaprovider/subsonic/trackiterator.go @@ -11,7 +11,7 @@ func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider. if searchQuery == "" { return &allTracksIterator{ s: s, - albumIter: s.IterateAlbums(AlbumSortArtistAZ, "", mediaprovider.AlbumFilter{}), + albumIter: s.IterateAlbums(AlbumSortArtistAZ, mediaprovider.AlbumFilter{}), } } return &searchTracksIterator{ diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 52e9b5b..c4cbfe4 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -74,7 +74,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, contr *controller.Controller, cfg.SortOrder = string(mp.AlbumSortOrders()[0]) } a.sortOrder.Selected = cfg.SortOrder - iter := mp.IterateAlbums(a.sortOrder.Selected, "", a.filter) + iter := mp.IterateAlbums(a.sortOrder.Selected, a.filter) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), im) contr.ConnectAlbumGridActions(a.grid) a.createSearchAndFilter() @@ -163,7 +163,7 @@ func (a *AlbumsPage) Reload() { if a.searchText != "" { a.doSearch(a.searchText) } else { - iter := a.mp.IterateAlbums(a.sortOrder.Selected, "", a.filter) + iter := a.mp.IterateAlbums(a.sortOrder.Selected, a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) a.grid.Refresh() } @@ -188,7 +188,7 @@ func (a *AlbumsPage) Save() SavedPage { } func (a *AlbumsPage) doSearch(query string) { - iter := widgets.NewGridViewAlbumIterator(a.mp.IterateAlbums("", query, a.filter)) + iter := widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, a.filter)) if a.searchGrid == nil { a.searchGrid = widgets.NewGridView(iter, a.im) a.contr.ConnectAlbumGridActions(a.searchGrid) @@ -201,7 +201,7 @@ func (a *AlbumsPage) doSearch(query string) { func (a *AlbumsPage) onSortOrderChanged(order string) { a.cfg.SortOrder = a.sortOrder.Selected - iter := a.mp.IterateAlbums(order, "", a.filter) + iter := a.mp.IterateAlbums(order, a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) if a.searchText == "" { a.container.Objects[0] = a.grid diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 86f0b41..e1a4c95 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -30,7 +30,7 @@ type artistPageState struct { cfg *backend.ArtistPageConfig pm *backend.PlaybackManager - sm *backend.ServerManager + mp mediaprovider.MediaProvider im *backend.ImageManager contr *controller.Controller } @@ -49,20 +49,20 @@ type ArtistPage struct { 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 if cfg.InitialView == "Top Tracks" { 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{ artistID: artistID, cfg: cfg, pm: pm, - sm: sm, + mp: mp, im: im, contr: contr, activeView: activeView, @@ -141,7 +141,7 @@ func (a *ArtistPage) playArtistRadio() { // should be called asynchronously func (a *ArtistPage) load() { - artist, err := a.sm.Server.GetArtist(a.artistID) + artist, err := a.mp.GetArtist(a.artistID) if err != nil { log.Printf("Failed to get artist: %s", err.Error()) return @@ -153,7 +153,7 @@ func (a *ArtistPage) load() { } else { a.showTopTracks() } - info, err := a.sm.Server.GetArtistInfo(a.artistID) + info, err := a.mp.GetArtistInfo(a.artistID) if err != nil { log.Printf("Failed to get artist info: %s", err.Error()) } @@ -189,7 +189,7 @@ func (a *ArtistPage) showTopTracks() { a.activeView = 1 // if page still loading, will show tracks view first return } - ts, err := a.sm.Server.GetTopTracks(a.artistInfo.Artist, 20) + ts, err := a.mp.GetTopTracks(a.artistInfo.Artist, 20) if err != nil { log.Printf("error getting top songs: %s", err.Error()) return @@ -232,7 +232,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } 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 { @@ -333,7 +333,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) { func (a *ArtistPageHeader) toggleFavorited() { params := mediaprovider.RatingFavoriteParameters{ArtistIDs: []string{a.artistID}} - a.artistPage.sm.Server.SetFavorite(params, a.favoriteBtn.IsFavorited) + a.artistPage.mp.SetFavorite(params, a.favoriteBtn.IsFavorited) } func (a *ArtistPageHeader) createContainer() { diff --git a/ui/browsing/artistsgenrespage.go b/ui/browsing/artistsgenrespage.go index 8a7078f..1b5901f 100644 --- a/ui/browsing/artistsgenrespage.go +++ b/ui/browsing/artistsgenrespage.go @@ -4,7 +4,6 @@ import ( "log" "strings" - "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" @@ -25,7 +24,7 @@ type ArtistsGenresPage struct { isGenresPage bool contr *controller.Controller - sm *backend.ServerManager + mp mediaprovider.MediaProvider model []widgets.ArtistGenreListItemModel list *widgets.ArtistGenreList @@ -34,11 +33,11 @@ type ArtistsGenresPage struct { searcher *widgets.SearchEntry } -func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager) *ArtistsGenresPage { - return newArtistsGenresPage(isGenresPage, contr, sm, "") +func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, mp mediaprovider.MediaProvider) *ArtistsGenresPage { + 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" if isGenresPage { title = "Genres" @@ -46,7 +45,7 @@ func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *b a := &ArtistsGenresPage{ isGenresPage: isGenresPage, contr: contr, - sm: sm, + mp: mp, titleDisp: widget.NewRichTextWithText(title), } a.ExtendBaseWidget(a) @@ -72,13 +71,13 @@ func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *b // should be called asynchronously func (a *ArtistsGenresPage) load(searchOnLoad bool) { if a.isGenresPage { - genres, err := a.sm.Server.GetGenres() + genres, err := a.mp.GetGenres() if err != nil { log.Printf("error loading genres: %v", err.Error()) } a.model = a.buildGenresListModel(genres) } else { - artists, err := a.sm.Server.GetArtists() + artists, err := a.mp.GetArtists() if err != nil { log.Printf("error loading artists: %v", err.Error()) } @@ -127,7 +126,7 @@ func (a *ArtistsGenresPage) Save() SavedPage { return &savedArtistsGenresPage{ isGenresPage: a.isGenresPage, contr: a.contr, - sm: a.sm, + mp: a.mp, searchText: a.searcher.Entry.Text, } } @@ -135,12 +134,12 @@ func (a *ArtistsGenresPage) Save() SavedPage { type savedArtistsGenresPage struct { isGenresPage bool contr *controller.Controller - sm *backend.ServerManager + mp mediaprovider.MediaProvider searchText string } 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 []*mediaprovider.Artist) []widgets.ArtistGenreListItemModel { diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 9a8ed65..aec026a 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -25,7 +25,7 @@ type FavoritesPage struct { contr *controller.Controller pm *backend.PlaybackManager im *backend.ImageManager - sm *backend.ServerManager + mp mediaprovider.MediaProvider filter mediaprovider.AlbumFilter searchText string @@ -43,18 +43,18 @@ type FavoritesPage struct { container *fyne.Container } -func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Controller, sm *backend.ServerManager, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage { +func NewFavoritesPage(cfg *backend.FavoritesPageConfig, contr *controller.Controller, mp mediaprovider.MediaProvider, pm *backend.PlaybackManager, im *backend.ImageManager) *FavoritesPage { a := &FavoritesPage{ filter: mediaprovider.AlbumFilter{ExcludeUnfavorited: true}, cfg: cfg, contr: contr, pm: pm, - sm: sm, + mp: mp, im: im, } a.ExtendBaseWidget(a) a.createHeader(0) - iter := sm.Server.IterateAlbums("", "", a.filter) + iter := mp.IterateAlbums("", a.filter) a.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.grid) if cfg.InitialView == "Artists" { @@ -98,7 +98,7 @@ func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { cfg: saved.cfg, contr: saved.contr, pm: saved.pm, - sm: saved.sm, + mp: saved.mp, im: saved.im, searchText: saved.searchText, filter: saved.filter, @@ -139,13 +139,13 @@ func (a *FavoritesPage) Reload() { if a.searchText != "" { a.doSearchAlbums(a.searchText) } else { - iter := a.sm.Server.IterateAlbums("", "", a.filter) + iter := a.mp.IterateAlbums("", a.filter) a.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) } if a.tracklistCtr != nil || a.artistListCtr != nil { go func() { // re-fetch starred info from server - starred, err := a.sm.Server.GetFavorites() + starred, err := a.mp.GetFavorites() if err != nil { log.Printf("error getting starred items: %s", err.Error()) return @@ -177,7 +177,7 @@ func (a *FavoritesPage) Save() SavedPage { cfg: a.cfg, contr: a.contr, pm: a.pm, - sm: a.sm, + mp: a.mp, im: a.im, filter: a.filter, searchText: a.searchText, @@ -230,7 +230,7 @@ func (a *FavoritesPage) SelectAll() { } func (a *FavoritesPage) doSearchAlbums(query string) { - iter := a.sm.Server.IterateAlbums("", query, a.filter) + iter := a.mp.SearchAlbums(query, a.filter) if a.searchGrid == nil { a.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), a.im) a.contr.ConnectAlbumGridActions(a.searchGrid) @@ -266,7 +266,7 @@ func (a *FavoritesPage) onShowFavoriteArtists() { a.createContainer(layout.NewSpacer()) } go func() { - fav, err := a.sm.Server.GetFavorites() + fav, err := a.mp.GetFavorites() if err != nil { log.Printf("error getting starred items: %s", err.Error()) return @@ -315,7 +315,7 @@ func (a *FavoritesPage) onShowFavoriteSongs() { a.createContainer(layout.NewSpacer()) } go func() { - fav, err := a.sm.Server.GetFavorites() + fav, err := a.mp.GetFavorites() if err != nil { log.Printf("error getting starred items: %s", err.Error()) return @@ -350,7 +350,7 @@ type savedFavoritesPage struct { cfg *backend.FavoritesPageConfig contr *controller.Controller pm *backend.PlaybackManager - sm *backend.ServerManager + mp mediaprovider.MediaProvider im *backend.ImageManager gridState widgets.GridViewState searchGridState widgets.GridViewState diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index e8c2f10..be3b527 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -54,7 +54,7 @@ func NewGenrePage(genre string, contr *controller.Controller, pm *backend.Playba SizeName: theme.SizeNameHeadingText, } g.playRandom = widget.NewButtonWithIcon(" Play random", myTheme.ShuffleIcon, g.playRandomSongs) - iter := g.mp.IterateAlbums("", "", g.filter) + iter := g.mp.IterateAlbums("", g.filter) g.grid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.contr.ConnectAlbumGridActions(g.grid) g.createSearchAndFilter() @@ -125,7 +125,7 @@ func (g *GenrePage) Reload() { if g.searchText != "" { g.doSearch(g.searchText) } else { - iter := g.mp.IterateAlbums("", "", g.filter) + iter := g.mp.IterateAlbums("", g.filter) g.grid.Reset(widgets.NewGridViewAlbumIterator(iter)) g.grid.Refresh() } @@ -168,7 +168,7 @@ func (g *GenrePage) OnSearched(query string) { } func (g *GenrePage) doSearch(query string) { - iter := g.mp.IterateAlbums("", query, g.filter) + iter := g.mp.SearchAlbums(query, g.filter) if g.searchGrid == nil { g.searchGrid = widgets.NewGridView(widgets.NewGridViewAlbumIterator(iter), g.im) g.contr.ConnectAlbumGridActions(g.searchGrid) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 03ea70b..ff1f0b4 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -27,7 +27,6 @@ type NowPlayingPage struct { type nowPlayingPageState struct { contr *controller.Controller conf *backend.NowPlayingPageConfig - sm *backend.ServerManager pm *backend.PlaybackManager } @@ -35,10 +34,9 @@ func NewNowPlayingPage( highlightedTrackID string, contr *controller.Controller, conf *backend.NowPlayingPageConfig, - sm *backend.ServerManager, pm *backend.PlaybackManager, ) *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.tracklist = widgets.NewTracklist(nil) a.tracklist.SetVisibleColumns(conf.TracklistColumns) @@ -117,5 +115,5 @@ func (a *NowPlayingPage) load(highlightedTrackID string) { } func (s *nowPlayingPageState) Restore() Page { - return NewNowPlayingPage("", s.contr, s.conf, s.sm, s.pm) + return NewNowPlayingPage("", s.contr, s.conf, s.pm) } diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index 2ebbf52..ba9eb07 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -26,7 +26,7 @@ type PlaylistsPage struct { cfg *backend.PlaylistsPageConfig contr *controller.Controller - sm *backend.ServerManager + mp mediaprovider.MediaProvider playlists []*mediaprovider.Playlist searchedPlaylists []*mediaprovider.Playlist @@ -38,18 +38,18 @@ type PlaylistsPage struct { 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 if cfg.InitialView == "Grid" { 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{ cfg: cfg, - sm: sm, + mp: mp, contr: contr, titleDisp: widget.NewRichTextWithText("Playlists"), } @@ -75,7 +75,7 @@ func newPlaylistsPage(contr *controller.Controller, cfg *backend.PlaylistsPageCo } func (a *PlaylistsPage) load(searchOnLoad bool) { - playlists, err := a.sm.Server.GetPlaylists() + playlists, err := a.mp.GetPlaylists() if err != nil { log.Printf("error loading playlists: %v", err.Error()) } @@ -213,7 +213,7 @@ func (a *PlaylistsPage) Save() SavedPage { return &savedPlaylistsPage{ contr: a.contr, cfg: a.cfg, - sm: a.sm, + mp: a.mp, searchText: a.searcher.Entry.Text, activeView: a.viewToggle.ActivatedButtonIndex(), } @@ -222,13 +222,13 @@ func (a *PlaylistsPage) Save() SavedPage { type savedPlaylistsPage struct { contr *controller.Controller cfg *backend.PlaylistsPageConfig - sm *backend.ServerManager + mp mediaprovider.MediaProvider searchText string activeView int } 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) { diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 10474d3..c337347 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -32,21 +32,21 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Albums: return NewAlbumsPage(&r.App.Config.AlbumsPage, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) 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: - return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager) + return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager.Server) case controller.Favorites: - return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) + return NewFavoritesPage(&r.App.Config.FavoritesPage, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) case controller.Genre: return NewGenrePage(rte.Arg, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: - return NewArtistsGenresPage(true, r.Controller, r.App.ServerManager) + return NewArtistsGenresPage(true, r.Controller, r.App.ServerManager.Server) 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: return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) 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: return NewTracksPage(r.Controller, &r.App.Config.TracksPage, r.App.ServerManager.Server) } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 98eaef3..52c7665 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -128,12 +128,14 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { m.NavigateTo(ArtistRoute(artistID)) } grid.OnAddToPlaylist = func(albumID string) { - album, err := m.App.ServerManager.Server.GetAlbum(albumID) - if err != nil { - log.Printf("error loading album: %s", err.Error()) - return - } - m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Tracks)) + go func() { + album, err := m.App.ServerManager.Server.GetAlbum(albumID) + if err != nil { + log.Printf("error loading album: %s", err.Error()) + return + } + m.DoAddTracksToPlaylistWorkflow(sharedutil.TracksToIDs(album.Tracks)) + }() } } @@ -169,36 +171,38 @@ func (m *Controller) PromptForFirstServer() { // Depending on the results of that dialog, potentially create a new playlist // Add tracks to the user-specified playlist func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { - pls, err := m.App.ServerManager.Server.GetPlaylists() - pls = sharedutil.FilterSlice(pls, func(pl *mediaprovider.Playlist) bool { - return pl.Owner == m.App.ServerManager.LoggedInUser - }) - if err != nil { - // TODO: surface this error to user - log.Printf("error getting user-owned playlists: %s", err.Error()) - return - } - plNames := make([]string, 0, len(pls)) - for _, pl := range pls { - 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 { - m.App.ServerManager.Server.CreatePlaylist(newPlaylistName, trackIDs) - } else { - m.App.ServerManager.Server.EditPlaylistTracks( - pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/) + go func() { + pls, err := m.App.ServerManager.Server.GetPlaylists() + pls = sharedutil.FilterSlice(pls, func(pl *mediaprovider.Playlist) bool { + return pl.Owner == m.App.ServerManager.LoggedInUser + }) + if err != nil { + // TODO: surface this error to user + log.Printf("error getting user-owned playlists: %s", err.Error()) + return } - } - m.haveModal = true - pop.Show() + plNames := make([]string, 0, len(pls)) + for _, pl := range pls { + 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 *mediaprovider.Playlist) { @@ -399,7 +403,7 @@ func (c *Controller) doModalClosed() { } func (c *Controller) SetTrackFavorites(trackIDs []string, favorite bool) { - c.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{ + go c.App.ServerManager.Server.SetFavorite(mediaprovider.RatingFavoriteParameters{ TrackIDs: trackIDs, }, favorite) @@ -409,7 +413,7 @@ func (c *Controller) SetTrackFavorites(trackIDs []string, favorite bool) { } func (c *Controller) SetTrackRatings(trackIDs []string, rating int) { - c.App.ServerManager.Server.SetRating(mediaprovider.RatingFavoriteParameters{ + go c.App.ServerManager.Server.SetRating(mediaprovider.RatingFavoriteParameters{ TrackIDs: trackIDs, }, rating) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 3b455a7..7c583cf 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -608,11 +608,11 @@ func (t *TrackRow) Update(tr *mediaprovider.Track, rowNum int) { // Render favorite column if tr.Favorite { - t.isFavorite = false - t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon - } else { t.isFavorite = true t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon + } else { + t.isFavorite = false + t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon } t.rating.Rating = tr.Rating From 5361de658190cf7ff8ce0996942a2f8ab1d1b5ed Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 15 May 2023 18:21:28 -0700 Subject: [PATCH 09/11] add back prefetching album cover callback to iterators --- backend/app.go | 6 +- backend/mediaprovider/mediaprovider.go | 2 + .../mediaprovider/subsonic/albumiterator.go | 83 +++++++++---------- .../subsonic/subsonicmediaprovider.go | 7 +- backend/servermanager.go | 9 ++ 5 files changed, 61 insertions(+), 46 deletions(-) diff --git a/backend/app.go b/backend/app.go index 0813583..2de52aa 100644 --- a/backend/app.go +++ b/backend/app.go @@ -63,9 +63,9 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A a.ServerManager = NewServerManager(appName) a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling) a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName)) - //a.LibraryManager.PreCacheCoverFn = func(coverID string) { - //_, _ = a.ImageManager.GetCoverThumbnail(coverID) - //} + a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) { + _, _ = a.ImageManager.GetCoverThumbnail(coverID) + }) return a, nil } diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index c8e6aaa..e38b6d8 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -32,6 +32,8 @@ type Favorites struct { } type MediaProvider interface { + SetPrefetchCoverCallback(cb func(coverArtID string)) + GetAlbum(albumID string) (*AlbumWithTracks, error) GetArtist(artistID string) (*ArtistWithAlbums, error) diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go index 6ed86a2..e95a477 100644 --- a/backend/mediaprovider/subsonic/albumiterator.go +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -60,28 +60,28 @@ func filterMatches(f mediaprovider.AlbumFilter, album *subsonic.AlbumID3) bool { func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { if sortOrder == "" && len(filter.Genres) == 1 { - return s.newBaseIter("byGenre", filter, map[string]string{"genre": filter.Genres[0]}) + return s.newBaseIter("byGenre", filter, s.prefetchCoverCB, map[string]string{"genre": filter.Genres[0]}) } if sortOrder == "" && filter.ExcludeUnfavorited { - return s.newBaseIter("starred", filter, make(map[string]string)) + return s.newBaseIter("starred", filter, s.prefetchCoverCB, make(map[string]string)) } switch sortOrder { case AlbumSortRecentlyAdded: - return s.newBaseIter("newest", filter, make(map[string]string)) + return s.newBaseIter("newest", filter, s.prefetchCoverCB, make(map[string]string)) case AlbumSortRecentlyPlayed: - return s.newBaseIter("recent", filter, make(map[string]string)) + return s.newBaseIter("recent", filter, s.prefetchCoverCB, make(map[string]string)) case AlbumSortFrequentlyPlayed: - return s.newBaseIter("frequent", filter, make(map[string]string)) + return s.newBaseIter("frequent", filter, s.prefetchCoverCB, make(map[string]string)) case AlbumSortRandom: - return s.newRandomIter() + return s.newRandomIter(filter, s.prefetchCoverCB) case AlbumSortTitleAZ: - return s.newBaseIter("alphabeticalByName", filter, make(map[string]string)) + return s.newBaseIter("alphabeticalByName", filter, s.prefetchCoverCB, make(map[string]string)) case AlbumSortArtistAZ: - return s.newBaseIter("alphabeticalByArtist", filter, make(map[string]string)) + return s.newBaseIter("alphabeticalByArtist", filter, s.prefetchCoverCB, make(map[string]string)) case AlbumSortYearAscending: - return s.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: - return s.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: log.Printf("Undefined album sort order: %s", sortOrder) return nil @@ -89,12 +89,13 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov } func (s *subsonicMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { - return s.newSearchIter(searchQuery, filter) + return s.newSearchIter(searchQuery, filter, s.prefetchCoverCB) } type baseIter struct { listType string filter mediaprovider.AlbumFilter + prefetchCB func(string) serverPos int s *subsonic.Client opts map[string]string @@ -103,12 +104,13 @@ type baseIter struct { done bool } -func (s *subsonicMediaProvider) newBaseIter(listType string, filter mediaprovider.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{ - listType: listType, - filter: filter, - s: s.client, - opts: opts, + prefetchCB: cb, + listType: listType, + filter: filter, + s: s.client, + opts: opts, } } @@ -141,20 +143,18 @@ func (r *baseIter) Next() *mediaprovider.Album { } } r.prefetchedPos = 1 - /* - if r.l.PreCacheCoverFn != nil { - for _, album := range r.prefetched { - go r.l.PreCacheCoverFn(album.CoverArt) - } + if r.prefetchCB != nil { + for _, album := range r.prefetched { + go r.prefetchCB(album.CoverArtID) } - */ - + } return r.prefetched[0] } type searchIter struct { searchIterBase + prefetchCB func(string) filter mediaprovider.AlbumFilter prefetched []*subsonic.AlbumID3 prefetchedPos int @@ -162,12 +162,13 @@ type searchIter struct { done bool } -func (s *subsonicMediaProvider) newSearchIter(query string, filter mediaprovider.AlbumFilter) *searchIter { +func (s *subsonicMediaProvider) newSearchIter(query string, filter mediaprovider.AlbumFilter, cb func(string)) *searchIter { return &searchIter{ searchIterBase: searchIterBase{ query: query, s: s.client, }, + prefetchCB: cb, filter: filter, albumIDset: make(map[string]bool), } @@ -241,16 +242,16 @@ func (s *searchIter) addNewAlbums(al []*subsonic.AlbumID3) { continue } s.prefetched = append(s.prefetched, album) - /* - if s.l.PreCacheCoverFn != nil { - go s.l.PreCacheCoverFn(album.CoverArt) - } - */ + if s.prefetchCB != nil { + go s.prefetchCB(album.CoverArt) + } s.albumIDset[album.ID] = true } } type randomIter struct { + filter mediaprovider.AlbumFilter + prefetchCB func(coverArtID string) albumIDSet map[string]bool s *subsonic.Client prefetched []*subsonic.AlbumID3 @@ -266,8 +267,10 @@ type randomIter struct { done bool } -func (s *subsonicMediaProvider) newRandomIter() *randomIter { +func (s *subsonicMediaProvider) newRandomIter(filter mediaprovider.AlbumFilter, cb func(string)) *randomIter { return &randomIter{ + filter: filter, + prefetchCB: cb, s: s.client, albumIDSet: make(map[string]bool), } @@ -293,13 +296,11 @@ func (r *randomIter) Next() *mediaprovider.Album { } r.offset += len(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) - /* - if r.l.PreCacheCoverFn != nil { - go r.l.PreCacheCoverFn(album.CoverArt) - } - */ + if r.prefetchCB != nil { + go r.prefetchCB(album.CoverArt) + } r.albumIDSet[album.ID] = true } } @@ -315,14 +316,12 @@ func (r *randomIter) Next() *mediaprovider.Album { } var hitCount int for _, album := range albums { - if _, ok := r.albumIDSet[album.ID]; !ok { + if _, ok := r.albumIDSet[album.ID]; !ok && filterMatches(r.filter, album) { hitCount++ r.prefetched = append(r.prefetched, album) - /* - if r.l.PreCacheCoverFn != nil { - go r.l.PreCacheCoverFn(album.CoverArt) - } - */ + if r.prefetchCB != nil { + go r.prefetchCB(album.CoverArt) + } r.albumIDSet[album.ID] = true } } diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 170da6f..2c071bc 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -13,13 +13,18 @@ import ( ) type subsonicMediaProvider struct { - client *subsonic.Client + 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}) } diff --git a/backend/servermanager.go b/backend/servermanager.go index 1698700..0cc44b0 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -18,6 +18,7 @@ type ServerManager struct { ServerID uuid.UUID Server mediaprovider.MediaProvider + prefetchCoverCB func(string) appName string onServerConnected []func() onLogout []func() @@ -29,12 +30,20 @@ func NewServerManager(appName string) *ServerManager { 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 { cli, err := s.testConnectionAndCreateClient(conf.ServerConnection, password) if err != nil { return err } s.Server = subsonicMP.SubsonicMediaProvider(cli) + s.Server.SetPrefetchCoverCallback(s.prefetchCoverCB) s.LoggedInUser = conf.Username s.ServerID = conf.ID for _, cb := range s.onServerConnected { From ce2e4a5688d1d920f2d864204251dd44998e070a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 15 May 2023 18:42:48 -0700 Subject: [PATCH 10/11] fix random iterator with filter --- .../mediaprovider/subsonic/albumiterator.go | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go index e95a477..b3d43b4 100644 --- a/backend/mediaprovider/subsonic/albumiterator.go +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -281,31 +281,31 @@ func (r *randomIter) Next() *mediaprovider.Album { 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 { - 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 && filterMatches(r.filter, album) { - r.prefetched = append(r.prefetched, album) - if r.prefetchCB != nil { - go r.prefetchCB(album.CoverArt) - } - r.albumIDSet[album.ID] = true + // fetch albums from deterministic order + albums, err := r.s.GetAlbumList2("newest", map[string]string{"size": "25", "offset": strconv.Itoa(r.offset)}) + if err != nil { + log.Printf("error fetching albums: %s", err.Error()) + 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 && filterMatches(r.filter, album) { + r.prefetched = append(r.prefetched, album) + if r.prefetchCB != nil { + go r.prefetchCB(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 { @@ -316,13 +316,17 @@ func (r *randomIter) Next() *mediaprovider.Album { } var hitCount int for _, album := range albums { - if _, ok := r.albumIDSet[album.ID]; !ok && filterMatches(r.filter, album) { + 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++ - r.prefetched = append(r.prefetched, album) - if r.prefetchCB != nil { - go r.prefetchCB(album.CoverArt) - } 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 { From f8ba58a02779e3e40e1dac60c65d2ea38e9d43d2 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 15 May 2023 18:45:17 -0700 Subject: [PATCH 11/11] use default album sort order if none specified --- backend/mediaprovider/subsonic/albumiterator.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/mediaprovider/subsonic/albumiterator.go b/backend/mediaprovider/subsonic/albumiterator.go index b3d43b4..3698443 100644 --- a/backend/mediaprovider/subsonic/albumiterator.go +++ b/backend/mediaprovider/subsonic/albumiterator.go @@ -65,6 +65,9 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov if sortOrder == "" && filter.ExcludeUnfavorited { return s.newBaseIter("starred", filter, s.prefetchCoverCB, make(map[string]string)) } + if sortOrder == "" { + sortOrder = AlbumSortRecentlyAdded // default + } switch sortOrder { case AlbumSortRecentlyAdded: return s.newBaseIter("newest", filter, s.prefetchCoverCB, make(map[string]string))