From f190aaf73219425c490c252e8cad1abd257af79a Mon Sep 17 00:00:00 2001 From: natilou <30585029+natilou@users.noreply.github.com> Date: Wed, 14 Jun 2023 21:55:34 -0300 Subject: [PATCH 1/4] Ability to download tracks - Adding the option to download the selected track or tracks. When the download is clicked it opens a dialog that lets you choose where you want to save the track and the file name (by default the file name is the track number, the track name and the extension, example "03 - my song.mp3") If there are many tracks selected it will open one dialog for each track - Next steps: download an album and a playlist using zip --- backend/mediaprovider/mediaprovider.go | 7 ++- .../subsonic/subsonicmediaprovider.go | 5 ++ go.mod | 2 +- go.sum | 2 + ui/controller/controller.go | 50 +++++++++++++++++++ ui/widgets/tracklist.go | 23 ++++++--- 6 files changed, 81 insertions(+), 8 deletions(-) diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index e38b6d8..9b2fd3c 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -1,6 +1,9 @@ package mediaprovider -import "image" +import ( + "image" + "io" +) type AlbumFilter struct { MinYear int @@ -83,4 +86,6 @@ type MediaProvider interface { DeletePlaylist(id string) error Scrobble(trackID string, submission bool) error + + DownloadTrack(trackID string) (io.Reader, error) } diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index a51bca6..446378f 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -3,6 +3,7 @@ package subsonic import ( "errors" "image" + "io" "math" "strconv" "sync" @@ -249,6 +250,10 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar return err } +func (s *subsonicMediaProvider) DownloadTrack(trackID string) (io.Reader, error) { + return s.client.Download(trackID) +} + func toTrack(ch *subsonic.Child) *mediaprovider.Track { if ch == nil { return nil diff --git a/go.mod b/go.mod index f59e29d..431921a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( fyne.io/x/fyne v0.0.0-20230611151101-afdcd6b92cf3 github.com/20after4/configdir v0.1.1 github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee - github.com/dweymouth/go-subsonic v0.0.0-20230513020020-0790f53c2868 + github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4 github.com/google/uuid v1.3.0 github.com/pelletier/go-toml v1.9.3 github.com/zalando/go-keyring v0.2.1 diff --git a/go.sum b/go.sum index 7be6d84..f84c408 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,8 @@ github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31 github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= github.com/dweymouth/go-subsonic v0.0.0-20230513020020-0790f53c2868 h1:403Dden/cdQyDM8ydonHLxTBPjgQXZ76PUfmBG92+Dw= github.com/dweymouth/go-subsonic v0.0.0-20230513020020-0790f53c2868/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= +github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4 h1:cuyvB4GjTMBHKJwMJ/LFpuKfSVXuCheNj6fgxBa3m1Y= +github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 2f0fb03..74cd80d 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -3,7 +3,10 @@ package controller import ( "fmt" "image" + "io" "log" + "os" + "strings" "time" "github.com/dweymouth/supersonic/backend" @@ -113,6 +116,7 @@ func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) { tracklist.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { m.ClosePopUpOnEscape(pop) } + tracklist.OnShowDownloadDialog = m.ShowDownloadDialog } func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { @@ -516,3 +520,49 @@ func (c *Controller) SetTrackRatings(trackIDs []string, rating int) { c.App.PlaybackManager.OnTrackRatingChanged(id, rating) } } + +func (c *Controller) ShowDownloadDialog(track *mediaprovider.Track) { + parts := strings.Split(track.FilePath, "/") + fileName := parts[len(parts)-1] + + dg := dialog.NewFileSave( + func(file fyne.URIWriteCloser, err error) { + if err != nil { + log.Println(err) + return + } + + if file == nil { + return + } + + filePath := file.URI().Path() + go c.downloadTrack(track.ID, filePath) + }, + c.MainWindow) + dg.SetFileName(fileName) + dg.Show() +} + +func (c *Controller) downloadTrack(trackID, filePath string) { + reader, err := c.App.ServerManager.Server.DownloadTrack(trackID) + if err != nil { + log.Println(err) + return + } + + saveDir, err := os.Create(filePath) + if err != nil { + log.Println(err) + return + } + defer saveDir.Close() + + _, err = io.Copy(saveDir, reader) + if err != nil { + log.Println(err) + return + } + + log.Printf("Saved song to: %s\n", saveDir.Name()) +} diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 7cc72f7..6776995 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -70,12 +70,13 @@ type Tracklist struct { DisableSorting bool // user action callbacks - OnPlayTrackAt func(int) - 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) + OnPlayTrackAt func(int) + 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) + OnShowDownloadDialog func(track *mediaprovider.Track) OnShowArtistPage func(artistID string) OnShowAlbumPage func(albumID string) @@ -508,6 +509,10 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { t.OnAddToPlaylist(t.SelectedTrackIDs()) } })) + t.ctxMenu.Items = append(t.ctxMenu.Items, + fyne.NewMenuItem("Download", func() { + t.onDownload(t.selectedTracks()) + })) t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator()) t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItem("Set favorite", func() { @@ -582,6 +587,12 @@ func (t *Tracklist) onAlbumTapped(albumID string) { } } +func (t *Tracklist) onDownload(tracks []*mediaprovider.Track) { + for _, track := range tracks { + t.OnShowDownloadDialog(track) + } +} + func (t *Tracklist) findTrackByID(id string) *mediaprovider.Track { idx := sharedutil.Find(t.tracks, func(tr *trackModel) bool { return tr.track.ID == id From 15a63681de654c1122fd0b45a8f0ebfcfacd1d03 Mon Sep 17 00:00:00 2001 From: natilou <30585029+natilou@users.noreply.github.com> Date: Thu, 15 Jun 2023 22:16:46 -0300 Subject: [PATCH 2/4] Implementing downloading multiple tracks into a single zip file, changing callback signature --- ui/controller/controller.go | 73 +++++++++++++++++++++++++++++++------ ui/widgets/tracklist.go | 18 ++++----- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 74cd80d..f382f3d 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -1,6 +1,7 @@ package controller import ( + "archive/zip" "fmt" "image" "io" @@ -116,7 +117,7 @@ func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) { tracklist.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { m.ClosePopUpOnEscape(pop) } - tracklist.OnShowDownloadDialog = m.ShowDownloadDialog + tracklist.OnDownload = m.ShowDownloadDialog } func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { @@ -521,9 +522,15 @@ func (c *Controller) SetTrackRatings(trackIDs []string, rating int) { } } -func (c *Controller) ShowDownloadDialog(track *mediaprovider.Track) { - parts := strings.Split(track.FilePath, "/") - fileName := parts[len(parts)-1] +func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track) { + numTracks := len(tracks) + var fileName string + if numTracks == 1 { + parts := strings.Split(tracks[0].FilePath, "/") + fileName = parts[len(parts)-1] + } else { + fileName = "downloaded_tracks.zip" + } dg := dialog.NewFileSave( func(file fyne.URIWriteCloser, err error) { @@ -535,34 +542,76 @@ func (c *Controller) ShowDownloadDialog(track *mediaprovider.Track) { if file == nil { return } + if numTracks == 1 { + go c.downloadTrack(tracks[0], file.URI().Path()) + } else { + go c.downloadTracks(tracks, file.URI().Path()) + } - filePath := file.URI().Path() - go c.downloadTrack(track.ID, filePath) }, c.MainWindow) dg.SetFileName(fileName) dg.Show() } -func (c *Controller) downloadTrack(trackID, filePath string) { - reader, err := c.App.ServerManager.Server.DownloadTrack(trackID) +func (c *Controller) downloadTrack(track *mediaprovider.Track, filePath string) { + reader, err := c.App.ServerManager.Server.DownloadTrack(track.ID) if err != nil { log.Println(err) return } - saveDir, err := os.Create(filePath) + file, err := os.Create(filePath) if err != nil { log.Println(err) return } - defer saveDir.Close() + defer file.Close() - _, err = io.Copy(saveDir, reader) + _, err = io.Copy(file, reader) if err != nil { log.Println(err) return } - log.Printf("Saved song to: %s\n", saveDir.Name()) + log.Printf("Saved song %s to: %s\n", track.Name, filePath) +} + +func (c *Controller) downloadTracks(tracks []*mediaprovider.Track, filePath string) { + zipFile, err := os.Create(filePath) + if err != nil { + log.Println(err) + return + } + defer zipFile.Close() + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + for _, track := range tracks { + reader, err := c.App.ServerManager.Server.DownloadTrack(track.ID) + if err != nil { + log.Println(err) + continue + } + + parts := strings.Split(track.FilePath, "/") + fileName := parts[len(parts)-1] + + fileWriter, err := zipWriter.Create(fileName) + if err != nil { + log.Println(err) + continue + } + + _, err = io.Copy(fileWriter, reader) + if err != nil { + log.Println(err) + continue + } + + log.Printf("Saved song %s to: %s\n", track.Name, filePath) + } + + log.Printf("Finished download to: %s\n", filePath) } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 6776995..48b3b12 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -70,13 +70,13 @@ type Tracklist struct { DisableSorting bool // user action callbacks - OnPlayTrackAt func(int) - 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) - OnShowDownloadDialog func(track *mediaprovider.Track) + OnPlayTrackAt func(int) + 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) + OnDownload func(tracks []*mediaprovider.Track) OnShowArtistPage func(artistID string) OnShowAlbumPage func(albumID string) @@ -588,9 +588,7 @@ func (t *Tracklist) onAlbumTapped(albumID string) { } func (t *Tracklist) onDownload(tracks []*mediaprovider.Track) { - for _, track := range tracks { - t.OnShowDownloadDialog(track) - } + t.OnDownload(tracks) } func (t *Tracklist) findTrackByID(id string) *mediaprovider.Track { From 701001121f8e2f45f839283b9a8f79fc0fec249c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 15 Jun 2023 21:01:52 -0700 Subject: [PATCH 3/4] add nil check around OnDownload call --- ui/widgets/tracklist.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 48b3b12..1de6841 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -588,7 +588,9 @@ func (t *Tracklist) onAlbumTapped(albumID string) { } func (t *Tracklist) onDownload(tracks []*mediaprovider.Track) { - t.OnDownload(tracks) + if t.OnDownload != nil { + t.OnDownload(tracks) + } } func (t *Tracklist) findTrackByID(id string) *mediaprovider.Track { From b1a266d217591f1e945047a5840c03b303ed81e8 Mon Sep 17 00:00:00 2001 From: natilou <30585029+natilou@users.noreply.github.com> Date: Fri, 16 Jun 2023 11:40:11 -0300 Subject: [PATCH 4/4] using filepath.Base(track.FilePath) for fileName --- ui/controller/controller.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index f382f3d..fd57e9d 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -7,7 +7,7 @@ import ( "io" "log" "os" - "strings" + "path/filepath" "time" "github.com/dweymouth/supersonic/backend" @@ -526,8 +526,7 @@ func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track) { numTracks := len(tracks) var fileName string if numTracks == 1 { - parts := strings.Split(tracks[0].FilePath, "/") - fileName = parts[len(parts)-1] + fileName = filepath.Base(tracks[0].FilePath) } else { fileName = "downloaded_tracks.zip" } @@ -595,8 +594,7 @@ func (c *Controller) downloadTracks(tracks []*mediaprovider.Track, filePath stri continue } - parts := strings.Split(track.FilePath, "/") - fileName := parts[len(parts)-1] + fileName := filepath.Base(track.FilePath) fileWriter, err := zipWriter.Create(fileName) if err != nil {