mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
feat: experimental downsampling support
This commit is contained in:
parent
41fd5862b8
commit
8372dee000
12 changed files with 318 additions and 168 deletions
|
@ -22,10 +22,11 @@ type nd struct {
|
|||
IgnoredArticles string `default:"The El La Los Las Le Les Os As O A"`
|
||||
IndexGroups string `default:"A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)"`
|
||||
|
||||
DisableDownsampling bool `default:"false"`
|
||||
DownsampleCommand string `default:"ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -"`
|
||||
ProbeCommand string `default:"ffmpeg %s -f ffmetadata"`
|
||||
ScanInterval string `default:"1m"`
|
||||
EnableDownsampling bool `default:"false"`
|
||||
MaxBitRate int `default:"0"`
|
||||
DownsampleCommand string `default:"ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -"`
|
||||
ProbeCommand string `default:"ffmpeg %s -f ffmetadata"`
|
||||
ScanInterval string `default:"1m"`
|
||||
|
||||
// DevFlags. These are used to enable/disable debugging and incomplete features
|
||||
DevDisableBanner bool `default:"false"`
|
||||
|
|
|
@ -4,11 +4,13 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestEngine(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
log.SetLevel(log.LevelCritical)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Engine Suite")
|
||||
|
|
205
engine/media_streamer.go
Normal file
205
engine/media_streamer.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/navidrome/conf"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/utils"
|
||||
)
|
||||
|
||||
type MediaStreamer interface {
|
||||
NewStream(ctx context.Context, id string, maxBitRate int, format string) (mediaStream, error)
|
||||
}
|
||||
|
||||
func NewMediaStreamer(ds model.DataStore) MediaStreamer {
|
||||
return &mediaStreamer{ds: ds}
|
||||
}
|
||||
|
||||
type mediaStream interface {
|
||||
io.ReadSeeker
|
||||
ContentType() string
|
||||
Name() string
|
||||
ModTime() time.Time
|
||||
Close() error
|
||||
}
|
||||
|
||||
type mediaStreamer struct {
|
||||
ds model.DataStore
|
||||
}
|
||||
|
||||
func (ms *mediaStreamer) NewStream(ctx context.Context, id string, maxBitRate int, format string) (mediaStream, error) {
|
||||
mf, err := ms.ds.MediaFile(ctx).Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bitRate int
|
||||
|
||||
if format == "raw" || !conf.Server.EnableDownsampling {
|
||||
bitRate = mf.BitRate
|
||||
format = mf.Suffix
|
||||
} else {
|
||||
if maxBitRate == 0 {
|
||||
bitRate = mf.BitRate
|
||||
} else {
|
||||
bitRate = utils.MinInt(mf.BitRate, maxBitRate)
|
||||
}
|
||||
format = mf.Suffix
|
||||
}
|
||||
if conf.Server.MaxBitRate != 0 {
|
||||
bitRate = utils.MinInt(bitRate, conf.Server.MaxBitRate)
|
||||
}
|
||||
|
||||
var stream mediaStream
|
||||
|
||||
if bitRate == mf.BitRate && mime.TypeByExtension("."+format) == mf.ContentType() {
|
||||
log.Debug(ctx, "Streaming raw file", "id", mf.ID, "path", mf.Path,
|
||||
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
||||
|
||||
f, err := os.Open(mf.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream = &rawMediaStream{ctx: ctx, mf: mf, file: f}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
log.Debug(ctx, "Streaming transcoded file", "id", mf.ID, "path", mf.Path,
|
||||
"requestBitrate", bitRate, "requestFormat", format,
|
||||
"originalBitrate", mf.BitRate, "originalFormat", mf.Suffix)
|
||||
|
||||
f := &transcodedMediaStream{ctx: ctx, mf: mf, bitRate: bitRate, format: format}
|
||||
return f, err
|
||||
}
|
||||
|
||||
type rawMediaStream struct {
|
||||
file *os.File
|
||||
ctx context.Context
|
||||
mf *model.MediaFile
|
||||
}
|
||||
|
||||
func (m *rawMediaStream) Read(p []byte) (n int, err error) {
|
||||
return m.file.Read(p)
|
||||
}
|
||||
|
||||
func (m *rawMediaStream) Seek(offset int64, whence int) (int64, error) {
|
||||
return m.file.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (m *rawMediaStream) ContentType() string {
|
||||
return m.mf.ContentType()
|
||||
}
|
||||
|
||||
func (m *rawMediaStream) Name() string {
|
||||
return m.mf.Path
|
||||
}
|
||||
|
||||
func (m *rawMediaStream) ModTime() time.Time {
|
||||
return m.mf.UpdatedAt
|
||||
}
|
||||
|
||||
func (m *rawMediaStream) Close() error {
|
||||
log.Trace(m.ctx, "Closing file", "id", m.mf.ID, "path", m.mf.Path)
|
||||
return m.file.Close()
|
||||
}
|
||||
|
||||
type transcodedMediaStream struct {
|
||||
ctx context.Context
|
||||
mf *model.MediaFile
|
||||
pipe io.ReadCloser
|
||||
bitRate int
|
||||
format string
|
||||
skip int64
|
||||
}
|
||||
|
||||
func (m *transcodedMediaStream) Read(p []byte) (n int, err error) {
|
||||
if m.pipe == nil {
|
||||
m.pipe, err = newTranscode(m.ctx, m.mf.Path, m.bitRate, m.format)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if m.skip > 0 {
|
||||
_, err := io.CopyN(ioutil.Discard, m.pipe, m.skip)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
n, err = m.pipe.Read(p)
|
||||
if err == io.EOF {
|
||||
m.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// This Seek function assumes internal details of http.ServeContent's implementation
|
||||
// A better approach would be to implement a http.FileSystem and use http.FileServer
|
||||
func (m *transcodedMediaStream) Seek(offset int64, whence int) (int64, error) {
|
||||
if whence == io.SeekEnd {
|
||||
if offset == 0 {
|
||||
size := (m.mf.Duration) * m.bitRate * 1000
|
||||
return int64(size / 8), nil
|
||||
}
|
||||
panic("seeking stream backwards not supported")
|
||||
}
|
||||
m.skip = offset
|
||||
var err error
|
||||
if m.pipe != nil {
|
||||
err = m.Close()
|
||||
}
|
||||
return offset, err
|
||||
}
|
||||
|
||||
func (m *transcodedMediaStream) ContentType() string {
|
||||
return mime.TypeByExtension(".mp3")
|
||||
}
|
||||
|
||||
func (m *transcodedMediaStream) Name() string {
|
||||
return m.mf.Path
|
||||
}
|
||||
|
||||
func (m *transcodedMediaStream) ModTime() time.Time {
|
||||
return m.mf.UpdatedAt
|
||||
}
|
||||
|
||||
func (m *transcodedMediaStream) Close() error {
|
||||
log.Trace(m.ctx, "Closing stream", "id", m.mf.ID, "path", m.mf.Path)
|
||||
err := m.pipe.Close()
|
||||
m.pipe = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func newTranscode(ctx context.Context, path string, maxBitRate int, format string) (f io.ReadCloser, err error) {
|
||||
cmdLine, args := createTranscodeCommand(path, maxBitRate, format)
|
||||
|
||||
log.Trace(ctx, "Executing ffmpeg command", "arg0", cmdLine, "args", args)
|
||||
cmd := exec.Command(cmdLine, args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
if f, err = cmd.StdoutPipe(); err != nil {
|
||||
return f, err
|
||||
}
|
||||
return f, cmd.Start()
|
||||
}
|
||||
|
||||
func createTranscodeCommand(path string, maxBitRate int, format string) (string, []string) {
|
||||
cmd := conf.Server.DownsampleCommand
|
||||
|
||||
split := strings.Split(cmd, " ")
|
||||
for i, s := range split {
|
||||
s = strings.Replace(s, "%s", path, -1)
|
||||
s = strings.Replace(s, "%b", strconv.Itoa(maxBitRate), -1)
|
||||
split[i] = s
|
||||
}
|
||||
|
||||
return split[0], split[1:]
|
||||
}
|
68
engine/media_streamer_test.go
Normal file
68
engine/media_streamer_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/navidrome/conf"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/persistence"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("MediaStreamer", func() {
|
||||
|
||||
var streamer MediaStreamer
|
||||
var ds model.DataStore
|
||||
ctx := log.NewContext(nil)
|
||||
|
||||
BeforeEach(func() {
|
||||
conf.Server.EnableDownsampling = true
|
||||
ds = &persistence.MockDataStore{}
|
||||
ds.MediaFile(ctx).(*persistence.MockMediaFile).SetData(`[{"id": "123", "path": "tests/fixtures/test.mp3", "bitRate": 128}]`, 1)
|
||||
streamer = NewMediaStreamer(ds)
|
||||
})
|
||||
|
||||
Context("NewStream", func() {
|
||||
It("returns a rawMediaStream if format is 'raw'", func() {
|
||||
Expect(streamer.NewStream(ctx, "123", 0, "raw")).To(BeAssignableToTypeOf(&rawMediaStream{}))
|
||||
})
|
||||
It("returns a rawMediaStream if maxBitRate is 0", func() {
|
||||
Expect(streamer.NewStream(ctx, "123", 0, "mp3")).To(BeAssignableToTypeOf(&rawMediaStream{}))
|
||||
})
|
||||
It("returns a rawMediaStream if maxBitRate is higher than file bitRate", func() {
|
||||
Expect(streamer.NewStream(ctx, "123", 256, "mp3")).To(BeAssignableToTypeOf(&rawMediaStream{}))
|
||||
})
|
||||
It("returns a transcodedMediaStream if maxBitRate is lower than file bitRate", func() {
|
||||
s, err := streamer.NewStream(ctx, "123", 64, "mp3")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(s).To(BeAssignableToTypeOf(&transcodedMediaStream{}))
|
||||
Expect(s.(*transcodedMediaStream).bitRate).To(Equal(64))
|
||||
})
|
||||
})
|
||||
|
||||
Context("rawMediaStream", func() {
|
||||
var rawStream mediaStream
|
||||
var modTime time.Time
|
||||
|
||||
BeforeEach(func() {
|
||||
modTime = time.Now()
|
||||
mf := &model.MediaFile{ID: "123", Path: "test.mp3", UpdatedAt: modTime, Suffix: "mp3"}
|
||||
file, err := os.Open("tests/fixtures/test.mp3")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rawStream = &rawMediaStream{mf: mf, file: file, ctx: ctx}
|
||||
})
|
||||
|
||||
It("returns the ContentType", func() {
|
||||
Expect(rawStream.ContentType()).To(Equal("audio/mpeg"))
|
||||
})
|
||||
|
||||
It("returns the ModTime", func() {
|
||||
Expect(rawStream.ModTime()).To(Equal(modTime))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,59 +0,0 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/deluan/navidrome/conf"
|
||||
"github.com/deluan/navidrome/log"
|
||||
)
|
||||
|
||||
// TODO Encapsulate as a io.Reader
|
||||
func Stream(ctx context.Context, path string, bitRate int, maxBitRate int, w io.Writer) error {
|
||||
var f io.Reader
|
||||
var err error
|
||||
enabled := !conf.Server.DisableDownsampling
|
||||
if enabled && maxBitRate > 0 && bitRate > maxBitRate {
|
||||
f, err = downsample(ctx, path, maxBitRate)
|
||||
} else {
|
||||
f, err = os.Open(path)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error opening file", "path", path, err)
|
||||
return err
|
||||
}
|
||||
if _, err = io.Copy(w, f); err != nil {
|
||||
log.Error(ctx, "Error copying file", "path", path, err)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func downsample(ctx context.Context, path string, maxBitRate int) (f io.Reader, err error) {
|
||||
cmdLine, args := createDownsamplingCommand(path, maxBitRate)
|
||||
|
||||
log.Debug(ctx, "Executing command", "cmdLine", cmdLine, "args", args)
|
||||
cmd := exec.Command(cmdLine, args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
if f, err = cmd.StdoutPipe(); err != nil {
|
||||
return f, err
|
||||
}
|
||||
return f, cmd.Start()
|
||||
}
|
||||
|
||||
func createDownsamplingCommand(path string, maxBitRate int) (string, []string) {
|
||||
cmd := conf.Server.DownsampleCommand
|
||||
|
||||
split := strings.Split(cmd, " ")
|
||||
for i, s := range split {
|
||||
s = strings.Replace(s, "%s", path, -1)
|
||||
s = strings.Replace(s, "%b", strconv.Itoa(maxBitRate), -1)
|
||||
split[i] = s
|
||||
}
|
||||
|
||||
return split[0], split[1:]
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/deluan/navidrome/tests"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDownsampling(t *testing.T) {
|
||||
|
||||
Init(t, false)
|
||||
|
||||
Convey("Subject: createDownsamplingCommand", t, func() {
|
||||
|
||||
Convey("It should create a valid command line", func() {
|
||||
cmd, args := createDownsamplingCommand("/music library/file.mp3", 128)
|
||||
|
||||
So(cmd, ShouldEqual, "ffmpeg")
|
||||
So(args[0], ShouldEqual, "-i")
|
||||
So(args[1], ShouldEqual, "/music library/file.mp3")
|
||||
So(args[2], ShouldEqual, "-b:a")
|
||||
So(args[3], ShouldEqual, "128k")
|
||||
So(args[4], ShouldEqual, "mp3")
|
||||
So(args[5], ShouldEqual, "-")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
|
@ -12,4 +12,5 @@ var Set = wire.NewSet(
|
|||
NewSearch,
|
||||
NewNowPlayingRepository,
|
||||
NewUsers,
|
||||
NewMediaStreamer,
|
||||
)
|
||||
|
|
|
@ -25,15 +25,17 @@ type Router struct {
|
|||
Scrobbler engine.Scrobbler
|
||||
Search engine.Search
|
||||
Users engine.Users
|
||||
Streamer engine.MediaStreamer
|
||||
|
||||
mux http.Handler
|
||||
}
|
||||
|
||||
func New(browser engine.Browser, cover engine.Cover, listGenerator engine.ListGenerator, users engine.Users,
|
||||
playlists engine.Playlists, ratings engine.Ratings, scrobbler engine.Scrobbler, search engine.Search) *Router {
|
||||
playlists engine.Playlists, ratings engine.Ratings, scrobbler engine.Scrobbler, search engine.Search,
|
||||
streamer engine.MediaStreamer) *Router {
|
||||
|
||||
r := &Router{Browser: browser, Cover: cover, ListGenerator: listGenerator, Playlists: playlists,
|
||||
Ratings: ratings, Scrobbler: scrobbler, Search: search, Users: users}
|
||||
Ratings: ratings, Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer}
|
||||
r.mux = r.routes()
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -2,92 +2,51 @@ package subsonic
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/deluan/navidrome/engine"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/server/subsonic/responses"
|
||||
"github.com/deluan/navidrome/utils"
|
||||
)
|
||||
|
||||
type StreamController struct {
|
||||
browser engine.Browser
|
||||
streamer engine.MediaStreamer
|
||||
}
|
||||
|
||||
func NewStreamController(browser engine.Browser) *StreamController {
|
||||
return &StreamController{browser: browser}
|
||||
func NewStreamController(streamer engine.MediaStreamer) *StreamController {
|
||||
return &StreamController{streamer: streamer}
|
||||
}
|
||||
|
||||
func (c *StreamController) getMediaFile(r *http.Request) (mf *engine.Entry, err error) {
|
||||
func (c *StreamController) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := RequiredParamString(r, "id", "id parameter required")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxBitRate := ParamInt(r, "maxBitRate", 0)
|
||||
format := ParamString(r, "format")
|
||||
|
||||
ms, err := c.streamer.NewStream(r.Context(), id, maxBitRate, format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Override Content-Type detected by http.FileServer
|
||||
w.Header().Set("Content-Type", ms.ContentType())
|
||||
http.ServeContent(w, r, ms.Name(), ms.ModTime(), ms)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *StreamController) Download(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := RequiredParamString(r, "id", "id parameter required")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mf, err = c.browser.GetSong(r.Context(), id)
|
||||
switch {
|
||||
case err == model.ErrNotFound:
|
||||
log.Error(r, "Mediafile not found", "id", id)
|
||||
return nil, NewError(responses.ErrorDataNotFound)
|
||||
case err != nil:
|
||||
log.Error(r, "Error reading mediafile from DB", "id", id, err)
|
||||
return nil, NewError(responses.ErrorGeneric, "Internal error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Still getting the "Conn.Write wrote more than the declared Content-Length" error.
|
||||
// Don't know if this causes any issues
|
||||
func (c *StreamController) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
mf, err := c.getMediaFile(r)
|
||||
ms, err := c.streamer.NewStream(r.Context(), id, 0, "raw")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxBitRate := ParamInt(r, "maxBitRate", 0)
|
||||
maxBitRate = utils.MinInt(mf.BitRate, maxBitRate)
|
||||
|
||||
log.Debug(r, "Streaming file", "id", mf.Id, "path", mf.AbsolutePath, "bitrate", mf.BitRate, "maxBitRate", maxBitRate)
|
||||
|
||||
// TODO Send proper estimated content-length
|
||||
//contentLength := mf.Size
|
||||
//if maxBitRate > 0 {
|
||||
// contentLength = strconv.Itoa((mf.Duration + 1) * maxBitRate * 1000 / 8)
|
||||
//}
|
||||
h := w.Header()
|
||||
h.Set("Content-Length", strconv.Itoa(mf.Size))
|
||||
h.Set("Content-Type", "audio/mpeg")
|
||||
h.Set("Expires", "0")
|
||||
h.Set("Cache-Control", "must-revalidate")
|
||||
h.Set("Pragma", "public")
|
||||
|
||||
if r.Method == "HEAD" {
|
||||
log.Debug(r, "Just a HEAD. Not streaming", "path", mf.AbsolutePath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err = engine.Stream(r.Context(), mf.AbsolutePath, mf.BitRate, maxBitRate, w)
|
||||
if err != nil {
|
||||
log.Error(r, "Error streaming file", "id", mf.Id, err)
|
||||
}
|
||||
|
||||
log.Debug(r, "Finished streaming", "path", mf.AbsolutePath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *StreamController) Download(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
mf, err := c.getMediaFile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug(r, "Sending file", "path", mf.AbsolutePath)
|
||||
|
||||
err = engine.Stream(r.Context(), mf.AbsolutePath, 0, 0, w)
|
||||
if err != nil {
|
||||
log.Error(r, "Error downloading file", "path", mf.AbsolutePath, err)
|
||||
}
|
||||
|
||||
log.Debug(r, "Finished sending", "path", mf.AbsolutePath)
|
||||
|
||||
// Override Content-Type detected by http.FileServer
|
||||
w.Header().Set("Content-Type", ms.ContentType())
|
||||
http.ServeContent(w, r, ms.Name(), ms.ModTime(), ms)
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -59,8 +59,8 @@ func initMediaRetrievalController(router *Router) *MediaRetrievalController {
|
|||
}
|
||||
|
||||
func initStreamController(router *Router) *StreamController {
|
||||
browser := router.Browser
|
||||
streamController := NewStreamController(browser)
|
||||
mediaStreamer := router.Streamer
|
||||
streamController := NewStreamController(mediaStreamer)
|
||||
return streamController
|
||||
}
|
||||
|
||||
|
@ -75,5 +75,5 @@ var allProviders = wire.NewSet(
|
|||
NewSearchingController,
|
||||
NewUsersController,
|
||||
NewMediaRetrievalController,
|
||||
NewStreamController, wire.FieldsOf(new(*Router), "Browser", "Cover", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search"),
|
||||
NewStreamController, wire.FieldsOf(new(*Router), "Browser", "Cover", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search", "Streamer"),
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@ var allProviders = wire.NewSet(
|
|||
NewUsersController,
|
||||
NewMediaRetrievalController,
|
||||
NewStreamController,
|
||||
wire.FieldsOf(new(*Router), "Browser", "Cover", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search"),
|
||||
wire.FieldsOf(new(*Router), "Browser", "Cover", "ListGenerator", "Playlists", "Ratings", "Scrobbler", "Search", "Streamer"),
|
||||
)
|
||||
|
||||
func initSystemController(router *Router) *SystemController {
|
||||
|
|
|
@ -41,7 +41,8 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
|
|||
ratings := engine.NewRatings(dataStore)
|
||||
scrobbler := engine.NewScrobbler(dataStore, nowPlayingRepository)
|
||||
search := engine.NewSearch(dataStore)
|
||||
router := subsonic.New(browser, cover, listGenerator, users, playlists, ratings, scrobbler, search)
|
||||
mediaStreamer := engine.NewMediaStreamer(dataStore)
|
||||
router := subsonic.New(browser, cover, listGenerator, users, playlists, ratings, scrobbler, search, mediaStreamer)
|
||||
return router
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue