mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
Adding configuration to enable DLNA server, (disabled by default),
subbing in navidrome log over log
This commit is contained in:
parent
a424a00718
commit
151092661c
@ -83,7 +83,10 @@ func runNavidrome(ctx context.Context) {
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.Go(startServer(ctx))
|
||||
g.Go(startDLNAServer(ctx))
|
||||
if conf.Server.DLNAServer.Enabled {
|
||||
g.Go(startDLNAServer(ctx))
|
||||
|
||||
}
|
||||
g.Go(startSignaller(ctx))
|
||||
g.Go(startScheduler(ctx))
|
||||
g.Go(startPlaybackServer(ctx))
|
||||
@ -395,6 +398,8 @@ func init() {
|
||||
rootCmd.Flags().Bool("prometheus.enabled", viper.GetBool("prometheus.enabled"), "enable/disable prometheus metrics endpoint`")
|
||||
rootCmd.Flags().String("prometheus.metricspath", viper.GetString("prometheus.metricspath"), "http endpoint for prometheus metrics")
|
||||
|
||||
rootCmd.Flags().Bool("dlnaserver.enabled", viper.GetBool("dlnaserver.enabled"), "enable/disable DLNA server")
|
||||
|
||||
_ = viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
|
||||
_ = viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
|
||||
_ = viper.BindPFlag("tlscert", rootCmd.Flags().Lookup("tlscert"))
|
||||
@ -409,6 +414,8 @@ func init() {
|
||||
_ = viper.BindPFlag("prometheus.enabled", rootCmd.Flags().Lookup("prometheus.enabled"))
|
||||
_ = viper.BindPFlag("prometheus.metricspath", rootCmd.Flags().Lookup("prometheus.metricspath"))
|
||||
|
||||
_ = viper.BindPFlag("dlnaserver.enabled", rootCmd.Flags().Lookup("dlnaserver.enabled"))
|
||||
|
||||
_ = viper.BindPFlag("enabletranscodingconfig", rootCmd.Flags().Lookup("enabletranscodingconfig"))
|
||||
_ = viper.BindPFlag("enabletranscodingcancellation", rootCmd.Flags().Lookup("enabletranscodingcancellation"))
|
||||
_ = viper.BindPFlag("transcodingcachesize", rootCmd.Flags().Lookup("transcodingcachesize"))
|
||||
|
||||
@ -107,6 +107,7 @@ type configOptions struct {
|
||||
EnableScrobbleHistory bool
|
||||
Tags map[string]TagConf `json:",omitempty"`
|
||||
Agents string
|
||||
DLNAServer dlnaServerOptions
|
||||
|
||||
// DevFlags. These are used to enable/disable debugging and incomplete features
|
||||
DevLogLevels map[string]string `json:",omitempty"`
|
||||
@ -251,6 +252,10 @@ type extAuthOptions struct {
|
||||
UserHeader string
|
||||
}
|
||||
|
||||
type dlnaServerOptions struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
var (
|
||||
Server = &configOptions{}
|
||||
hooks []func()
|
||||
@ -672,6 +677,8 @@ func setViperDefaults() {
|
||||
viper.SetDefault("plugins.cachesize", "200MB")
|
||||
viper.SetDefault("plugins.autoreload", false)
|
||||
|
||||
viper.SetDefault("dlnaserver.enabled", false)
|
||||
|
||||
// DevFlags. These are used to enable/disable debugging and incomplete features
|
||||
viper.SetDefault("devlogsourceline", false)
|
||||
viper.SetDefault("devenableprofiler", false)
|
||||
|
||||
@ -33,17 +33,17 @@ func (cds *contentDirectoryService) updateIDString() string {
|
||||
// returned if the entry is not of interest.
|
||||
func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, isContainer bool, host string) (ret interface{}, err error) {
|
||||
obj := upnpav.Object{
|
||||
ID: cdsObject.ID(),
|
||||
ID: cdsObject.ID(),
|
||||
Restricted: 1,
|
||||
ParentID: cdsObject.ParentID(),
|
||||
Title: filepath.Base(cdsObject.Path),
|
||||
Title: filepath.Base(cdsObject.Path),
|
||||
}
|
||||
|
||||
if isContainer {
|
||||
defaultChildCount := 1
|
||||
obj.Class = "object.container.storageFolder"
|
||||
return upnpav.Container{
|
||||
Object: obj,
|
||||
Object: obj,
|
||||
ChildCount: &defaultChildCount,
|
||||
}, nil
|
||||
}
|
||||
@ -56,7 +56,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is
|
||||
|
||||
item := upnpav.Item{
|
||||
Object: obj,
|
||||
Res: make([]upnpav.Resource, 0, 1),
|
||||
Res: make([]upnpav.Resource, 0, 1),
|
||||
}
|
||||
|
||||
item.Res = append(item.Res, upnpav.Resource{
|
||||
@ -108,21 +108,21 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
|
||||
ret = append(ret, convObj)
|
||||
}
|
||||
case "/Music/Artists":
|
||||
indexes,err := cds.ds.Artist(cds.ctx).GetIndex()
|
||||
if err!= nil {
|
||||
indexes, err := cds.ds.Artist(cds.ctx).GetIndex()
|
||||
if err != nil {
|
||||
fmt.Printf("Error retrieving Indexes: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
for indexItem := range indexes {
|
||||
child := object{
|
||||
path.Join(o.Path, indexes[indexItem].Artists[0].Name), //TODO handle multiple artists here, fold it into some sort of unique list
|
||||
path.Join(o.Path, indexes[indexItem].Artists[0].Name), //TODO handle multiple artists here, fold it into some sort of unique list
|
||||
}
|
||||
convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host)
|
||||
ret = append(ret, convObj)
|
||||
}
|
||||
case "/Music/Albums":
|
||||
indexes,err := cds.ds.Album(cds.ctx).GetAllWithoutGenres()
|
||||
if err!= nil {
|
||||
indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres()
|
||||
if err != nil {
|
||||
fmt.Printf("Error retrieving Indexes: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
@ -134,7 +134,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
|
||||
ret = append(ret, convObj)
|
||||
}
|
||||
case "/Music/Genres":
|
||||
indexes,err := cds.ds.Genre(cds.ctx).GetAll()
|
||||
indexes, err := cds.ds.Genre(cds.ctx).GetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("Error retrieving Indexes: %+v", err)
|
||||
return nil, err
|
||||
@ -147,7 +147,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
|
||||
ret = append(ret, convObj)
|
||||
}
|
||||
case "/Music/Playlists":
|
||||
indexes,err := cds.ds.Playlist(cds.ctx).GetAll()
|
||||
indexes, err := cds.ds.Playlist(cds.ctx).GetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("Error retrieving Indexes: %+v", err)
|
||||
return nil, err
|
||||
@ -161,10 +161,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if strings.HasPrefix(o.Path, "/Music/Files/") {
|
||||
libraryPath,_ := strings.CutPrefix(o.Path, "/Music/Files")
|
||||
libraryPath, _ := strings.CutPrefix(o.Path, "/Music/Files")
|
||||
log.Printf("library path: %s", libraryPath)
|
||||
files, _ := os.ReadDir(path.Join(conf.Server.MusicFolder, libraryPath))
|
||||
for _, file := range files {
|
||||
@ -179,9 +177,9 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
|
||||
}
|
||||
|
||||
type browse struct {
|
||||
ObjectID string
|
||||
BrowseFlag string
|
||||
Filter string
|
||||
ObjectID string
|
||||
BrowseFlag string
|
||||
Filter string
|
||||
StartingIndex int
|
||||
RequestedCount int
|
||||
}
|
||||
@ -223,13 +221,13 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt
|
||||
}
|
||||
obj, err := cds.objectFromID(browse.ObjectID)
|
||||
if err != nil {
|
||||
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
|
||||
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, "%s", err.Error())
|
||||
}
|
||||
switch browse.BrowseFlag {
|
||||
case "BrowseDirectChildren":
|
||||
objs, err := cds.readContainer(obj, host)
|
||||
if err != nil {
|
||||
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
|
||||
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, "%s", err.Error())
|
||||
}
|
||||
totalMatches := len(objs)
|
||||
objs = objs[func() (low int) {
|
||||
@ -249,8 +247,8 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt
|
||||
return map[string]string{
|
||||
"TotalMatches": fmt.Sprint(totalMatches),
|
||||
"NumberReturned": fmt.Sprint(len(objs)),
|
||||
"Result": didlLite(string(result)),
|
||||
"UpdateID": cds.updateIDString(),
|
||||
"Result": didlLite(string(result)),
|
||||
"UpdateID": cds.updateIDString(),
|
||||
}, nil
|
||||
case "BrowseMetadata":
|
||||
//TODO
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -24,6 +23,7 @@ import (
|
||||
"github.com/anacrolix/dms/ssdp"
|
||||
"github.com/anacrolix/dms/upnp"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/events"
|
||||
)
|
||||
@ -34,6 +34,7 @@ const (
|
||||
resPath = "/r/"
|
||||
serviceControlURL = "/ctl"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticContent embed.FS
|
||||
|
||||
@ -41,7 +42,7 @@ type DLNAServer struct {
|
||||
ds model.DataStore
|
||||
broker events.Broker
|
||||
ssdp SSDPServer
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type SSDPServer struct {
|
||||
@ -57,7 +58,7 @@ type SSDPServer struct {
|
||||
RootDeviceUUID string
|
||||
|
||||
FriendlyName string
|
||||
ModelNumber string
|
||||
ModelNumber string
|
||||
|
||||
// For waiting on the listener to close
|
||||
waitChan chan struct{}
|
||||
@ -74,7 +75,7 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer {
|
||||
AnnounceInterval: time.Duration(30) * time.Second,
|
||||
Interfaces: listInterfaces(),
|
||||
FriendlyName: "Navidrome",
|
||||
ModelNumber: "0.0.1", //TODO
|
||||
ModelNumber: "0.0.1", //TODO
|
||||
RootDeviceUUID: makeDeviceUUID("Navidrome"),
|
||||
waitChan: make(chan struct{}),
|
||||
},
|
||||
@ -95,7 +96,7 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer {
|
||||
//setup dedicated HTTP server for UPNP
|
||||
r := http.NewServeMux()
|
||||
r.Handle(resPath, http.StripPrefix(resPath, http.HandlerFunc(s.ssdp.resourceHandler)))
|
||||
|
||||
|
||||
r.Handle("/static/", http.FileServer(http.FS(staticContent)))
|
||||
r.HandleFunc(rootDescPath, s.ssdp.rootDescHandler)
|
||||
r.HandleFunc(serviceControlURL, s.ssdp.serviceControlHandler)
|
||||
@ -107,6 +108,8 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer {
|
||||
|
||||
// Run starts the DLNA server (both SSDP and HTTP) with the given address
|
||||
func (s *DLNAServer) Run(ctx context.Context, addr string, port int) (err error) {
|
||||
log.Warn("Starting DLNA Server")
|
||||
|
||||
s.ctx = ctx
|
||||
if s.ssdp.HTTPConn == nil {
|
||||
network := "tcp4"
|
||||
@ -194,7 +197,7 @@ func (s *SSDPServer) ssdpInterface(intf net.Interface) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Started SSDP on %v", intf.Name)
|
||||
log.Info(fmt.Sprintf("Started SSDP on %v", intf.Name))
|
||||
|
||||
// Note that the devices and services advertised here via SSDP should be
|
||||
// in agreement with the rootDesc XML descriptor that is defined above.
|
||||
@ -227,16 +230,18 @@ func (s *SSDPServer) ssdpInterface(intf net.Interface) {
|
||||
// good.
|
||||
return
|
||||
}
|
||||
log.Printf("Error creating ssdp server on %s: %s", intf.Name, err)
|
||||
log.Error(fmt.Sprintf("Error creating ssdp server on %s: %s", intf.Name), err)
|
||||
return
|
||||
}
|
||||
defer ssdpServer.Close()
|
||||
log.Printf("Started SSDP on %v", intf.Name)
|
||||
|
||||
log.Info(fmt.Sprintf("Started SSDP on %v", intf.Name))
|
||||
stopped := make(chan struct{})
|
||||
go func() {
|
||||
defer close(stopped)
|
||||
if err := ssdpServer.Serve(); err != nil {
|
||||
log.Printf("%q: %q\n", intf.Name, err)
|
||||
log.Error(fmt.Sprintf("Err %q", intf.Name), err)
|
||||
|
||||
}
|
||||
}()
|
||||
select {
|
||||
@ -250,7 +255,6 @@ func (s *SSDPServer) ssdpInterface(intf net.Interface) {
|
||||
func listInterfaces() []net.Interface {
|
||||
ifs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
log.Println("list network interfaces: %v", err)
|
||||
return []net.Interface{}
|
||||
}
|
||||
|
||||
@ -266,18 +270,18 @@ func isAppropriatelyConfigured(intf net.Interface) bool {
|
||||
return intf.Flags&net.FlagUp != 0 && intf.Flags&net.FlagMulticast != 0 && intf.MTU > 0
|
||||
}
|
||||
|
||||
//handler for all paths under `/r`
|
||||
// handler for all paths under `/r`
|
||||
func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
remotePath := r.URL.Path
|
||||
|
||||
localFile,_ := strings.CutPrefix(remotePath,"Music/Files/")
|
||||
localFile, _ := strings.CutPrefix(remotePath, "Music/Files/")
|
||||
localFilePath := path.Join(conf.Server.MusicFolder, localFile)
|
||||
|
||||
log.Printf("resource handler Executed with remote path: %s, localpath: %s", remotePath, localFilePath)
|
||||
|
||||
fileStats,err := os.Stat(localFilePath)
|
||||
log.Info(fmt.Sprintf("resource handler Executed with remote path: %s, localpath: %s", remotePath, localFilePath))
|
||||
|
||||
fileStats, err := os.Stat(localFilePath)
|
||||
if err != nil {
|
||||
http.NotFound(w,r)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(fileStats.Size(), 10))
|
||||
@ -291,8 +295,8 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("transferMode.dlna.org", "Streaming")
|
||||
|
||||
os.Open(localFilePath)
|
||||
fileHandle,err := os.Open(localFilePath)
|
||||
if err != nil {
|
||||
fileHandle, err := os.Open(localFilePath)
|
||||
if err != nil {
|
||||
fmt.Printf("file streaming error: %+v\n", err)
|
||||
return
|
||||
}
|
||||
@ -382,7 +386,7 @@ func didlLite(chardata string) string {
|
||||
func mustMarshalXML(value interface{}) []byte {
|
||||
ret, err := xml.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
log.Panicf("mustMarshalXML failed to marshal %v: %s", value, err)
|
||||
log.Fatal(fmt.Sprintf("mustMarshalXML failed to marshal %v: %s $s", value, err))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -403,7 +407,7 @@ func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte {
|
||||
func makeDeviceUUID(unique string) string {
|
||||
h := md5.New()
|
||||
if _, err := io.WriteString(h, unique); err != nil {
|
||||
log.Panicf("makeDeviceUUID write failed: %s", err)
|
||||
log.Fatal(fmt.Sprintf("makeDeviceUUID write failed: %s", err))
|
||||
}
|
||||
buf := h.Sum(nil)
|
||||
return upnp.FormatUUID(buf)
|
||||
@ -420,6 +424,7 @@ func withHeader(name string, value string, next http.Handler) http.Handler {
|
||||
// serveError returns an http.StatusInternalServerError and logs the error
|
||||
func serveError(what interface{}, w http.ResponseWriter, text string, err error) {
|
||||
http.Error(w, text+".", http.StatusInternalServerError)
|
||||
log.Error(fmt.Sprintf("serveError: %s, %s, %s", what, text), err)
|
||||
}
|
||||
|
||||
func GetTemplate() (tpl *template.Template, err error) {
|
||||
@ -483,4 +488,4 @@ func GetTemplate() (tpl *template.Template, err error) {
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user