From 151092661c5c627ac1d728f2abf4d3f2a6a595c6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 12:29:29 +0000 Subject: [PATCH] Adding configuration to enable DLNA server, (disabled by default), subbing in navidrome log over log --- cmd/root.go | 9 ++++++- conf/configuration.go | 7 +++++ dlna/contenddirectoryservice.go | 40 +++++++++++++--------------- dlna/dlnaserver.go | 47 ++++++++++++++++++--------------- 4 files changed, 60 insertions(+), 43 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 144ca4d5f..4261a7a0f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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")) diff --git a/conf/configuration.go b/conf/configuration.go index e2aca6c13..6b403a884 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -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) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index ee2ae863e..33e13b26c 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -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 diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 5c271e359..b985c2c2a 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -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 -} \ No newline at end of file +}