mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 04:27:37 +03:00
Add share download endpoint
This commit is contained in:
parent
50d9838652
commit
a22eef39f7
7 changed files with 64 additions and 27 deletions
|
@ -54,13 +54,13 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
||||||
transcodingCache := core.GetTranscodingCache()
|
transcodingCache := core.GetTranscodingCache()
|
||||||
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
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)
|
players := core.NewPlayers(dataStore)
|
||||||
scanner := GetScanner()
|
scanner := GetScanner()
|
||||||
broker := events.GetBroker()
|
broker := events.GetBroker()
|
||||||
playlists := core.NewPlaylists(dataStore)
|
playlists := core.NewPlaylists(dataStore)
|
||||||
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
|
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
|
||||||
share := core.NewShare(dataStore)
|
|
||||||
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker, share)
|
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scanner, broker, playlists, playTracker, share)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,8 @@ func CreatePublicRouter() *public.Router {
|
||||||
transcodingCache := core.GetTranscodingCache()
|
transcodingCache := core.GetTranscodingCache()
|
||||||
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||||
share := core.NewShare(dataStore)
|
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
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,16 +18,18 @@ import (
|
||||||
type Archiver interface {
|
type Archiver interface {
|
||||||
ZipAlbum(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
|
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
|
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
|
ZipPlaylist(ctx context.Context, id string, format string, bitrate int, w io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArchiver(ms MediaStreamer, ds model.DataStore) Archiver {
|
func NewArchiver(ms MediaStreamer, ds model.DataStore, shares Share) Archiver {
|
||||||
return &archiver{ds: ds, ms: ms}
|
return &archiver{ds: ds, ms: ms, shares: shares}
|
||||||
}
|
}
|
||||||
|
|
||||||
type archiver struct {
|
type archiver struct {
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
ms MediaStreamer
|
ms MediaStreamer
|
||||||
|
shares Share
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *archiver) ZipAlbum(ctx context.Context, id string, format string, bitrate int, out io.Writer) error {
|
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)
|
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 {
|
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)
|
pls, err := a.ds.Playlist(ctx).GetWithTracks(id, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err)
|
log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err)
|
||||||
return 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()
|
mfs := pls.MediaFiles()
|
||||||
log.Debug(ctx, "Zipping playlist", "name", pls.Name, "format", format, "bitrate", bitrate, "numTracks", len(mfs))
|
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 {
|
for idx, mf := range mfs {
|
||||||
file := a.playlistFilename(mf, format, idx)
|
file := a.playlistFilename(mf, format, idx)
|
||||||
_ = a.addFileToZip(ctx, z, mf, format, bitrate, file)
|
_ = 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
|
var r io.ReadCloser
|
||||||
if format != "raw" {
|
if format != "raw" && format != "" {
|
||||||
r, err = a.ms.DoStream(ctx, &mf, format, bitrate)
|
r, err = a.ms.DoStream(ctx, &mf, format, bitrate)
|
||||||
} else {
|
} else {
|
||||||
r, err = os.Open(mf.Path)
|
r, err = os.Open(mf.Path)
|
||||||
|
|
|
@ -35,7 +35,7 @@ func (s *shareService) Load(ctx context.Context, id string) (*model.Share, error
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !share.ExpiresAt.IsZero() && share.ExpiresAt.Before(time.Now()) {
|
if !share.ExpiresAt.IsZero() && share.ExpiresAt.Before(time.Now()) {
|
||||||
return nil, model.ErrNotAvailable
|
return nil, model.ErrExpired
|
||||||
}
|
}
|
||||||
share.LastVisitedAt = time.Now()
|
share.LastVisitedAt = time.Now()
|
||||||
share.VisitCount++
|
share.VisitCount++
|
||||||
|
|
|
@ -6,5 +6,6 @@ var (
|
||||||
ErrNotFound = errors.New("data not found")
|
ErrNotFound = errors.New("data not found")
|
||||||
ErrInvalidAuth = errors.New("invalid authentication")
|
ErrInvalidAuth = errors.New("invalid authentication")
|
||||||
ErrNotAuthorized = errors.New("not authorized")
|
ErrNotAuthorized = errors.New("not authorized")
|
||||||
|
ErrExpired = errors.New("access expired")
|
||||||
ErrNotAvailable = errors.New("functionality not available")
|
ErrNotAvailable = errors.New("functionality not available")
|
||||||
)
|
)
|
||||||
|
|
16
server/public/handle_downloads.go
Normal file
16
server/public/handle_downloads.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package public
|
package public
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"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
|
// If it is not, consider it a share ID
|
||||||
s, err := p.share.Load(r.Context(), 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 {
|
if err != nil {
|
||||||
|
checkShareError(r.Context(), w, err, id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +37,20 @@ func (p *Router) handleShares(w http.ResponseWriter, r *http.Request) {
|
||||||
server.IndexWithShare(p.ds, ui.BuildAssets(), s)(w, r)
|
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 {
|
func (p *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
|
||||||
s.URL = ShareURL(r, s.ID)
|
s.URL = ShareURL(r, s.ID)
|
||||||
s.ImageURL = ImageURL(r, s.CoverArtID(), consts.UICoverArtSize)
|
s.ImageURL = ImageURL(r, s.CoverArtID(), consts.UICoverArtSize)
|
||||||
|
|
|
@ -20,13 +20,14 @@ type Router struct {
|
||||||
http.Handler
|
http.Handler
|
||||||
artwork artwork.Artwork
|
artwork artwork.Artwork
|
||||||
streamer core.MediaStreamer
|
streamer core.MediaStreamer
|
||||||
|
archiver core.Archiver
|
||||||
share core.Share
|
share core.Share
|
||||||
assetsHandler http.Handler
|
assetsHandler http.Handler
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, share core.Share) *Router {
|
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}
|
p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share, archiver: archiver}
|
||||||
shareRoot := path.Join(conf.Server.BasePath, consts.URLPathPublic)
|
shareRoot := path.Join(conf.Server.BasePath, consts.URLPathPublic)
|
||||||
p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
|
p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
|
||||||
p.Handler = p.routes()
|
p.Handler = p.routes()
|
||||||
|
@ -51,6 +52,7 @@ func (p *Router) routes() http.Handler {
|
||||||
})
|
})
|
||||||
if conf.Server.EnableSharing {
|
if conf.Server.EnableSharing {
|
||||||
r.HandleFunc("/s/{id}", p.handleStream)
|
r.HandleFunc("/s/{id}", p.handleStream)
|
||||||
|
r.HandleFunc("/d/{id}", p.handleDownloads)
|
||||||
r.HandleFunc("/{id}", p.handleShares)
|
r.HandleFunc("/{id}", p.handleShares)
|
||||||
r.HandleFunc("/", p.handleShares)
|
r.HandleFunc("/", p.handleShares)
|
||||||
r.Handle("/*", p.assetsHandler)
|
r.Handle("/*", p.assetsHandler)
|
Loading…
Add table
Add a link
Reference in a new issue