diff --git a/core/archiver.go b/core/archiver.go index 6d8a4b9fd..cea107697 100644 --- a/core/archiver.go +++ b/core/archiver.go @@ -16,6 +16,7 @@ import ( type Archiver interface { ZipAlbum(ctx context.Context, id string, w io.Writer) error ZipArtist(ctx context.Context, id string, w io.Writer) error + ZipPlaylist(ctx context.Context, id string, w io.Writer) error } func NewArchiver(ds model.DataStore) Archiver { @@ -26,13 +27,15 @@ type archiver struct { ds model.DataStore } +type createHeader func(idx int, mf model.MediaFile) *zip.FileHeader + func (a *archiver) ZipAlbum(ctx context.Context, id string, out io.Writer) error { mfs, err := a.ds.MediaFile(ctx).FindByAlbum(id) if err != nil { log.Error(ctx, "Error loading mediafiles from album", "id", id, err) return err } - return a.zipTracks(ctx, id, out, mfs) + return a.zipTracks(ctx, id, out, mfs, a.createHeader) } func (a *archiver) ZipArtist(ctx context.Context, id string, out io.Writer) error { @@ -44,13 +47,22 @@ func (a *archiver) ZipArtist(ctx context.Context, id string, out io.Writer) erro log.Error(ctx, "Error loading mediafiles from artist", "id", id, err) return err } - return a.zipTracks(ctx, id, out, mfs) + return a.zipTracks(ctx, id, out, mfs, a.createHeader) } -func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs model.MediaFiles) error { +func (a *archiver) ZipPlaylist(ctx context.Context, id string, out io.Writer) error { + pls, err := a.ds.Playlist(ctx).Get(id) + if err != nil { + log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err) + return err + } + return a.zipTracks(ctx, id, out, pls.Tracks, a.createPlaylistHeader) +} + +func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs model.MediaFiles, ch createHeader) error { z := zip.NewWriter(out) - for _, mf := range mfs { - _ = a.addFileToZip(ctx, z, mf) + for idx, mf := range mfs { + _ = a.addFileToZip(ctx, z, mf, ch(idx, mf)) } err := z.Close() if err != nil { @@ -59,13 +71,26 @@ func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs return err } -func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile) error { +func (a *archiver) createHeader(idx int, mf model.MediaFile) *zip.FileHeader { _, file := filepath.Split(mf.Path) - w, err := z.CreateHeader(&zip.FileHeader{ + return &zip.FileHeader{ Name: fmt.Sprintf("%s/%s", mf.Album, file), Modified: mf.UpdatedAt, Method: zip.Store, - }) + } +} + +func (a *archiver) createPlaylistHeader(idx int, mf model.MediaFile) *zip.FileHeader { + _, file := filepath.Split(mf.Path) + return &zip.FileHeader{ + Name: fmt.Sprintf("%d - %s-%s", idx, mf.AlbumArtist, file), + Modified: mf.UpdatedAt, + Method: zip.Store, + } +} + +func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.MediaFile, zh *zip.FileHeader) error { + w, err := z.CreateHeader(zh) if err != nil { log.Error(ctx, "Error creating zip entry", "file", mf.Path, err) return err diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index 4a5378e78..b93fea44e 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -269,6 +269,10 @@ func getEntityByID(ctx context.Context, ds model.DataStore, id string) (interfac if err == nil { return al, nil } + pls, err := ds.Playlist(ctx).Get(id) + if err == nil { + return pls, nil + } mf, err := ds.MediaFile(ctx).Get(id) if err == nil { return mf, nil diff --git a/server/subsonic/stream.go b/server/subsonic/stream.go index 05c3480ee..32e4d3ffb 100644 --- a/server/subsonic/stream.go +++ b/server/subsonic/stream.go @@ -109,6 +109,9 @@ func (c *StreamController) Download(w http.ResponseWriter, r *http.Request) (*re case *model.Artist: setHeaders(v.Name) err = c.archiver.ZipArtist(ctx, id, w) + case *model.Playlist: + setHeaders(v.Name) + err = c.archiver.ZipPlaylist(ctx, id, w) default: err = model.ErrNotFound } diff --git a/ui/src/album/AlbumActions.js b/ui/src/album/AlbumActions.js index 876ce63ef..746634aa5 100644 --- a/ui/src/album/AlbumActions.js +++ b/ui/src/album/AlbumActions.js @@ -1,3 +1,5 @@ +import React from 'react' +import { useDispatch } from 'react-redux' import { Button, sanitizeListRestProps, @@ -7,8 +9,6 @@ import { import PlayArrowIcon from '@material-ui/icons/PlayArrow' import ShuffleIcon from '@material-ui/icons/Shuffle' import CloudDownloadOutlinedIcon from '@material-ui/icons/CloudDownloadOutlined' -import React from 'react' -import { useDispatch } from 'react-redux' import { playTracks, shuffleTracks } from '../audioplayer' import subsonic from '../subsonic' diff --git a/ui/src/playlist/PlaylistActions.js b/ui/src/playlist/PlaylistActions.js index ca54b5b4e..803ff9677 100644 --- a/ui/src/playlist/PlaylistActions.js +++ b/ui/src/playlist/PlaylistActions.js @@ -1,3 +1,5 @@ +import React from 'react' +import { useDispatch } from 'react-redux' import { Button, sanitizeListRestProps, @@ -6,9 +8,9 @@ import { } from 'react-admin' import PlayArrowIcon from '@material-ui/icons/PlayArrow' import ShuffleIcon from '@material-ui/icons/Shuffle' -import React from 'react' -import { useDispatch } from 'react-redux' +import CloudDownloadOutlinedIcon from '@material-ui/icons/CloudDownloadOutlined' import { playTracks, shuffleTracks } from '../audioplayer' +import subsonic from '../subsonic' const PlaylistActions = ({ className, @@ -16,6 +18,7 @@ const PlaylistActions = ({ data, exporter, permanentFilter, + playlistId, ...rest }) => { const dispatch = useDispatch() @@ -39,6 +42,14 @@ const PlaylistActions = ({ > + ) } diff --git a/ui/src/playlist/PlaylistShow.js b/ui/src/playlist/PlaylistShow.js index e4575912c..b15945e13 100644 --- a/ui/src/playlist/PlaylistShow.js +++ b/ui/src/playlist/PlaylistShow.js @@ -26,7 +26,7 @@ const PlaylistShow = (props) => { playlistId={props.id} readOnly={isReadOnly(record && record.owner)} title={} - actions={<PlaylistActions />} + actions={<PlaylistActions playlistId={props.id} />} filter={{ playlist_id: props.id }} resource={'playlistTrack'} exporter={false}