diff --git a/backend/mediaprovider/jellyfin/artistiterator.go b/backend/mediaprovider/jellyfin/artistiterator.go index ab85ee2..218d035 100644 --- a/backend/mediaprovider/jellyfin/artistiterator.go +++ b/backend/mediaprovider/jellyfin/artistiterator.go @@ -9,7 +9,7 @@ import ( "github.com/dweymouth/supersonic/sharedutil" ) -func (j *jellyfinMediaProvider) ArtistSortOrders() []string { +func (j *JellyfinMediaProvider) ArtistSortOrders() []string { return []string{ mediaprovider.ArtistSortAlbumCount, mediaprovider.ArtistSortNameAZ, @@ -17,7 +17,7 @@ func (j *jellyfinMediaProvider) ArtistSortOrders() []string { } } -func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator { +func (j *JellyfinMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator { var jfSort jellyfin.Sort var disablePagination bool var sortFn func([]*jellyfin.Artist) []*jellyfin.Artist @@ -63,7 +63,7 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediapro return helpers.NewArtistIterator(fetcher, filter, j.prefetchCoverCB) } -func (j *jellyfinMediaProvider) SearchArtists(searchQuery string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator { +func (j *JellyfinMediaProvider) SearchArtists(searchQuery string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator { // TODO: Jellyfin API is not returning search results for artists. // Uncomment the following code once the issue is resolved. // Related issue: https://github.com/jellyfin/jellyfin/issues/8222 diff --git a/backend/mediaprovider/jellyfin/iterators.go b/backend/mediaprovider/jellyfin/iterators.go index 10df9f7..9982144 100644 --- a/backend/mediaprovider/jellyfin/iterators.go +++ b/backend/mediaprovider/jellyfin/iterators.go @@ -9,7 +9,7 @@ import ( "github.com/dweymouth/supersonic/sharedutil" ) -func (j *jellyfinMediaProvider) AlbumSortOrders() []string { +func (j *JellyfinMediaProvider) AlbumSortOrders() []string { return []string{ mediaprovider.AlbumSortRecentlyAdded, mediaprovider.AlbumSortRandom, @@ -20,7 +20,7 @@ func (j *jellyfinMediaProvider) AlbumSortOrders() []string { } } -func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { +func (j *JellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { var jfSort jellyfin.Sort switch sortOrder { case mediaprovider.AlbumSortRecentlyAdded: @@ -75,7 +75,7 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov return helpers.NewAlbumIterator(fetcher, modifiedFilter, j.prefetchCoverCB) } -func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { +func (j *JellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { fetcher := func(offs, limit int) ([]*mediaprovider.Album, error) { var opts jellyfin.QueryOpts opts.Paging = jellyfin.Paging{StartIndex: offs, Limit: limit} @@ -89,7 +89,7 @@ func (j *jellyfinMediaProvider) SearchAlbums(searchQuery string, filter mediapro return helpers.NewAlbumIterator(fetcher, filter, j.prefetchCoverCB) } -func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.TrackIterator { +func (j *JellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.TrackIterator { var fetcher helpers.TrackFetchFn if searchQuery == "" { fetcher = func(offs, limit int) ([]*mediaprovider.Track, error) { diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index ab60543..ba93664 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -38,9 +38,9 @@ func (j *JellyfinServer) MediaProvider() mediaprovider.MediaProvider { return newJellyfinMediaProvider(&j.Client) } -var _ mediaprovider.MediaProvider = (*jellyfinMediaProvider)(nil) +var _ mediaprovider.MediaProvider = (*JellyfinMediaProvider)(nil) -type jellyfinMediaProvider struct { +type JellyfinMediaProvider struct { client *jellyfin.Client prefetchCoverCB func(coverArtID string) @@ -51,17 +51,17 @@ type jellyfinMediaProvider struct { } func newJellyfinMediaProvider(cli *jellyfin.Client) mediaprovider.MediaProvider { - return &jellyfinMediaProvider{ + return &JellyfinMediaProvider{ client: cli, genresCached: make([]*mediaprovider.Genre, 0), } } -func (j *jellyfinMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID string)) { +func (j *JellyfinMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID string)) { j.prefetchCoverCB = cb } -func (j *jellyfinMediaProvider) GetLibraries() ([]mediaprovider.Library, error) { +func (j *JellyfinMediaProvider) GetLibraries() ([]mediaprovider.Library, error) { v, err := j.client.GetUserViews() if err != nil { return nil, err @@ -71,41 +71,41 @@ func (j *jellyfinMediaProvider) GetLibraries() ([]mediaprovider.Library, error) }), nil } -func (j *jellyfinMediaProvider) SetLibrary(id string) error { +func (j *JellyfinMediaProvider) SetLibrary(id string) error { j.currentLibraryID = id j.genresCached = nil return nil } -func (j *jellyfinMediaProvider) CreatePlaylistWithTracks(name string, trackIDs []string) error { +func (j *JellyfinMediaProvider) CreatePlaylistWithTracks(name string, trackIDs []string) error { return j.client.CreatePlaylist(name, "", false, trackIDs) } -func (j *jellyfinMediaProvider) DeletePlaylist(id string) error { +func (j *JellyfinMediaProvider) DeletePlaylist(id string) error { return j.client.DeletePlaylist(id) } -func (j *jellyfinMediaProvider) CanMakePublicPlaylist() bool { +func (j *JellyfinMediaProvider) CanMakePublicPlaylist() bool { return false } -func (j *jellyfinMediaProvider) EditPlaylist(id, name, description string, public bool) error { +func (j *JellyfinMediaProvider) EditPlaylist(id, name, description string, public bool) error { return j.client.UpdatePlaylistMetadata(id, name, description, false) } -func (j *jellyfinMediaProvider) CreatePlaylist(name, description string, public bool) error { +func (j *JellyfinMediaProvider) CreatePlaylist(name, description string, public bool) error { return j.client.CreatePlaylist(name, description, public, nil) } -func (j *jellyfinMediaProvider) AddPlaylistTracks(id string, trackIDsToAdd []string) error { +func (j *JellyfinMediaProvider) AddPlaylistTracks(id string, trackIDsToAdd []string) error { return j.client.AddSongsToPlaylist(id, trackIDsToAdd) } -func (j *jellyfinMediaProvider) RemovePlaylistTracks(playlistID string, removeIdxs []int) error { +func (j *JellyfinMediaProvider) RemovePlaylistTracks(playlistID string, removeIdxs []int) error { return j.client.RemoveSongsFromPlaylist(playlistID, removeIdxs) } -func (j *jellyfinMediaProvider) ReplacePlaylistTracks(playlistID string, trackIDs []string) error { +func (j *JellyfinMediaProvider) ReplacePlaylistTracks(playlistID string, trackIDs []string) error { pl, err := j.client.GetPlaylist(playlistID) if err != nil { return err @@ -120,7 +120,7 @@ func (j *jellyfinMediaProvider) ReplacePlaylistTracks(playlistID string, trackID return j.client.AddSongsToPlaylist(playlistID, trackIDs) } -func (j *jellyfinMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWithTracks, error) { +func (j *JellyfinMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWithTracks, error) { al, err := j.client.GetAlbum(albumID) if err != nil { return nil, err @@ -138,7 +138,7 @@ func (j *jellyfinMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWi return album, nil } -func (j *jellyfinMediaProvider) GetAlbumInfo(albumID string) (*mediaprovider.AlbumInfo, error) { +func (j *JellyfinMediaProvider) GetAlbumInfo(albumID string) (*mediaprovider.AlbumInfo, error) { al, err := j.client.GetAlbum(albumID) if err != nil { return nil, err @@ -148,7 +148,7 @@ func (j *jellyfinMediaProvider) GetAlbumInfo(albumID string) (*mediaprovider.Alb }, nil } -func (j *jellyfinMediaProvider) GetArtist(artistID string) (*mediaprovider.ArtistWithAlbums, error) { +func (j *JellyfinMediaProvider) GetArtist(artistID string) (*mediaprovider.ArtistWithAlbums, error) { ar, err := j.client.GetArtist(artistID) if err != nil { return nil, err @@ -167,11 +167,11 @@ func (j *jellyfinMediaProvider) GetArtist(artistID string) (*mediaprovider.Artis return artist, nil } -func (j *jellyfinMediaProvider) GetArtistTracks(artistID string) ([]*mediaprovider.Track, error) { +func (j *JellyfinMediaProvider) GetArtistTracks(artistID string) ([]*mediaprovider.Track, error) { return helpers.GetArtistTracks(j, artistID) } -func (j *jellyfinMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.ArtistInfo, error) { +func (j *JellyfinMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.ArtistInfo, error) { ar, err := j.client.GetArtist(artistID) if err != nil { return nil, err @@ -186,7 +186,7 @@ func (j *jellyfinMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.A }, nil } -func (j *jellyfinMediaProvider) GetTrack(trackID string) (*mediaprovider.Track, error) { +func (j *JellyfinMediaProvider) GetTrack(trackID string) (*mediaprovider.Track, error) { tr, err := j.client.GetSong(trackID) if err != nil { return nil, err @@ -194,7 +194,7 @@ func (j *jellyfinMediaProvider) GetTrack(trackID string) (*mediaprovider.Track, return toTrack(tr), nil } -func (j *jellyfinMediaProvider) GetTopTracks(artist mediaprovider.Artist, limit int) ([]*mediaprovider.Track, error) { +func (j *JellyfinMediaProvider) GetTopTracks(artist mediaprovider.Artist, limit int) ([]*mediaprovider.Track, error) { var opts jellyfin.QueryOpts opts.Paging.Limit = limit opts.Filter.ArtistID = artist.ID @@ -213,7 +213,7 @@ func (j *jellyfinMediaProvider) GetTopTracks(artist mediaprovider.Artist, limit return sharedutil.MapSlice(tr, toTrack), nil } -func (j *jellyfinMediaProvider) GetRandomTracks(genreName string, limit int) ([]*mediaprovider.Track, error) { +func (j *JellyfinMediaProvider) GetRandomTracks(genreName string, limit int) ([]*mediaprovider.Track, error) { var opts jellyfin.QueryOpts opts.Paging.Limit = limit opts.Filter.Genres = []string{genreName} @@ -228,7 +228,7 @@ func (j *jellyfinMediaProvider) GetRandomTracks(genreName string, limit int) ([] return sharedutil.MapSlice(tr, toTrack), nil } -func (j *jellyfinMediaProvider) GetSimilarTracks(artistID string, limit int) ([]*mediaprovider.Track, error) { +func (j *JellyfinMediaProvider) GetSimilarTracks(artistID string, limit int) ([]*mediaprovider.Track, error) { tr, err := j.client.GetInstantMix(artistID, jellyfin.TypeArtist, limit) if err != nil { return nil, err @@ -236,11 +236,11 @@ func (j *jellyfinMediaProvider) GetSimilarTracks(artistID string, limit int) ([] return sharedutil.MapSlice(tr, toTrack), nil } -func (j *jellyfinMediaProvider) GetCoverArt(id string, size int) (image.Image, error) { +func (j *JellyfinMediaProvider) GetCoverArt(id string, size int) (image.Image, error) { return j.client.GetItemImage(id, "Primary", size, 92) } -func (j *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) { +func (j *JellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) { var wg sync.WaitGroup var favorites mediaprovider.Favorites @@ -278,7 +278,7 @@ func (j *jellyfinMediaProvider) GetFavorites() (mediaprovider.Favorites, error) return favorites, nil } -func (j *jellyfinMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) { +func (j *JellyfinMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) { if j.genresCached != nil && time.Now().Unix()-j.genresCachedAt < cacheValidDurationSeconds { return j.genresCached, nil } @@ -298,7 +298,7 @@ func (j *jellyfinMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) { return j.genresCached, nil } -func (j *jellyfinMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) { +func (j *JellyfinMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) { pl, err := j.client.GetPlaylists() if err != nil { return nil, err @@ -306,7 +306,7 @@ func (j *jellyfinMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error return sharedutil.MapSlice(pl, j.toPlaylist), nil } -func (j *jellyfinMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.PlaylistWithTracks, error) { +func (j *JellyfinMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.PlaylistWithTracks, error) { tr, err := j.client.GetPlaylistSongs(playlistID) if err != nil { return nil, err @@ -323,7 +323,7 @@ func (j *jellyfinMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.P return playlist, nil } -func (j *jellyfinMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error { +func (j *JellyfinMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error { var allIDs []string allIDs = append(allIDs, params.AlbumIDs...) allIDs = append(allIDs, params.ArtistIDs...) @@ -357,7 +357,7 @@ func (j *jellyfinMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteP return err } -func (j *jellyfinMediaProvider) GetStreamURL(trackID string, transcode *mediaprovider.TranscodeSettings, forceRaw bool) (string, error) { +func (j *JellyfinMediaProvider) GetStreamURL(trackID string, transcode *mediaprovider.TranscodeSettings, forceRaw bool) (string, error) { var jfTranscode *jellyfin.TranscodeOptions if transcode != nil { jfTranscode = &jellyfin.TranscodeOptions{ @@ -371,7 +371,7 @@ func (j *jellyfinMediaProvider) GetStreamURL(trackID string, transcode *mediapro return j.client.GetStreamURL(trackID, jfTranscode) } -func (j *jellyfinMediaProvider) DownloadTrack(trackID string) (io.Reader, error) { +func (j *JellyfinMediaProvider) DownloadTrack(trackID string) (io.Reader, error) { url, err := j.client.GetStreamURL(trackID, nil) if err != nil { return nil, err @@ -383,23 +383,23 @@ func (j *jellyfinMediaProvider) DownloadTrack(trackID string) (io.Reader, error) return resp.Body, nil } -func (j *jellyfinMediaProvider) ClientDecidesScrobble() bool { return false } +func (j *JellyfinMediaProvider) ClientDecidesScrobble() bool { return false } -func (j *jellyfinMediaProvider) TrackBeganPlayback(trackID string) error { +func (j *JellyfinMediaProvider) TrackBeganPlayback(trackID string) error { return j.client.UpdatePlayStatus(trackID, jellyfin.Start, 0) } -func (j *jellyfinMediaProvider) TrackEndedPlayback(trackID string, position int, submission bool) error { +func (j *JellyfinMediaProvider) TrackEndedPlayback(trackID string, position int, submission bool) error { return j.client.UpdatePlayStatus(trackID, jellyfin.Stop, int64(position)*runTimeTicksPerMicrosecond*1_000_000) } -func (j *jellyfinMediaProvider) RescanLibrary() error { +func (j *JellyfinMediaProvider) RescanLibrary() error { return j.client.RefreshLibrary() } -var _ mediaprovider.LyricsProvider = (*jellyfinMediaProvider)(nil) +var _ mediaprovider.LyricsProvider = (*JellyfinMediaProvider)(nil) -func (j *jellyfinMediaProvider) GetLyrics(tr *mediaprovider.Track) (*mediaprovider.Lyrics, error) { +func (j *JellyfinMediaProvider) GetLyrics(tr *mediaprovider.Track) (*mediaprovider.Lyrics, error) { l, err := j.client.GetLyrics(tr.ID) if err != nil { return nil, err @@ -515,13 +515,13 @@ func fillAlbum(a *jellyfin.Album, album *mediaprovider.Album) { album.ReleaseTypes = mediaprovider.ReleaseTypeAlbum } -func (j *jellyfinMediaProvider) toPlaylist(p *jellyfin.Playlist) *mediaprovider.Playlist { +func (j *JellyfinMediaProvider) toPlaylist(p *jellyfin.Playlist) *mediaprovider.Playlist { pl := &mediaprovider.Playlist{} j.fillPlaylist(p, pl) return pl } -func (j *jellyfinMediaProvider) fillPlaylist(p *jellyfin.Playlist, pl *mediaprovider.Playlist) { +func (j *JellyfinMediaProvider) fillPlaylist(p *jellyfin.Playlist, pl *mediaprovider.Playlist) { pl.Name = p.Name pl.ID = p.ID pl.CoverArtID = p.ID @@ -533,7 +533,7 @@ func (j *jellyfinMediaProvider) fillPlaylist(p *jellyfin.Playlist, pl *mediaprov pl.Public = false } -func (j *jellyfinMediaProvider) GetSongRadio(trackID string, count int) ([]*mediaprovider.Track, error) { +func (j *JellyfinMediaProvider) GetSongRadio(trackID string, count int) ([]*mediaprovider.Track, error) { tr, err := j.client.GetInstantMix(trackID, jellyfin.TypeSong, count) if err != nil { return nil, err diff --git a/backend/mediaprovider/jellyfin/searchall.go b/backend/mediaprovider/jellyfin/searchall.go index ee14380..10c417a 100644 --- a/backend/mediaprovider/jellyfin/searchall.go +++ b/backend/mediaprovider/jellyfin/searchall.go @@ -11,7 +11,7 @@ import ( "github.com/dweymouth/supersonic/sharedutil" ) -func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([]*mediaprovider.SearchResult, error) { +func (j *JellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([]*mediaprovider.SearchResult, error) { limit := maxResults / 3 var wg sync.WaitGroup var albums []*jellyfin.Album @@ -75,7 +75,7 @@ func (j *jellyfinMediaProvider) SearchAll(searchQuery string, maxResults int) ([ return results, nil } -func (j *jellyfinMediaProvider) mergeResults( +func (j *JellyfinMediaProvider) mergeResults( albums []*jellyfin.Album, artists []*jellyfin.Artist, songs []*jellyfin.Song, diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 3ded22e..cf82346 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -42,6 +42,9 @@ type PlaybackManager struct { lastPlayingID string wfmUpdateImageCancel context.CancelFunc wfmImageJobs [3]*WaveformImageJob + + // whether autoplay tracks are currently being fetched/enqueued + pendingAutoplay bool } type RemotePlaybackDevice struct { @@ -100,8 +103,14 @@ func (p *PlaybackManager) addOnTrackChangeHook() { // On Windows, MPV sometimes fails to start playback when switching to a track // with a different sample rate than the previous. If this is detected, // send a command to the MPV player to force restart playback. - p.OnPlayTimeUpdate(func(curTime, _ float64, _ bool) { + p.OnPlayTimeUpdate(func(curTime, totalTime float64, _ bool) { p.lastPlayTime = curTime + + // enqueue autoplay tracks if enabled and nearing end of queue + if p.cfg.Autoplay && !p.pendingAutoplay && totalTime-curTime < 10.0 && + p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + p.enqueueAutoplayTracks() + } }) p.engine.onBeforeSongChange = append(p.engine.onBeforeSongChange, func(item mediaprovider.MediaItem) { @@ -116,10 +125,6 @@ func (p *PlaybackManager) addOnTrackChangeHook() { }) p.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { - // Autoplay if enabled and we are on the last track - if p.cfg.Autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { - p.enqueueAutoplayTracks() - } p.handleWaveformImageSongChange(item) if runtime.GOOS != "windows" { @@ -737,7 +742,10 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { // since this func is invoked in a callback from the playback engine, // need to do the rest async as it may take time and block other callbacks + p.pendingAutoplay = true go func() { + defer func() { p.pendingAutoplay = false }() + // first 2 strategies - similar by artist, and similar by genres - only work for tracks if nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeTrack { tr := nowPlaying.(*mediaprovider.Track) diff --git a/go.mod b/go.mod index a025346..e585fb9 100644 --- a/go.mod +++ b/go.mod @@ -33,8 +33,9 @@ require ( require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect - fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect + fyne.io/systray v1.12.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect + github.com/FyshOS/fancyfs v0.0.0-20251025194026-1f03098ff624 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v1.1.1 // indirect @@ -67,7 +68,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace fyne.io/fyne/v2 v2.7.1 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20251115180245-c1288f7754ab +replace fyne.io/fyne/v2 v2.7.1 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20260121163844-a0f2c1dd0601 replace github.com/go-audio/wav v1.1.0 => github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0 diff --git a/go.sum b/go.sum index c5193bc..991f2e8 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI= -fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= +fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/20after4/configdir v0.1.1 h1:ylL5dO+aGxBV4jDtG9Ej9hZfeE8Fw9jULQVq0N+ErJ8= github.com/20after4/configdir v0.1.1/go.mod h1:kZ7yOiD6MFUABqBI2/N62QA1wyEB8sGeI+mygme1pl4= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/FyshOS/fancyfs v0.0.0-20251025194026-1f03098ff624 h1:ryzrudl71BPWW9jalH/6qSXOmj/B96lunzJEi6zuuR4= +github.com/FyshOS/fancyfs v0.0.0-20251025194026-1f03098ff624/go.mod h1:oLKntpN0BPY75aajV735V/14CnSF/GEHCa6mKNDhOjw= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/boxes-ltd/imaging v1.7.1 h1:JlfPQkr9sowLuPY24OLNw1Wa0UjriPVo2BN0YyxdCf0= @@ -25,8 +27,8 @@ github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c h1:UX github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c/go.mod h1:Idgzr4LYzve8IPHF1stLO1bdGQZnDKt/bT9WfJflCGw= github.com/dweymouth/fyne-tooltip v0.3.0 h1:NKCyTkh9NtvnTsiHtTOtaJzRDOFYP8AckQ2tyhOh6JY= github.com/dweymouth/fyne-tooltip v0.3.0/go.mod h1:m04ShLW/Tp6LXrNieTumApvNgo7YSB+wi+jZTN+kDBU= -github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20251115180245-c1288f7754ab h1:3a3Dop0y5uGYSgjoyReYlwZqpo7xFQu+tULfZueCjEQ= -github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20251115180245-c1288f7754ab/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE= +github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20260121163844-a0f2c1dd0601 h1:Z75H0KinbkuXn0y/+vFB4R6jvwgwHkgCivvPT5fIYcc= +github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20260121163844-a0f2c1dd0601/go.mod h1:M3Fkb8eUU6se8E+coikK8x7f3vCIakxAW7bPyw3d9fA= github.com/dweymouth/go-jellyfin v0.0.0-20250928223159-bd2fb9681ef5 h1:Or5VJodg7cGmdnBIcS+FrEH0twBi7mZsFCz6V0vCOQI= github.com/dweymouth/go-jellyfin v0.0.0-20250928223159-bd2fb9681ef5/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI= github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0 h1:mYcctuWgVArHhSLJxndlUM43C3hoE18BLDBkXKM2tl0= diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index aa579ad..d607f06 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -12,6 +12,7 @@ import ( "fyne.io/fyne/v2/widget" "github.com/deluan/sanitize" "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/backend/mediaprovider/jellyfin" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/util" ) @@ -49,7 +50,13 @@ func (sp *SelectPlaylist) fetchUserOwnedPlaylists() { log.Printf("error getting playlists: %s", err.Error()) } userPlaylists := sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool { - return playlist.Owner == sp.loggedInUser + if _, isJellyfin := sp.mp.(*jellyfin.JellyfinMediaProvider); isJellyfin { + // Jellyfin usernames are case-insensitive + return strings.EqualFold(playlist.Owner, sp.loggedInUser) + } else { + // Subsonic usernames are case-sensitive + return playlist.Owner == sp.loggedInUser + } }) sp.allPlaylistResuts = sharedutil.MapSlice(userPlaylists, sp.playlistToSearchResult) } diff --git a/ui/visualizations/peakmeter.go b/ui/visualizations/peakmeter.go index 5da0135..67296c8 100644 --- a/ui/visualizations/peakmeter.go +++ b/ui/visualizations/peakmeter.go @@ -32,6 +32,10 @@ type PeakMeter struct { lPeakHoldFrame uint64 rPeakHoldFrame uint64 frameCounter uint64 + + // true iff only a layout is needed, rathern than a full refresh. + // cleared by the renderer + refreshLayoutOnly bool } func NewPeakMeter() *PeakMeter { @@ -68,6 +72,7 @@ func (p *PeakMeter) UpdatePeaks(lPeak, rPeak, lRMS, rRMS float64) { } p.frameCounter++ + p.refreshLayoutOnly = true p.Refresh() } @@ -75,6 +80,11 @@ func (p *PeakMeter) CreateRenderer() fyne.WidgetRenderer { return newPeakMeterRenderer(p) } +func (p *PeakMeter) Refresh() { + p.refreshLayoutOnly = false + p.BaseWidget.Refresh() +} + type peakMeterRenderer struct { p *PeakMeter @@ -168,6 +178,12 @@ func (l *peakMeterRenderer) Layout(size fyne.Size) { } func (l *peakMeterRenderer) Refresh() { + if l.p.refreshLayoutOnly { + l.p.refreshLayoutOnly = false + l.Layout(l.p.Size()) + return + } + foreground := theme.ForegroundColor() background := theme.BackgroundColor() errC := theme.ErrorColor() diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index d5063f4..847dcc1 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -410,6 +410,17 @@ func (t *tracklistRowBase) TrackID() string { } func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int, onUpdate func()) { + // Show only columns configured to be visible + for i := 2; i < len(t.tracklist.columns); i++ { + t.setColVisibility(i, !t.tracklist.visibleColumns[i]) + } + + // Ealy return if nothing else needs updating + if !t.needsUpdate(tm, rowNum) { + return + } + + // Throttle updates when many are happening in a short time if tracklistUpdateCounter.NumEventsSince(time.Now().Add(-300*time.Millisecond)) > 10 { t.doUpdate(&emptyTrack, 1) if t.nextUpdateModel == nil { @@ -434,11 +445,31 @@ func (t *tracklistRowBase) Update(tm *util.TrackListModel, rowNum int, onUpdate } } +func (t *tracklistRowBase) needsUpdate(tm *util.TrackListModel, rowNum int) bool { + tr := tm.Track() + if tm.Selected != t.Selected { + return true + } + if t.trackID != tr.ID || t.trackNum != rowNum { + return true + } + if t.playCount != tr.PlayCount || t.isFavorite != tr.Favorite || t.rating.Rating != tr.Rating { + return true + } + if t.rating.IsDisabled != t.tracklist.Options.DisableRating { + return true + } + if isPlaying := t.tracklist.nowPlayingID == tr.ID; isPlaying != t.isPlaying { + return true + } + return false +} + func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) { - changed := false + fullRefresh := false if tm.Selected != t.Selected { t.Selected = tm.Selected - changed = true + fullRefresh = true } // Update info that can change if this row is bound to @@ -473,7 +504,7 @@ func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) { t.dateAdded.Text = util.FormatDate(tr.DateAdded) t.path.Text = tr.FilePath t.path.SetToolTip(tr.FilePath) - changed = true + fullRefresh = true } // Update track num if needed @@ -493,21 +524,20 @@ func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) { } else { str = strconv.Itoa(rowNum) } - t.num.Text = str - changed = true + t.num.SetText(str) } // Update play count if needed if tr.PlayCount != t.playCount { t.playCount = tr.PlayCount - t.plays.Text = strconv.Itoa(int(tr.PlayCount)) - changed = true + t.plays.SetText(strconv.Itoa(int(tr.PlayCount))) } // Render whether track is playing or not if isPlaying := t.tracklist.nowPlayingID == tr.ID; isPlaying != t.isPlaying { t.isPlaying = isPlaying t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying + t.name.Refresh() if isPlaying { t.originalNumColContent = t.Content.(*fyne.Container).Objects[0] @@ -515,14 +545,14 @@ func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) { } else { t.Content.(*fyne.Container).Objects[0] = t.originalNumColContent } - changed = true + canvas.Refresh(t) } // Update favorite column if tr.Favorite != t.isFavorite { t.isFavorite = tr.Favorite t.favorite.Objects[0].(*FavoriteIcon).Favorite = tr.Favorite - changed = true + canvas.Refresh(t.favorite) } // Update rating column @@ -535,14 +565,7 @@ func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) { t.rating.Refresh() } - // Show only columns configured to be visible - for i := 2; i < len(t.tracklist.columns); i++ { - if ch := t.setColVisibility(i, !t.tracklist.visibleColumns[i]); ch { - changed = true - } - } - - if changed { + if fullRefresh { tracklistUpdateCounter.Add() t.Refresh() }