Adding configuration to enable DLNA server, (disabled by default),

subbing in navidrome log over log
This commit is contained in:
Rob Emery 2025-01-11 12:29:29 +00:00
parent a424a00718
commit 151092661c
4 changed files with 60 additions and 43 deletions

View File

@ -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"))

View File

@ -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)

View File

@ -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

View File

@ -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
}
}