From a22eef39f71309a85ac1c04877f623f69a533b52 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 10 Mar 2023 20:46:13 -0500 Subject: [PATCH] Add share download endpoint --- cmd/wire_gen.go | 7 ++-- core/archiver.go | 32 +++++++++++++------ core/share.go | 2 +- model/errors.go | 1 + server/public/handle_downloads.go | 16 ++++++++++ server/public/handle_shares.go | 27 +++++++++------- .../public/{public_endpoints.go => public.go} | 6 ++-- 7 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 server/public/handle_downloads.go rename server/public/{public_endpoints.go => public.go} (91%) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 673d8928..65ddb435 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -54,13 +54,13 @@ func CreateSubsonicAPIRouter() *subsonic.Router { artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata) transcodingCache := core.GetTranscodingCache() mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache) - archiver := core.NewArchiver(mediaStreamer, dataStore) + share := core.NewShare(dataStore) + archiver := core.NewArchiver(mediaStreamer, dataStore, share) players := core.NewPlayers(dataStore) scanner := GetScanner() broker := events.GetBroker() playlists := core.NewPlaylists(dataStore) playTracker := scrobbler.GetPlayTracker(dataStore, broker) - share := core.NewShare(dataStore) router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker, share) return router } @@ -76,7 +76,8 @@ func CreatePublicRouter() *public.Router { transcodingCache := core.GetTranscodingCache() mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache) share := core.NewShare(dataStore) - router := public.New(dataStore, artworkArtwork, mediaStreamer, share) + archiver := core.NewArchiver(mediaStreamer, dataStore, share) + router := public.New(dataStore, artworkArtwork, mediaStreamer, share, archiver) return router } diff --git a/core/archiver.go b/core/archiver.go index ba11d01c..6e2a4884 100644 --- a/core/archiver.go +++ b/core/archiver.go @@ -18,16 +18,18 @@ import ( type Archiver interface { ZipAlbum(ctx context.Context, id string, format string, bitrate int, w io.Writer) error ZipArtist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error + ZipShare(ctx context.Context, id string, w io.Writer) error ZipPlaylist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error } -func NewArchiver(ms MediaStreamer, ds model.DataStore) Archiver { - return &archiver{ds: ds, ms: ms} +func NewArchiver(ms MediaStreamer, ds model.DataStore, shares Share) Archiver { + return &archiver{ds: ds, ms: ms, shares: shares} } type archiver struct { - ds model.DataStore - ms MediaStreamer + ds model.DataStore + ms MediaStreamer + shares Share } func (a *archiver) ZipAlbum(ctx context.Context, id string, format string, bitrate int, out io.Writer) error { @@ -87,19 +89,29 @@ func (a *archiver) albumFilename(mf model.MediaFile, format string, isMultDisc b return fmt.Sprintf("%s/%s", mf.Album, file) } +func (a *archiver) ZipShare(ctx context.Context, id string, out io.Writer) error { + s, err := a.shares.Load(ctx, id) + if err != nil { + log.Error(ctx, "Error loading mediafiles from share", "id", id, err) + return err + } + log.Debug(ctx, "Zipping share", "name", s.ID, "format", s.Format, "bitrate", s.MaxBitRate, "numTracks", len(s.Tracks)) + return a.zipMediaFiles(ctx, id, s.Format, s.MaxBitRate, out, s.Tracks) +} + func (a *archiver) ZipPlaylist(ctx context.Context, id string, format string, bitrate int, out io.Writer) error { pls, err := a.ds.Playlist(ctx).GetWithTracks(id, true) if err != nil { log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err) return err } - return a.zipPlaylist(ctx, id, format, bitrate, out, pls) -} - -func (a *archiver) zipPlaylist(ctx context.Context, id string, format string, bitrate int, out io.Writer, pls *model.Playlist) error { - z := createZipWriter(out, format, bitrate) mfs := pls.MediaFiles() log.Debug(ctx, "Zipping playlist", "name", pls.Name, "format", format, "bitrate", bitrate, "numTracks", len(mfs)) + return a.zipMediaFiles(ctx, id, format, bitrate, out, mfs) +} + +func (a *archiver) zipMediaFiles(ctx context.Context, id string, format string, bitrate int, out io.Writer, mfs model.MediaFiles) error { + z := createZipWriter(out, format, bitrate) for idx, mf := range mfs { file := a.playlistFilename(mf, format, idx) _ = a.addFileToZip(ctx, z, mf, format, bitrate, file) @@ -132,7 +144,7 @@ func (a *archiver) addFileToZip(ctx context.Context, z *zip.Writer, mf model.Med } var r io.ReadCloser - if format != "raw" { + if format != "raw" && format != "" { r, err = a.ms.DoStream(ctx, &mf, format, bitrate) } else { r, err = os.Open(mf.Path) diff --git a/core/share.go b/core/share.go index aa87015f..3eb95ca4 100644 --- a/core/share.go +++ b/core/share.go @@ -35,7 +35,7 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error return nil, err } if !share.ExpiresAt.IsZero() && share.ExpiresAt.Before(time.Now()) { - return nil, model.ErrNotAvailable + return nil, model.ErrExpired } share.LastVisitedAt = time.Now() share.VisitCount++ diff --git a/model/errors.go b/model/errors.go index 8503bbaf..ff4be572 100644 --- a/model/errors.go +++ b/model/errors.go @@ -6,5 +6,6 @@ var ( ErrNotFound = errors.New("data not found") ErrInvalidAuth = errors.New("invalid authentication") ErrNotAuthorized = errors.New("not authorized") + ErrExpired = errors.New("access expired") ErrNotAvailable = errors.New("functionality not available") ) diff --git a/server/public/handle_downloads.go b/server/public/handle_downloads.go new file mode 100644 index 00000000..c6cf5a52 --- /dev/null +++ b/server/public/handle_downloads.go @@ -0,0 +1,16 @@ +package public + +import ( + "net/http" +) + +func (p *Router) handleDownloads(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get(":id") + if id == "" { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + + err := p.archiver.ZipShare(r.Context(), id, w) + checkShareError(r.Context(), w, err, id) +} diff --git a/server/public/handle_shares.go b/server/public/handle_shares.go index 776b4067..89dde771 100644 --- a/server/public/handle_shares.go +++ b/server/public/handle_shares.go @@ -1,6 +1,7 @@ package public import ( + "context" "errors" "net/http" @@ -27,18 +28,8 @@ func (p *Router) handleShares(w http.ResponseWriter, r *http.Request) { // If it is not, consider it a share ID s, err := p.share.Load(r.Context(), id) - switch { - case errors.Is(err, model.ErrNotAvailable): - log.Error(r, "Share expired", "id", id, err) - http.Error(w, "Share not available anymore", http.StatusGone) - case errors.Is(err, model.ErrNotFound): - log.Error(r, "Share not found", "id", id, err) - http.Error(w, "Share not found", http.StatusNotFound) - case err != nil: - log.Error(r, "Error retrieving share", "id", id, err) - http.Error(w, "Error retrieving share", http.StatusInternalServerError) - } if err != nil { + checkShareError(r.Context(), w, err, id) return } @@ -46,6 +37,20 @@ func (p *Router) handleShares(w http.ResponseWriter, r *http.Request) { server.IndexWithShare(p.ds, ui.BuildAssets(), s)(w, r) } +func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id string) { + switch { + case errors.Is(err, model.ErrExpired): + log.Error(ctx, "Share expired", "id", id, err) + http.Error(w, "Share not available anymore", http.StatusGone) + case errors.Is(err, model.ErrNotFound): + log.Error(ctx, "Share not found", "id", id, err) + http.Error(w, "Share not found", http.StatusNotFound) + case err != nil: + log.Error(ctx, "Error retrieving share", "id", id, err) + http.Error(w, "Error retrieving share", http.StatusInternalServerError) + } +} + func (p *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share { s.URL = ShareURL(r, s.ID) s.ImageURL = ImageURL(r, s.CoverArtID(), consts.UICoverArtSize) diff --git a/server/public/public_endpoints.go b/server/public/public.go similarity index 91% rename from server/public/public_endpoints.go rename to server/public/public.go index b7c6b9a7..3c43dbff 100644 --- a/server/public/public_endpoints.go +++ b/server/public/public.go @@ -20,13 +20,14 @@ type Router struct { http.Handler artwork artwork.Artwork streamer core.MediaStreamer + archiver core.Archiver share core.Share assetsHandler http.Handler ds model.DataStore } -func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, share core.Share) *Router { - p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share} +func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, share core.Share, archiver core.Archiver) *Router { + p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share, archiver: archiver} shareRoot := path.Join(conf.Server.BasePath, consts.URLPathPublic) p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets()))) p.Handler = p.routes() @@ -51,6 +52,7 @@ func (p *Router) routes() http.Handler { }) if conf.Server.EnableSharing { r.HandleFunc("/s/{id}", p.handleStream) + r.HandleFunc("/d/{id}", p.handleDownloads) r.HandleFunc("/{id}", p.handleShares) r.HandleFunc("/", p.handleShares) r.Handle("/*", p.assetsHandler)