mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-14 22:28:41 +01:00
148 lines
4.6 KiB
Go
148 lines
4.6 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core/stream"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
|
"github.com/navidrome/navidrome/utils/req"
|
|
"github.com/navidrome/navidrome/utils/str"
|
|
)
|
|
|
|
func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
|
ctx := r.Context()
|
|
p := req.Params(r)
|
|
id, err := p.String("id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
maxBitRate := p.IntOr("maxBitRate", 0)
|
|
format, _ := p.String("format")
|
|
timeOffset := p.IntOr("timeOffset", 0)
|
|
|
|
mf, err := api.ds.MediaFile().Get(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
streamReq := api.transcodeDecision.ResolveRequest(ctx, mf, format, maxBitRate, timeOffset)
|
|
stream, err := api.streamer.NewStream(ctx, mf, streamReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Make sure the stream will be closed at the end, to avoid leakage
|
|
defer func() {
|
|
if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
|
log.Error("Error closing stream", "id", id, "file", stream.Name(), err)
|
|
}
|
|
}()
|
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32))
|
|
|
|
_, err = stream.Serve(ctx, w, r)
|
|
return nil, err
|
|
}
|
|
|
|
func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
|
ctx := r.Context()
|
|
username, _ := request.UsernameFrom(ctx)
|
|
p := req.Params(r)
|
|
id, err := p.String("id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !conf.Server().EnableDownloads {
|
|
log.Warn(ctx, "Downloads are disabled", "user", username, "id", id)
|
|
return nil, newError(responses.ErrorAuthorizationFail, "downloads are disabled")
|
|
}
|
|
|
|
entity, err := model.GetEntityByID(ctx, api.ds, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
maxBitRate := p.IntOr("bitrate", 0)
|
|
format, _ := p.String("format")
|
|
|
|
if format == "" {
|
|
if conf.Server().AutoTranscodeDownload {
|
|
// if we are not provided a format, see if we have requested transcoding for this client
|
|
// This must be enabled via a config option. For the UI, we are always given an option.
|
|
// This will impact other clients which do not use the UI
|
|
transcoding, ok := request.TranscodingFrom(ctx)
|
|
|
|
if !ok {
|
|
format = "raw"
|
|
} else {
|
|
format = transcoding.TargetFormat
|
|
maxBitRate = transcoding.DefaultBitRate
|
|
}
|
|
} else {
|
|
format = "raw"
|
|
}
|
|
} else {
|
|
// An explicit download format must win over the player's forced transcoding
|
|
ctx = request.WithTranscoding(ctx, model.Transcoding{})
|
|
}
|
|
|
|
setHeaders := func(name string) {
|
|
w.Header().Set("Content-Disposition", str.ContentDispositionAttachment(name+".zip"))
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
}
|
|
|
|
switch v := entity.(type) {
|
|
case *model.MediaFile:
|
|
streamReq := api.transcodeDecision.ResolveRequest(ctx, v, format, maxBitRate, 0)
|
|
stream, err := api.streamer.NewStream(ctx, v, streamReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Make sure the stream will be closed at the end, to avoid leakage
|
|
defer func() {
|
|
if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
|
log.Error("Error closing stream", "id", id, "file", stream.Name(), err)
|
|
}
|
|
}()
|
|
|
|
w.Header().Set("Content-Disposition", str.ContentDispositionAttachment(stream.Name()))
|
|
|
|
_, err = stream.Serve(ctx, w, r)
|
|
return nil, err
|
|
case *model.Album:
|
|
setHeaders(v.Name)
|
|
return nil, handleArchiveErr(ctx, id, api.archiver.ZipAlbum(ctx, id, format, maxBitRate, w))
|
|
case *model.Artist:
|
|
setHeaders(v.Name)
|
|
return nil, handleArchiveErr(ctx, id, api.archiver.ZipArtist(ctx, id, format, maxBitRate, w))
|
|
case *model.Playlist:
|
|
setHeaders(v.Name)
|
|
return nil, handleArchiveErr(ctx, id, api.archiver.ZipPlaylist(ctx, id, format, maxBitRate, w))
|
|
default:
|
|
return nil, model.ErrNotFound
|
|
}
|
|
}
|
|
|
|
// handleArchiveErr swallows ErrTooManyTranscodes from archive downloads so the
|
|
// outer error handler does not try to write a 429 onto a response whose status
|
|
// and Content-Disposition have already been flushed. The archive ends up with
|
|
// the tracks that were written before the rejection (the rejected track and
|
|
// any following ones are omitted); the server-side log is the unambiguous
|
|
// signal operators can act on.
|
|
func handleArchiveErr(ctx context.Context, id string, err error) error {
|
|
if errors.Is(err, stream.ErrTooManyTranscodes) {
|
|
log.Warn(ctx, "Archive download finalized early: transcode cap reached", "id", id, err)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|