mirror of
https://github.com/navidrome/navidrome.git
synced 2026-06-02 07:01:36 +00:00
141 lines
4.0 KiB
Go
141 lines
4.0 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
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(ctx).Get(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
streamReq := api.transcodeDecision.ResolveRequest(ctx, mf, format, maxBitRate, timeOffset)
|
|
s, err := api.streamer.NewStream(ctx, mf, streamReq)
|
|
if err != nil {
|
|
if errors.Is(err, stream.ErrTranscodingBusy) {
|
|
return nil, newError(responses.ErrorGeneric, "too many concurrent transcodes, try again later")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Make sure the stream will be closed at the end, to avoid leakage
|
|
defer func() {
|
|
if err := s.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
|
log.Error("Error closing stream", "id", id, "file", s.Name(), err)
|
|
}
|
|
}()
|
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(s.Duration()), 'G', -1, 32))
|
|
|
|
_, err = s.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"
|
|
}
|
|
}
|
|
|
|
setHeaders := func(name string) {
|
|
name = strings.ReplaceAll(name, ",", "_")
|
|
disposition := fmt.Sprintf("attachment; filename=\"%s.zip\"", name)
|
|
w.Header().Set("Content-Disposition", disposition)
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
}
|
|
|
|
switch v := entity.(type) {
|
|
case *model.MediaFile:
|
|
streamReq := api.transcodeDecision.ResolveRequest(ctx, v, format, maxBitRate, 0)
|
|
s, err := api.streamer.NewStream(ctx, v, streamReq)
|
|
if err != nil {
|
|
if errors.Is(err, stream.ErrTranscodingBusy) {
|
|
return nil, newError(responses.ErrorGeneric, "too many concurrent transcodes, try again later")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Make sure the stream will be closed at the end, to avoid leakage
|
|
defer func() {
|
|
if err := s.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
|
log.Error("Error closing stream", "id", id, "file", s.Name(), err)
|
|
}
|
|
}()
|
|
|
|
disposition := fmt.Sprintf("attachment; filename=\"%s\"", s.Name())
|
|
w.Header().Set("Content-Disposition", disposition)
|
|
|
|
_, err = s.Serve(ctx, w, r)
|
|
return nil, err
|
|
case *model.Album:
|
|
setHeaders(v.Name)
|
|
return nil, api.archiver.ZipAlbum(ctx, id, format, maxBitRate, w)
|
|
case *model.Artist:
|
|
setHeaders(v.Name)
|
|
return nil, api.archiver.ZipArtist(ctx, id, format, maxBitRate, w)
|
|
case *model.Playlist:
|
|
setHeaders(v.Name)
|
|
return nil, api.archiver.ZipPlaylist(ctx, id, format, maxBitRate, w)
|
|
default:
|
|
return nil, model.ErrNotFound
|
|
}
|
|
}
|