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 83697d6..128866d 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/v2 v2.0.8 github.com/zalando/go-keyring v0.2.1 diff --git a/go.sum b/go.sum index b48e4d1..b13d885 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..fd57e9d 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -1,9 +1,13 @@ package controller import ( + "archive/zip" "fmt" "image" + "io" "log" + "os" + "path/filepath" "time" "github.com/dweymouth/supersonic/backend" @@ -113,6 +117,7 @@ func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) { tracklist.OnColumnVisibilityMenuShown = func(pop *widget.PopUp) { m.ClosePopUpOnEscape(pop) } + tracklist.OnDownload = m.ShowDownloadDialog } func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { @@ -516,3 +521,95 @@ func (c *Controller) SetTrackRatings(trackIDs []string, rating int) { c.App.PlaybackManager.OnTrackRatingChanged(id, rating) } } + +func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track) { + numTracks := len(tracks) + var fileName string + if numTracks == 1 { + fileName = filepath.Base(tracks[0].FilePath) + } else { + fileName = "downloaded_tracks.zip" + } + + dg := dialog.NewFileSave( + func(file fyne.URIWriteCloser, err error) { + if err != nil { + log.Println(err) + return + } + + if file == nil { + return + } + if numTracks == 1 { + go c.downloadTrack(tracks[0], file.URI().Path()) + } else { + go c.downloadTracks(tracks, file.URI().Path()) + } + + }, + c.MainWindow) + dg.SetFileName(fileName) + dg.Show() +} + +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 + } + + file, err := os.Create(filePath) + if err != nil { + log.Println(err) + return + } + defer file.Close() + + _, err = io.Copy(file, reader) + if err != nil { + log.Println(err) + return + } + + 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 + } + + fileName := filepath.Base(track.FilePath) + + 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 7cc72f7..1de6841 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -76,6 +76,7 @@ type Tracklist struct { 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) @@ -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) { + if t.OnDownload != nil { + t.OnDownload(tracks) + } +} + func (t *Tracklist) findTrackByID(id string) *mediaprovider.Track { idx := sharedutil.Find(t.tracks, func(tr *trackModel) bool { return tr.track.ID == id