From 14639ee0755c7b40973e2db9382bd28f54314a53 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 12 Oct 2025 18:37:06 +0100 Subject: [PATCH 01/87] BUilds --- Makefile | 7 +++++-- cmd/root.go | 8 ++++++++ cmd/wire_gen.go | 11 ++++++++++- cmd/wire_injectors.go | 8 ++++++++ go.sum | 6 ++++++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 52aa5a8cd..81aef6f10 100644 --- a/Makefile +++ b/Makefile @@ -143,10 +143,13 @@ setup-git: ##@Development Setup Git hooks (pre-commit and pre-push) @(cd .git/hooks && ln -sf ../../git/* .) .PHONY: setup-git -build: check_go_env buildjs ##@Build Build the project - go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)" -tags=netgo +build: check_go_env buildjs buildgo ##@Build Build the project .PHONY: build +buildgo: + go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)" -tags=netgo +.PHONY: buildgo + buildall: deprecated build .PHONY: buildall diff --git a/cmd/root.go b/cmd/root.go index 74a15abc1..5980fea12 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -83,6 +83,7 @@ func runNavidrome(ctx context.Context) { g, ctx := errgroup.WithContext(ctx) g.Go(startServer(ctx)) + g.Go(startDLNAServer(ctx)) g.Go(startSignaller(ctx)) g.Go(startScheduler(ctx)) g.Go(startPlaybackServer(ctx)) @@ -142,6 +143,13 @@ func startServer(ctx context.Context) func() error { } } +func startDLNAServer(ctx context.Context) func() error { + return func() error { + a := CreateDLNAServer() + return a.Run(ctx, conf.Server.Address, conf.Server.Port, conf.Server.TLSCert, conf.Server.TLSKey) + } +} + // schedulePeriodicScan schedules a periodic scan of the music library, if configured. func schedulePeriodicScan(ctx context.Context) func() error { return func() error { diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 7a9d38d92..9f8a72c33 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -8,6 +8,7 @@ package cmd import ( "context" + "github.com/google/wire" "github.com/navidrome/navidrome/adapters/lastfm" "github.com/navidrome/navidrome/adapters/listenbrainz" @@ -20,6 +21,7 @@ import ( "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/db" + "github.com/navidrome/navidrome/dlna" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/plugins" @@ -37,7 +39,6 @@ import ( _ "github.com/navidrome/navidrome/adapters/lastfm" _ "github.com/navidrome/navidrome/adapters/listenbrainz" _ "github.com/navidrome/navidrome/adapters/spotify" - _ "github.com/navidrome/navidrome/adapters/taglib" ) // Injectors from wire_injectors.go: @@ -57,6 +58,14 @@ func CreateServer() *server.Server { return serverServer } +func CreateDLNAServer() *dlna.DLNAServer { + dbDB := db.Db() + dataStore := persistence.New(dbDB) + broker := events.GetBroker() + dlnaServer := dlna.New(dataStore, broker) + return dlnaServer +} + func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router { sqlDB := db.Db() dataStore := persistence.New(sqlDB) diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go index 56206feb6..426e917a9 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -15,6 +15,7 @@ import ( "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/db" + "github.com/navidrome/navidrome/dlna" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/plugins" @@ -30,6 +31,7 @@ var allProviders = wire.NewSet( core.Set, artwork.Set, server.New, + dlna.New, subsonic.New, nativeapi.New, public.New, @@ -62,6 +64,12 @@ func CreateServer() *server.Server { )) } +func CreateDLNAServer() *dlna.DLNAServer { + panic(wire.Build( + allProviders, + )) +} + func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router { panic(wire.Build( allProviders, diff --git a/go.sum b/go.sum index 769e45818..680bdf362 100644 --- a/go.sum +++ b/go.sum @@ -333,6 +333,12 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= From 56bdeef08a3ca947cfc2c3e307a5de123c39b9bb Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 19:49:49 +0000 Subject: [PATCH 02/87] BUilds --- dlna/dlnaserver.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 dlna/dlnaserver.go diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go new file mode 100644 index 000000000..63d14cf24 --- /dev/null +++ b/dlna/dlnaserver.go @@ -0,0 +1,23 @@ +package dlna + +import ( + "context" + + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/server/events" +) + +type DLNAServer struct { + ds model.DataStore + broker events.Broker +} + +func New(ds model.DataStore, broker events.Broker) *DLNAServer { + s := &DLNAServer{ds: ds, broker: broker} + return s +} + +// Run starts the server with the given address, and if specified, with TLS enabled. +func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert string, tlsKey string) error { + return nil +} From bdc146ab74bf8fd829765735f0165409e3e30a3c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 20:11:22 +0000 Subject: [PATCH 03/87] Compiles, ssdp --- dlna/dlnaserver.go | 214 ++++++++++++++++++++++++++++++++++++++++++++- go.mod | 3 + go.sum | 6 ++ 3 files changed, 222 insertions(+), 1 deletion(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 63d14cf24..9e96c65d1 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -2,18 +2,60 @@ package dlna import ( "context" + "encoding/xml" + "fmt" + "log" + "net" + "net/http" + "net/url" + "strings" + "time" + "github.com/anacrolix/dms/soap" + "github.com/anacrolix/dms/ssdp" + "github.com/anacrolix/dms/upnp" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/events" ) +const ( + serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0" + rootDescPath = "/rootDesc.xml" + resPath = "/r/" + serviceControlURL = "/ctl" +) + type DLNAServer struct { ds model.DataStore broker events.Broker + ssdp SSDPServer +} + +type SSDPServer struct { + // The service SOAP handler keyed by service URN. + services map[string]UPnPService + + Interfaces []net.Interface + + HTTPConn net.Listener + httpListenAddr string + handler http.Handler + + RootDeviceUUID string + + FriendlyName string + + // For waiting on the listener to close + waitChan chan struct{} + + // Time interval between SSPD announces + AnnounceInterval time.Duration } func New(ds model.DataStore, broker events.Broker) *DLNAServer { - s := &DLNAServer{ds: ds, broker: broker} + s := &DLNAServer{ds: ds, broker: broker, ssdp: SSDPServer{}} + s.ssdp.Interfaces = listInterfaces() + return s } @@ -21,3 +63,173 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert string, tlsKey string) error { return nil } + +type UPnPService interface { + Handle(action string, argsXML []byte, r *http.Request) (respArgs map[string]string, err error) + Subscribe(callback []*url.URL, timeoutSeconds int) (sid string, actualTimeout int, err error) + Unsubscribe(sid string) error +} + +func (s *SSDPServer) startSSDP() { + active := 0 + stopped := make(chan struct{}) + for _, intf := range s.Interfaces { + active++ + go func(intf2 net.Interface) { + defer func() { + stopped <- struct{}{} + }() + s.ssdpInterface(intf2) + }(intf) + } + for active > 0 { + <-stopped + active-- + } +} + +// Run SSDP server on an interface. +func (s *SSDPServer) ssdpInterface(intf net.Interface) { + // Figure out whether should an ip be announced + ipfilterFn := func(ip net.IP) bool { + listenaddr := s.HTTPConn.Addr().String() + listenip := listenaddr[:strings.LastIndex(listenaddr, ":")] + switch listenip { + case "0.0.0.0": + if strings.Contains(ip.String(), ":") { + // Any IPv6 address should not be announced + // because SSDP only listen on IPv4 multicast address + return false + } + return true + case "[::]": + // In the @Serve() section, the default settings have been made to not listen on IPv6 addresses. + // If actually still listening on [::], then allow to announce any address. + return true + default: + if listenip == ip.String() { + return true + } + return false + } + } + + // Figure out which HTTP location to advertise based on the interface IP. + advertiseLocationFn := func(ip net.IP) string { + url := url.URL{ + Scheme: "http", + Host: (&net.TCPAddr{ + IP: ip, + Port: s.HTTPConn.Addr().(*net.TCPAddr).Port, + }).String(), + Path: rootDescPath, + } + return url.String() + } + + _, err := intf.Addrs() + if err != nil { + panic(err) + } + log.Printf("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. + ssdpServer := ssdp.Server{ + Interface: intf, + Devices: []string{ + "urn:schemas-upnp-org:device:MediaServer:1"}, + Services: []string{ + "urn:schemas-upnp-org:service:ContentDirectory:1", + "urn:schemas-upnp-org:service:ConnectionManager:1", + "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"}, + IPFilter: ipfilterFn, + Location: advertiseLocationFn, + Server: serverField, + UUID: s.RootDeviceUUID, + NotifyInterval: s.AnnounceInterval, + } + + // An interface with these flags should be valid for SSDP. + const ssdpInterfaceFlags = net.FlagUp | net.FlagMulticast + + if err := ssdpServer.Init(); err != nil { + if intf.Flags&ssdpInterfaceFlags != ssdpInterfaceFlags { + // Didn't expect it to work anyway. + return + } + if strings.Contains(err.Error(), "listen") { + // OSX has a lot of dud interfaces. Failure to create a socket on + // the interface are what we're expecting if the interface is no + // good. + return + } + log.Printf("Error creating ssdp server on %s: %s", intf.Name, err) + return + } + defer ssdpServer.Close() + log.Printf("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) + } + }() + select { + case <-s.waitChan: + // Returning will close the server. + case <-stopped: + } +} + +// Get all available active network interfaces. +func listInterfaces() []net.Interface { + ifs, err := net.Interfaces() + if err != nil { + log.Println("list network interfaces: %v", err) + return []net.Interface{} + } + + var active []net.Interface + for _, intf := range ifs { + if isAppropriatelyConfigured(intf) { + active = append(active, intf) + } + } + return active +} +func isAppropriatelyConfigured(intf net.Interface) bool { + return intf.Flags&net.FlagUp != 0 && intf.Flags&net.FlagMulticast != 0 && intf.MTU > 0 +} + +func didlLite(chardata string) string { + return `` + + chardata + + `` +} + +func mustMarshalXML(value interface{}) []byte { + ret, err := xml.MarshalIndent(value, "", " ") + if err != nil { + log.Panicf("mustMarshalXML failed to marshal %v: %s", value, err) + } + return ret +} + +// Marshal SOAP response arguments into a response XML snippet. +func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte { + soapArgs := make([]soap.Arg, 0, len(args)) + for argName, value := range args { + soapArgs = append(soapArgs, soap.Arg{ + XMLName: xml.Name{Local: argName}, + Value: value, + }) + } + return []byte(fmt.Sprintf(`%[3]s`, + sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs))) +} diff --git a/go.mod b/go.mod index da005b7c7..383aaee53 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,9 @@ require ( dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/atombender/go-jsonschema v0.20.0 // indirect + github.com/anacrolix/dms v1.7.1 // indirect + github.com/anacrolix/generics v0.0.1 // indirect + github.com/anacrolix/log v0.15.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/reflex v0.3.1 // indirect diff --git a/go.sum b/go.sum index 680bdf362..47f2211e3 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,12 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY= github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8= +github.com/anacrolix/dms v1.7.1 h1:XVOpT3eoO5Ds34B1X+TE3R2ApfqGGeqotEoCVNP8BaI= +github.com/anacrolix/dms v1.7.1/go.mod h1:excFJW5MKBhn5yt5ZMyeE9iFVqnO6tEGQl7YG/2tUoQ= +github.com/anacrolix/generics v0.0.1 h1:4WVhK6iLb3UAAAQP6I3uYlMOHcp9FqJC9j4n81Wv9Ks= +github.com/anacrolix/generics v0.0.1/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/log v0.15.2 h1:LTSf5Wm6Q4GNWPFMBP7NPYV6UBVZzZLKckL+/Lj72Oo= +github.com/anacrolix/log v0.15.2/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= From 915481bbf79e38f87fa655fc24e709f5b177150d Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 20:20:20 +0000 Subject: [PATCH 04/87] Runs without crashing now --- dlna/dlnaserver.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 9e96c65d1..cf5f94ab6 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -60,7 +60,21 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { } // Run starts the server with the given address, and if specified, with TLS enabled. -func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert string, tlsKey string) error { +func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert string, tlsKey string) (err error) { + + if s.ssdp.HTTPConn == nil { + network := "tcp4" + if strings.Count(s.ssdp.httpListenAddr, ":") > 1 { + network = "tcp" + } + s.ssdp.HTTPConn, err = net.Listen(network, s.ssdp.httpListenAddr) + if err != nil { + return + } + } + go func() { + s.ssdp.startSSDP() + }() return nil } From 4755bdc5a82d85dee1c7173d4827cd6e8652dd8f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 20:33:49 +0000 Subject: [PATCH 05/87] Defaulting Interval --- dlna/dlnaserver.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index cf5f94ab6..ae0f1b3a1 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -55,6 +55,7 @@ type SSDPServer struct { func New(ds model.DataStore, broker events.Broker) *DLNAServer { s := &DLNAServer{ds: ds, broker: broker, ssdp: SSDPServer{}} s.ssdp.Interfaces = listInterfaces() + s.ssdp.AnnounceInterval = time.Duration(30) * time.Second return s } From b3d030249c4224882b6b6329560a96a5fa59c1d8 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:04:40 +0000 Subject: [PATCH 06/87] Raw copies from rclone --- dlna/connectionmanagerservice.go | 28 +++ dlna/contenddirectoryservice.go | 349 ++++++++++++++++++++++++++ dlna/dlnaserver.go | 12 + dlna/mediareceiverregistrarservice.go | 29 +++ dlna/upnpav/upnpav.go | 64 +++++ 5 files changed, 482 insertions(+) create mode 100644 dlna/connectionmanagerservice.go create mode 100644 dlna/contenddirectoryservice.go create mode 100644 dlna/mediareceiverregistrarservice.go create mode 100644 dlna/upnpav/upnpav.go diff --git a/dlna/connectionmanagerservice.go b/dlna/connectionmanagerservice.go new file mode 100644 index 000000000..8f868d3d7 --- /dev/null +++ b/dlna/connectionmanagerservice.go @@ -0,0 +1,28 @@ +//go:build go1.21 + +package dlna + +import ( + "net/http" + + "github.com/anacrolix/dms/upnp" +) + +const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*" + +type connectionManagerService struct { + *DLNAServer + upnp.Eventing +} + +func (cms *connectionManagerService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) { + switch action { + case "GetProtocolInfo": + return map[string]string{ + "Source": defaultProtocolInfo, + "Sink": "", + }, nil + default: + return nil, upnp.InvalidActionError + } +} diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go new file mode 100644 index 000000000..694e7d743 --- /dev/null +++ b/dlna/contenddirectoryservice.go @@ -0,0 +1,349 @@ +//go:build go1.21 + +package dlna + +import ( + "context" + "encoding/xml" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + "strings" + + "github.com/anacrolix/dms/dlna" + "github.com/anacrolix/dms/upnp" +) + +type contentDirectoryService struct { + *DLNAServer + upnp.Eventing +} + +func (cds *contentDirectoryService) updateIDString() string { + return fmt.Sprintf("%d", uint32(os.Getpid())) +} + +var mediaMimeTypeRegexp = regexp.MustCompile("^(video|audio|image)/") + +// Turns the given entry and DMS host into a UPnP object. A nil object is +// returned if the entry is not of interest. +func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo vfs.Node, resources vfs.Nodes, host string) (ret interface{}, err error) { + obj := upnpav.Object{ + ID: cdsObject.ID(), + Restricted: 1, + ParentID: cdsObject.ParentID(), + } + + if fileInfo.IsDir() { + defaultChildCount := 1 + obj.Class = "object.container.storageFolder" + obj.Title = fileInfo.Name() + return upnpav.Container{ + Object: obj, + ChildCount: &defaultChildCount, + }, nil + } + + if !fileInfo.Mode().IsRegular() { + return + } + + // Read the mime type from the fs.Object if possible, + // otherwise fall back to working out what it is from the file path. + var mimeType string + if o, ok := fileInfo.DirEntry().(fs.Object); ok { + mimeType = fs.MimeType(context.TODO(), o) + // If backend doesn't know what the mime type is then + // try getting it from the file name + if mimeType == "application/octet-stream" { + mimeType = fs.MimeTypeFromName(fileInfo.Name()) + } + } else { + mimeType = fs.MimeTypeFromName(fileInfo.Name()) + } + + mediaType := mediaMimeTypeRegexp.FindStringSubmatch(mimeType) + if mediaType == nil { + return + } + + obj.Class = "object.item." + mediaType[1] + "Item" + obj.Title = fileInfo.Name() + obj.Date = upnpav.Timestamp{Time: fileInfo.ModTime()} + + item := upnpav.Item{ + Object: obj, + Res: make([]upnpav.Resource, 0, 1), + } + + item.Res = append(item.Res, upnpav.Resource{ + URL: (&url.URL{ + Scheme: "http", + Host: host, + Path: path.Join(resPath, cdsObject.Path), + }).String(), + ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ + SupportRange: true, + }.String()), + Size: uint64(fileInfo.Size()), + }) + + for _, resource := range resources { + subtitleURL := (&url.URL{ + Scheme: "http", + Host: host, + Path: path.Join(resPath, resource.Path()), + }).String() + item.Res = append(item.Res, upnpav.Resource{ + URL: subtitleURL, + ProtocolInfo: fmt.Sprintf("http-get:*:%s:*", "text/srt"), + }) + } + + ret = item + return +} + +// Returns all the upnpav objects in a directory. +func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { + node, err := cds.vfs.Stat(o.Path) + if err != nil { + return + } + + if !node.IsDir() { + err = errors.New("not a directory") + return + } + + dir := node.(*vfs.Dir) + dirEntries, err := dir.ReadDirAll() + if err != nil { + err = errors.New("failed to list directory") + return + } + + dirEntries, mediaResources := mediaWithResources(dirEntries) + for _, de := range dirEntries { + child := object{ + path.Join(o.Path, de.Name()), + } + obj, err := cds.cdsObjectToUpnpavObject(child, de, mediaResources[de], host) + if err != nil { + fs.Errorf(cds, "error with %s: %s", child.FilePath(), err) + continue + } + if obj == nil { + fs.Debugf(cds, "unrecognized file type: %s", de) + continue + } + ret = append(ret, obj) + } + + return +} + +// Given a list of nodes, separate them into potential media items and any associated resources (external subtitles, +// for example.) +// +// The result is a slice of potential media nodes (in their original order) and a map containing associated +// resources nodes of each media node, if any. +func mediaWithResources(nodes vfs.Nodes) (vfs.Nodes, map[vfs.Node]vfs.Nodes) { + media, mediaResources := vfs.Nodes{}, make(map[vfs.Node]vfs.Nodes) + + // First, separate out the subtitles and media into maps, keyed by their lowercase base names. + mediaByName, subtitlesByName := make(map[string]vfs.Nodes), make(map[string]vfs.Node) + for _, node := range nodes { + baseName, ext := splitExt(strings.ToLower(node.Name())) + switch ext { + case ".srt", ".ass", ".ssa", ".sub", ".idx", ".sup", ".jss", ".txt", ".usf", ".cue", ".vtt", ".css": + // .idx should be with .sub, .css should be with vtt otherwise they should be culled, + // and their mimeTypes are not consistent, but anyway these negatives don't throw errors. + subtitlesByName[baseName] = node + default: + mediaByName[baseName] = append(mediaByName[baseName], node) + media = append(media, node) + } + } + + // Find the associated media file for each subtitle + for baseName, node := range subtitlesByName { + // Find a media file with the same basename (video.mp4 for video.srt) + mediaNodes, found := mediaByName[baseName] + if !found { + // Or basename of the basename (video.mp4 for video.en.srt) + baseName, _ = splitExt(baseName) + mediaNodes, found = mediaByName[baseName] + } + + // Just advise if no match found + if !found { + fs.Infof(node, "could not find associated media for subtitle: %s", node.Name()) + continue + } + + // Associate with all potential media nodes + fs.Debugf(mediaNodes, "associating subtitle: %s", node.Name()) + for _, mediaNode := range mediaNodes { + mediaResources[mediaNode] = append(mediaResources[mediaNode], node) + } + } + + return media, mediaResources +} + +type browse struct { + ObjectID string + BrowseFlag string + Filter string + StartingIndex int + RequestedCount int +} + +// ContentDirectory object from ObjectID. +func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) { + o.Path, err = url.QueryUnescape(id) + if err != nil { + return + } + if o.Path == "0" { + o.Path = "/" + } + o.Path = path.Clean(o.Path) + if !path.IsAbs(o.Path) { + err = fmt.Errorf("bad ObjectID %v", o.Path) + return + } + return +} + +func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) { + host := r.Host + + switch action { + case "GetSystemUpdateID": + return map[string]string{ + "Id": cds.updateIDString(), + }, nil + case "GetSortCapabilities": + return map[string]string{ + "SortCaps": "dc:title", + }, nil + case "Browse": + var browse browse + if err := xml.Unmarshal(argsXML, &browse); err != nil { + return nil, err + } + obj, err := cds.objectFromID(browse.ObjectID) + if err != nil { + return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error()) + } + switch browse.BrowseFlag { + case "BrowseDirectChildren": + objs, err := cds.readContainer(obj, host) + if err != nil { + return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error()) + } + totalMatches := len(objs) + objs = objs[func() (low int) { + low = browse.StartingIndex + if low > len(objs) { + low = len(objs) + } + return + }():] + if browse.RequestedCount != 0 && browse.RequestedCount < len(objs) { + objs = objs[:browse.RequestedCount] + } + result, err := xml.Marshal(objs) + if err != nil { + return nil, err + } + return map[string]string{ + "TotalMatches": fmt.Sprint(totalMatches), + "NumberReturned": fmt.Sprint(len(objs)), + "Result": didlLite(string(result)), + "UpdateID": cds.updateIDString(), + }, nil + case "BrowseMetadata": + node, err := cds.vfs.Stat(obj.Path) + if err != nil { + return nil, err + } + // TODO: External subtitles won't appear in the metadata here, but probably should. + upnpObject, err := cds.cdsObjectToUpnpavObject(obj, node, vfs.Nodes{}, host) + if err != nil { + return nil, err + } + result, err := xml.Marshal(upnpObject) + if err != nil { + return nil, err + } + return map[string]string{ + "Result": didlLite(string(result)), + }, nil + default: + return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "unhandled browse flag: %v", browse.BrowseFlag) + } + case "GetSearchCapabilities": + return map[string]string{ + "SearchCaps": "", + }, nil + // Samsung Extensions + case "X_GetFeatureList": + return map[string]string{ + "FeatureList": ` + + + + + +`}, nil + case "X_SetBookmark": + // just ignore + return map[string]string{}, nil + default: + return nil, upnp.InvalidActionError + } +} + +// Represents a ContentDirectory object. +type object struct { + Path string // The cleaned, absolute path for the object relative to the server. +} + +// Returns the actual local filesystem path for the object. +func (o *object) FilePath() string { + return filepath.FromSlash(o.Path) +} + +// Returns the ObjectID for the object. This is used in various ContentDirectory actions. +func (o object) ID() string { + if !path.IsAbs(o.Path) { + log.Panicf("Relative object path: %s", o.Path) + } + if len(o.Path) == 1 { + return "0" + } + return url.QueryEscape(o.Path) +} + +func (o *object) IsRoot() bool { + return o.Path == "/" +} + +// Returns the object's parent ObjectID. Fortunately it can be deduced from the +// ObjectID (for now). +func (o object) ParentID() string { + if o.IsRoot() { + return "-1" + } + o.Path = path.Dir(o.Path) + return o.ID() +} diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index ae0f1b3a1..eda01c832 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -57,6 +57,18 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { s.ssdp.Interfaces = listInterfaces() s.ssdp.AnnounceInterval = time.Duration(30) * time.Second + s.ssdp.services = map[string]UPnPService { + "ContentDirectory": &contentDirectoryService{ + server: s, + }, + "ConnectionManager": &connectionManagerService{ + server: s, + }, + "X_MS_MediaReceiverRegistrar": &mediaReceiverRegistrarService{ + server: s, + }, + } + return s } diff --git a/dlna/mediareceiverregistrarservice.go b/dlna/mediareceiverregistrarservice.go new file mode 100644 index 000000000..0be03bdb2 --- /dev/null +++ b/dlna/mediareceiverregistrarservice.go @@ -0,0 +1,29 @@ +//go:build go1.21 + +package dlna + +import ( + "net/http" + + "github.com/anacrolix/dms/upnp" +) + +type mediaReceiverRegistrarService struct { + *DLNAServer + upnp.Eventing +} + +func (mrrs *mediaReceiverRegistrarService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) { + switch action { + case "IsAuthorized", "IsValidated": + return map[string]string{ + "Result": "1", + }, nil + case "RegisterDevice": + return map[string]string{ + "RegistrationRespMsg": mrrs.RootDeviceUUID, + }, nil + default: + return nil, upnp.InvalidActionError + } +} diff --git a/dlna/upnpav/upnpav.go b/dlna/upnpav/upnpav.go new file mode 100644 index 000000000..c6dc9dc4f --- /dev/null +++ b/dlna/upnpav/upnpav.go @@ -0,0 +1,64 @@ +// Package upnpav provides utilities for DLNA server. +package upnpav + +import ( + "encoding/xml" + "time" +) + +const ( + // NoSuchObjectErrorCode : The specified ObjectID is invalid. + NoSuchObjectErrorCode = 701 +) + +// Resource description +type Resource struct { + XMLName xml.Name `xml:"res"` + ProtocolInfo string `xml:"protocolInfo,attr"` + URL string `xml:",chardata"` + Size uint64 `xml:"size,attr,omitempty"` + Bitrate uint `xml:"bitrate,attr,omitempty"` + Duration string `xml:"duration,attr,omitempty"` + Resolution string `xml:"resolution,attr,omitempty"` +} + +// Container description +type Container struct { + Object + XMLName xml.Name `xml:"container"` + ChildCount *int `xml:"childCount,attr"` +} + +// Item description +type Item struct { + Object + XMLName xml.Name `xml:"item"` + Res []Resource + InnerXML string `xml:",innerxml"` +} + +// Object description +type Object struct { + ID string `xml:"id,attr"` + ParentID string `xml:"parentID,attr"` + Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable + Class string `xml:"upnp:class"` + Icon string `xml:"upnp:icon,omitempty"` + Title string `xml:"dc:title"` + Date Timestamp `xml:"dc:date"` + Artist string `xml:"upnp:artist,omitempty"` + Album string `xml:"upnp:album,omitempty"` + Genre string `xml:"upnp:genre,omitempty"` + AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` + Searchable int `xml:"searchable,attr"` +} + +// Timestamp wraps time.Time for formatting purposes +type Timestamp struct { + time.Time +} + +// MarshalXML formats the Timestamp per DIDL-Lite spec +func (t Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(t.Format("2006-01-02"), start) +} From 3152c7ef3f59f15a00c2561e929569ad8a1e9208 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:09:54 +0000 Subject: [PATCH 07/87] Compiles --- dlna/contenddirectoryservice.go | 151 ++------------------------ dlna/dlnaserver.go | 6 +- dlna/mediareceiverregistrarservice.go | 2 +- 3 files changed, 14 insertions(+), 145 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 694e7d743..4e1ecdce0 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -3,9 +3,7 @@ package dlna import ( - "context" "encoding/xml" - "errors" "fmt" "log" "net/http" @@ -14,10 +12,11 @@ import ( "path" "path/filepath" "regexp" - "strings" + "time" "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/upnp" + "github.com/navidrome/navidrome/dlna/upnpav" ) type contentDirectoryService struct { @@ -33,49 +32,25 @@ var mediaMimeTypeRegexp = regexp.MustCompile("^(video|audio|image)/") // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. -func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo vfs.Node, resources vfs.Nodes, host string) (ret interface{}, err error) { +func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, host string) (ret interface{}, err error) { obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, ParentID: cdsObject.ParentID(), } - if fileInfo.IsDir() { - defaultChildCount := 1 - obj.Class = "object.container.storageFolder" - obj.Title = fileInfo.Name() - return upnpav.Container{ - Object: obj, - ChildCount: &defaultChildCount, - }, nil - } - - if !fileInfo.Mode().IsRegular() { - return - } - // Read the mime type from the fs.Object if possible, // otherwise fall back to working out what it is from the file path. - var mimeType string - if o, ok := fileInfo.DirEntry().(fs.Object); ok { - mimeType = fs.MimeType(context.TODO(), o) - // If backend doesn't know what the mime type is then - // try getting it from the file name - if mimeType == "application/octet-stream" { - mimeType = fs.MimeTypeFromName(fileInfo.Name()) - } - } else { - mimeType = fs.MimeTypeFromName(fileInfo.Name()) - } - + var mimeType = "audio/mp3" //TODO + mediaType := mediaMimeTypeRegexp.FindStringSubmatch(mimeType) if mediaType == nil { return } obj.Class = "object.item." + mediaType[1] + "Item" - obj.Title = fileInfo.Name() - obj.Date = upnpav.Timestamp{Time: fileInfo.ModTime()} + obj.Title = "TITLE" + obj.Date = upnpav.Timestamp{Time: time.Now()} item := upnpav.Item{ Object: obj, @@ -91,113 +66,19 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: true, }.String()), - Size: uint64(fileInfo.Size()), + Size: uint64(1048576), //TODO }) - for _, resource := range resources { - subtitleURL := (&url.URL{ - Scheme: "http", - Host: host, - Path: path.Join(resPath, resource.Path()), - }).String() - item.Res = append(item.Res, upnpav.Resource{ - URL: subtitleURL, - ProtocolInfo: fmt.Sprintf("http-get:*:%s:*", "text/srt"), - }) - } - ret = item return } // Returns all the upnpav objects in a directory. func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { - node, err := cds.vfs.Stat(o.Path) - if err != nil { - return - } - - if !node.IsDir() { - err = errors.New("not a directory") - return - } - - dir := node.(*vfs.Dir) - dirEntries, err := dir.ReadDirAll() - if err != nil { - err = errors.New("failed to list directory") - return - } - - dirEntries, mediaResources := mediaWithResources(dirEntries) - for _, de := range dirEntries { - child := object{ - path.Join(o.Path, de.Name()), - } - obj, err := cds.cdsObjectToUpnpavObject(child, de, mediaResources[de], host) - if err != nil { - fs.Errorf(cds, "error with %s: %s", child.FilePath(), err) - continue - } - if obj == nil { - fs.Debugf(cds, "unrecognized file type: %s", de) - continue - } - ret = append(ret, obj) - } return } -// Given a list of nodes, separate them into potential media items and any associated resources (external subtitles, -// for example.) -// -// The result is a slice of potential media nodes (in their original order) and a map containing associated -// resources nodes of each media node, if any. -func mediaWithResources(nodes vfs.Nodes) (vfs.Nodes, map[vfs.Node]vfs.Nodes) { - media, mediaResources := vfs.Nodes{}, make(map[vfs.Node]vfs.Nodes) - - // First, separate out the subtitles and media into maps, keyed by their lowercase base names. - mediaByName, subtitlesByName := make(map[string]vfs.Nodes), make(map[string]vfs.Node) - for _, node := range nodes { - baseName, ext := splitExt(strings.ToLower(node.Name())) - switch ext { - case ".srt", ".ass", ".ssa", ".sub", ".idx", ".sup", ".jss", ".txt", ".usf", ".cue", ".vtt", ".css": - // .idx should be with .sub, .css should be with vtt otherwise they should be culled, - // and their mimeTypes are not consistent, but anyway these negatives don't throw errors. - subtitlesByName[baseName] = node - default: - mediaByName[baseName] = append(mediaByName[baseName], node) - media = append(media, node) - } - } - - // Find the associated media file for each subtitle - for baseName, node := range subtitlesByName { - // Find a media file with the same basename (video.mp4 for video.srt) - mediaNodes, found := mediaByName[baseName] - if !found { - // Or basename of the basename (video.mp4 for video.en.srt) - baseName, _ = splitExt(baseName) - mediaNodes, found = mediaByName[baseName] - } - - // Just advise if no match found - if !found { - fs.Infof(node, "could not find associated media for subtitle: %s", node.Name()) - continue - } - - // Associate with all potential media nodes - fs.Debugf(mediaNodes, "associating subtitle: %s", node.Name()) - for _, mediaNode := range mediaNodes { - mediaResources[mediaNode] = append(mediaResources[mediaNode], node) - } - } - - return media, mediaResources -} - type browse struct { ObjectID string BrowseFlag string @@ -272,21 +153,9 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt "UpdateID": cds.updateIDString(), }, nil case "BrowseMetadata": - node, err := cds.vfs.Stat(obj.Path) - if err != nil { - return nil, err - } - // TODO: External subtitles won't appear in the metadata here, but probably should. - upnpObject, err := cds.cdsObjectToUpnpavObject(obj, node, vfs.Nodes{}, host) - if err != nil { - return nil, err - } - result, err := xml.Marshal(upnpObject) - if err != nil { - return nil, err - } + //TODO return map[string]string{ - "Result": didlLite(string(result)), + "Result": didlLite(string("result")), }, nil default: return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "unhandled browse flag: %v", browse.BrowseFlag) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index eda01c832..2aa01524a 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -59,13 +59,13 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { s.ssdp.services = map[string]UPnPService { "ContentDirectory": &contentDirectoryService{ - server: s, + DLNAServer: s, }, "ConnectionManager": &connectionManagerService{ - server: s, + DLNAServer: s, }, "X_MS_MediaReceiverRegistrar": &mediaReceiverRegistrarService{ - server: s, + DLNAServer: s, }, } diff --git a/dlna/mediareceiverregistrarservice.go b/dlna/mediareceiverregistrarservice.go index 0be03bdb2..dc1260b40 100644 --- a/dlna/mediareceiverregistrarservice.go +++ b/dlna/mediareceiverregistrarservice.go @@ -21,7 +21,7 @@ func (mrrs *mediaReceiverRegistrarService) Handle(action string, argsXML []byte, }, nil case "RegisterDevice": return map[string]string{ - "RegistrationRespMsg": mrrs.RootDeviceUUID, + "RegistrationRespMsg": mrrs.ssdp.RootDeviceUUID, }, nil default: return nil, upnp.InvalidActionError From 07f2433dfc9c1578051b8e2ca542f252ccafcf18 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:22:06 +0000 Subject: [PATCH 08/87] Fleshing out ssdp init --- dlna/dlnaserver.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 2aa01524a..0fe5b406a 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -2,8 +2,10 @@ package dlna import ( "context" + "crypto/md5" "encoding/xml" "fmt" + "io" "log" "net" "net/http" @@ -53,9 +55,17 @@ type SSDPServer struct { } func New(ds model.DataStore, broker events.Broker) *DLNAServer { - s := &DLNAServer{ds: ds, broker: broker, ssdp: SSDPServer{}} - s.ssdp.Interfaces = listInterfaces() - s.ssdp.AnnounceInterval = time.Duration(30) * time.Second + s := &DLNAServer{ + ds: ds, + broker: broker, + ssdp: SSDPServer{ + AnnounceInterval: time.Duration(30) * time.Second, + Interfaces: listInterfaces(), + FriendlyName: "Navidrome", + RootDeviceUUID: makeDeviceUUID("Navidrome"), + waitChan: make(chan struct{}), + }, + } s.ssdp.services = map[string]UPnPService { "ContentDirectory": &contentDirectoryService{ @@ -260,3 +270,12 @@ func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte { return []byte(fmt.Sprintf(`%[3]s`, sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs))) } + +func makeDeviceUUID(unique string) string { + h := md5.New() + if _, err := io.WriteString(h, unique); err != nil { + log.Panicf("makeDeviceUUID write failed: %s", err) + } + buf := h.Sum(nil) + return upnp.FormatUUID(buf) +} \ No newline at end of file From deabd7040abf8442c2239dd9983ce2b2aef444a6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:40:43 +0000 Subject: [PATCH 09/87] Raw copy --- dlna/dlnaserver.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 0fe5b406a..ffe6fac85 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -79,6 +79,19 @@ 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.HandleFunc(rootDescPath, s.ssdp.rootDescHandler) + r.HandleFunc(serviceControlURL, s.ssdp.serviceControlHandler) + + r.Handle("/static/", http.StripPrefix("/static/", + withHeader("Cache-Control", "public, max-age=86400", + http.FileServer(data.Assets)))) + + //s.handler = logging(withHeader("Server", serverField, r)) + return s } @@ -240,6 +253,36 @@ func isAppropriatelyConfigured(intf net.Interface) bool { return intf.Flags&net.FlagUp != 0 && intf.Flags&net.FlagMulticast != 0 && intf.MTU > 0 } +func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { + remotePath := r.URL.Path + + node, err := s.vfs.Stat(r.URL.Path) + if err != nil { + http.NotFound(w, r) + return + } + + w.Header().Set("Content-Length", strconv.FormatInt(node.Size(), 10)) + + // add some DLNA specific headers + if r.Header.Get("getContentFeatures.dlna.org") != "" { + w.Header().Set("contentFeatures.dlna.org", dms_dlna.ContentFeatures{ + SupportRange: true, + }.String()) + } + w.Header().Set("transferMode.dlna.org", "Streaming") + + file := node.(*vfs.File) + in, err := file.Open(os.O_RDONLY) + if err != nil { + serveError(node, w, "Could not open resource", err) + return + } + defer fs.CheckClose(in, &err) + + http.ServeContent(w, r, remotePath, node.ModTime(), in) +} + func didlLite(chardata string) string { return ` Date: Thu, 2 Jan 2025 10:46:00 +0000 Subject: [PATCH 10/87] Compiles --- dlna/dlnaserver.go | 86 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index ffe6fac85..48a7f2b87 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -1,6 +1,7 @@ package dlna import ( + "bytes" "context" "crypto/md5" "encoding/xml" @@ -10,9 +11,11 @@ import ( "net" "net/http" "net/url" + "strconv" "strings" "time" + dms_dlna "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/soap" "github.com/anacrolix/dms/ssdp" "github.com/anacrolix/dms/upnp" @@ -88,7 +91,7 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { r.Handle("/static/", http.StripPrefix("/static/", withHeader("Cache-Control", "public, max-age=86400", - http.FileServer(data.Assets)))) + http.FileServer(http.Dir("/tmp"))))) //TODO //s.handler = logging(withHeader("Server", serverField, r)) @@ -254,15 +257,9 @@ func isAppropriatelyConfigured(intf net.Interface) bool { } func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { - remotePath := r.URL.Path + //remotePath := r.URL.Path - node, err := s.vfs.Stat(r.URL.Path) - if err != nil { - http.NotFound(w, r) - return - } - - w.Header().Set("Content-Length", strconv.FormatInt(node.Size(), 10)) + w.Header().Set("Content-Length", strconv.FormatInt(1024, 10)) //TODO // add some DLNA specific headers if r.Header.Get("getContentFeatures.dlna.org") != "" { @@ -272,17 +269,64 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("transferMode.dlna.org", "Streaming") - file := node.(*vfs.File) - in, err := file.Open(os.O_RDONLY) + //http.ServeContent(w, r, remotePath, time.Now(), in) +} + + + +func (s *SSDPServer) rootDescHandler(w http.ResponseWriter, r *http.Request) { + + buffer := new(bytes.Buffer) + + w.Header().Set("content-type", `text/xml; charset="utf-8"`) + w.Header().Set("cache-control", "private, max-age=60") + w.Header().Set("content-length", strconv.FormatInt(int64(buffer.Len()), 10)) + buffer.WriteTo(w) +} + +// Handle a service control HTTP request. +func (s *SSDPServer) serviceControlHandler(w http.ResponseWriter, r *http.Request) { + soapActionString := r.Header.Get("SOAPACTION") + soapAction, err := upnp.ParseActionHTTPHeader(soapActionString) if err != nil { - serveError(node, w, "Could not open resource", err) + serveError(s, w, "Could not parse SOAPACTION header", err) + return + } + var env soap.Envelope + if err := xml.NewDecoder(r.Body).Decode(&env); err != nil { + serveError(s, w, "Could not parse SOAP request body", err) return } - defer fs.CheckClose(in, &err) - http.ServeContent(w, r, remotePath, node.ModTime(), in) + w.Header().Set("Content-Type", `text/xml; charset="utf-8"`) + w.Header().Set("Ext", "") + soapRespXML, code := func() ([]byte, int) { + respArgs, err := s.soapActionResponse(soapAction, env.Body.Action, r) + if err != nil { + fmt.Printf("Error invoking %v: %v", soapAction, err) + upnpErr := upnp.ConvertError(err) + return mustMarshalXML(soap.NewFault("UPnPError", upnpErr)), http.StatusInternalServerError + } + return marshalSOAPResponse(soapAction, respArgs), http.StatusOK + }() + bodyStr := fmt.Sprintf(`%s`, soapRespXML) + w.WriteHeader(code) + if _, err := w.Write([]byte(bodyStr)); err != nil { + fmt.Printf("Error writing response: %v", err) + } } +// Handle a SOAP request and return the response arguments or UPnP error. +func (s *SSDPServer) soapActionResponse(sa upnp.SoapAction, actionRequestXML []byte, r *http.Request) (map[string]string, error) { + service, ok := s.services[sa.Type] + if !ok { + // TODO: What's the invalid service error? + return nil, upnp.Errorf(upnp.InvalidActionErrorCode, "Invalid service: %s", sa.Type) + } + return service.Handle(sa.Action, actionRequestXML, r) +} + + func didlLite(chardata string) string { return ` Date: Thu, 2 Jan 2025 17:58:14 +0000 Subject: [PATCH 11/87] handler hooked in --- dlna/dlnaserver.go | 76 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 48a7f2b87..11406fde6 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -59,18 +59,18 @@ type SSDPServer struct { func New(ds model.DataStore, broker events.Broker) *DLNAServer { s := &DLNAServer{ - ds: ds, - broker: broker, + ds: ds, + broker: broker, ssdp: SSDPServer{ AnnounceInterval: time.Duration(30) * time.Second, - Interfaces: listInterfaces(), - FriendlyName: "Navidrome", - RootDeviceUUID: makeDeviceUUID("Navidrome"), - waitChan: make(chan struct{}), + Interfaces: listInterfaces(), + FriendlyName: "Navidrome", + RootDeviceUUID: makeDeviceUUID("Navidrome"), + waitChan: make(chan struct{}), }, } - s.ssdp.services = map[string]UPnPService { + s.ssdp.services = map[string]UPnPService{ "ContentDirectory": &contentDirectoryService{ DLNAServer: s, }, @@ -85,15 +85,15 @@ 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.HandleFunc(rootDescPath, s.ssdp.rootDescHandler) r.HandleFunc(serviceControlURL, s.ssdp.serviceControlHandler) r.Handle("/static/", http.StripPrefix("/static/", withHeader("Cache-Control", "public, max-age=86400", - http.FileServer(http.Dir("/tmp"))))) //TODO - - //s.handler = logging(withHeader("Server", serverField, r)) + http.FileServer(http.Dir("/tmp"))))) //TODO + + s.ssdp.handler = logging(withHeader("Server", serverField, r)) return s } @@ -258,8 +258,8 @@ func isAppropriatelyConfigured(intf net.Interface) bool { func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { //remotePath := r.URL.Path - - w.Header().Set("Content-Length", strconv.FormatInt(1024, 10)) //TODO + + w.Header().Set("Content-Length", strconv.FormatInt(1024, 10)) //TODO // add some DLNA specific headers if r.Header.Get("getContentFeatures.dlna.org") != "" { @@ -272,8 +272,6 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { //http.ServeContent(w, r, remotePath, time.Now(), in) } - - func (s *SSDPServer) rootDescHandler(w http.ResponseWriter, r *http.Request) { buffer := new(bytes.Buffer) @@ -326,7 +324,6 @@ func (s *SSDPServer) soapActionResponse(sa upnp.SoapAction, actionRequestXML []b return service.Handle(sa.Action, actionRequestXML, r) } - func didlLite(chardata string) string { return ` Date: Thu, 2 Jan 2025 17:58:45 +0000 Subject: [PATCH 12/87] logging thing not required --- dlna/dlnaserver.go | 46 +--------------------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 11406fde6..d1cf5467a 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -93,7 +93,7 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { withHeader("Cache-Control", "public, max-age=86400", http.FileServer(http.Dir("/tmp"))))) //TODO - s.ssdp.handler = logging(withHeader("Server", serverField, r)) + s.ssdp.handler = r return s } @@ -376,47 +376,3 @@ func withHeader(name string, value string, next http.Handler) http.Handler { func serveError(what interface{}, w http.ResponseWriter, text string, err error) { http.Error(w, text+".", http.StatusInternalServerError) } - -type loggingResponseWriter struct { - http.ResponseWriter - request *http.Request - committed bool -} - -func (lrw *loggingResponseWriter) logRequest(code int, err interface{}) { - // Choose appropriate log level based on response status code. - - if err == nil { - err = "" - } - - log.Printf("%s %s %d %s %s", - lrw.request.RemoteAddr, lrw.request.Method, code, - lrw.request.Header.Get("SOAPACTION"), err) -} - -func (lrw *loggingResponseWriter) WriteHeader(code int) { - lrw.committed = true - lrw.logRequest(code, nil) - lrw.ResponseWriter.WriteHeader(code) -} - -// HTTP handler that logs requests and any errors or panics. -func logging(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - lrw := &loggingResponseWriter{ResponseWriter: w, request: r} - defer func() { - err := recover() - if err != nil { - if !lrw.committed { - lrw.logRequest(http.StatusInternalServerError, err) - http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) - } else { - // Too late to send the error to client, but at least log it. - log.Printf("Recovered panic: %v", err) - } - } - }() - next.ServeHTTP(lrw, r) - }) -} From e4035f6a457f8d29d4ec4f1752c028e258e0381e Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 00:31:30 +0000 Subject: [PATCH 13/87] Adding new HTTP server for dlna --- dlna/dlnaserver.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index d1cf5467a..299c19e86 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -114,6 +114,9 @@ func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert str go func() { s.ssdp.startSSDP() }() + go func() { + s.ssdp.serveHTTP() + }() return nil } @@ -324,6 +327,19 @@ func (s *SSDPServer) soapActionResponse(sa upnp.SoapAction, actionRequestXML []b return service.Handle(sa.Action, actionRequestXML, r) } +func (s *SSDPServer) serveHTTP() error { + srv := &http.Server{ + Handler: s.handler, + } + err := srv.Serve(s.HTTPConn) + select { + case <-s.waitChan: + return nil + default: + return err + } +} + func didlLite(chardata string) string { return ` Date: Fri, 3 Jan 2025 10:26:23 +0000 Subject: [PATCH 14/87] Hacking in rootDesc from rclone --- dlna/dlnaserver.go | 85 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 299c19e86..feb78a207 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "time" + "text/template" dms_dlna "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/soap" @@ -91,7 +92,7 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { r.Handle("/static/", http.StripPrefix("/static/", withHeader("Cache-Control", "public, max-age=86400", - http.FileServer(http.Dir("/tmp"))))) //TODO + http.FileServer(http.Dir("/tmp"))))) //TODO, is /static needed? s.ssdp.handler = r @@ -275,9 +276,12 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { //http.ServeContent(w, r, remotePath, time.Now(), in) } +// returns content for /rootDesc.xml func (s *SSDPServer) rootDescHandler(w http.ResponseWriter, r *http.Request) { + tmpl, _ := GetTemplate() buffer := new(bytes.Buffer) + _ = tmpl.Execute(buffer, s) w.Header().Set("content-type", `text/xml; charset="utf-8"`) w.Header().Set("cache-control", "private, max-age=60") @@ -392,3 +396,82 @@ func withHeader(name string, value string, next http.Handler) http.Handler { func serveError(what interface{}, w http.ResponseWriter, text string, err error) { http.Error(w, text+".", http.StatusInternalServerError) } + +func GetTemplate() (tpl *template.Template, err error) { + + templateBytes := ` + + + 1 + 0 + + + urn:schemas-upnp-org:device:MediaServer:1 + {{.FriendlyName}} + rclone (rclone.org) + https://rclone.org/ + rclone + rclone + {{.ModelNumber}} + https://rclone.org/ + 00000000 + {{.RootDeviceUUID}} + + DMS-1.50 + M-DMS-1.50 + smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec + smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec + + + image/png + 48 + 48 + 8 + /static/rclone-48x48.png + + + image/png + 120 + 120 + 8 + /static/rclone-120x120.png + + + + + urn:schemas-upnp-org:service:ContentDirectory:1 + urn:upnp-org:serviceId:ContentDirectory + /static/ContentDirectory.xml + /ctl + + + + urn:schemas-upnp-org:service:ConnectionManager:1 + urn:upnp-org:serviceId:ConnectionManager + /static/ConnectionManager.xml + /ctl + + + + urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1 + urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar + /static/X_MS_MediaReceiverRegistrar.xml + /ctl + + + + / + +` + + var templateString = string(templateBytes) + + tpl, err = template.New("rootDesc").Parse(templateString) + if err != nil { + return nil, fmt.Errorf("get template parse: %w", err) + } + + return +} \ No newline at end of file From eb7bbaaa26860cc1d465471f2069ec375bdb1eb9 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 10:27:29 +0000 Subject: [PATCH 15/87] cutting image out --- dlna/dlnaserver.go | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index feb78a207..2e706ff66 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -410,12 +410,12 @@ func GetTemplate() (tpl *template.Template, err error) { urn:schemas-upnp-org:device:MediaServer:1 {{.FriendlyName}} - rclone (rclone.org) - https://rclone.org/ - rclone - rclone + Navidrome + https://www.navidrome.org/ + Navidrome + Navidrome {{.ModelNumber}} - https://rclone.org/ + https://www.navidrome.org/ 00000000 {{.RootDeviceUUID}} @@ -423,22 +423,6 @@ func GetTemplate() (tpl *template.Template, err error) { M-DMS-1.50 smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec - - - image/png - 48 - 48 - 8 - /static/rclone-48x48.png - - - image/png - 120 - 120 - 8 - /static/rclone-120x120.png - - urn:schemas-upnp-org:service:ContentDirectory:1 From 441dfa31e44e700d873185152d9fa0cbe1d4ebca Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 10:30:41 +0000 Subject: [PATCH 16/87] Adding ModelNumber placeholder so full template is sent --- dlna/dlnaserver.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 2e706ff66..13236b143 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -50,6 +50,7 @@ type SSDPServer struct { RootDeviceUUID string FriendlyName string + ModelNumber string // For waiting on the listener to close waitChan chan struct{} @@ -66,6 +67,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 RootDeviceUUID: makeDeviceUUID("Navidrome"), waitChan: make(chan struct{}), }, From 95d78f0e2bd53f8bae4fde37c36f71767310959b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 10:48:45 +0000 Subject: [PATCH 17/87] Getting rid of /static for now --- dlna/dlnaserver.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 13236b143..486dfccf8 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -91,11 +91,7 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { r.HandleFunc(rootDescPath, s.ssdp.rootDescHandler) r.HandleFunc(serviceControlURL, s.ssdp.serviceControlHandler) - - r.Handle("/static/", http.StripPrefix("/static/", - withHeader("Cache-Control", "public, max-age=86400", - http.FileServer(http.Dir("/tmp"))))) //TODO, is /static needed? - + s.ssdp.handler = r return s @@ -262,6 +258,7 @@ 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` func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { //remotePath := r.URL.Path @@ -278,7 +275,7 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { //http.ServeContent(w, r, remotePath, time.Now(), in) } -// returns content for /rootDesc.xml +// returns /rootDesc.xml templated func (s *SSDPServer) rootDescHandler(w http.ResponseWriter, r *http.Request) { tmpl, _ := GetTemplate() From 9f40c5705435bcd45e336426457393a80b78fa86 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 11:06:17 +0000 Subject: [PATCH 18/87] this almost works, but /static/static is needed --- dlna/dlnaserver.go | 10 +- dlna/static/ConnectionManager.xml | 182 +++++++ dlna/static/ContentDirectory.xml | 504 ++++++++++++++++++++ dlna/static/X_MS_MediaReceiverRegistrar.xml | 88 ++++ 4 files changed, 781 insertions(+), 3 deletions(-) create mode 100644 dlna/static/ConnectionManager.xml create mode 100644 dlna/static/ContentDirectory.xml create mode 100644 dlna/static/X_MS_MediaReceiverRegistrar.xml diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 486dfccf8..cd5704104 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/md5" + "embed" "encoding/xml" "fmt" "io" @@ -13,8 +14,8 @@ import ( "net/url" "strconv" "strings" - "time" "text/template" + "time" dms_dlna "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/soap" @@ -30,6 +31,8 @@ const ( resPath = "/r/" serviceControlURL = "/ctl" ) +//go:embed static/* +var staticContent embed.FS type DLNAServer struct { ds model.DataStore @@ -88,10 +91,11 @@ 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.StripPrefix("/static/", http.FileServer(http.FS(staticContent)))) r.HandleFunc(rootDescPath, s.ssdp.rootDescHandler) r.HandleFunc(serviceControlURL, s.ssdp.serviceControlHandler) - + s.ssdp.handler = r return s diff --git a/dlna/static/ConnectionManager.xml b/dlna/static/ConnectionManager.xml new file mode 100644 index 000000000..97a52f152 --- /dev/null +++ b/dlna/static/ConnectionManager.xml @@ -0,0 +1,182 @@ + + + + 1 + 0 + + + + GetProtocolInfo + + + Source + out + SourceProtocolInfo + + + Sink + out + SinkProtocolInfo + + + + + PrepareForConnection + + + RemoteProtocolInfo + in + A_ARG_TYPE_ProtocolInfo + + + PeerConnectionManager + in + A_ARG_TYPE_ConnectionManager + + + PeerConnectionID + in + A_ARG_TYPE_ConnectionID + + + Direction + in + A_ARG_TYPE_Direction + + + ConnectionID + out + A_ARG_TYPE_ConnectionID + + + AVTransportID + out + A_ARG_TYPE_AVTransportID + + + RcsID + out + A_ARG_TYPE_RcsID + + + + + ConnectionComplete + + + ConnectionID + in + A_ARG_TYPE_ConnectionID + + + + + GetCurrentConnectionIDs + + + ConnectionIDs + out + CurrentConnectionIDs + + + + + GetCurrentConnectionInfo + + + ConnectionID + in + A_ARG_TYPE_ConnectionID + + + RcsID + out + A_ARG_TYPE_RcsID + + + AVTransportID + out + A_ARG_TYPE_AVTransportID + + + ProtocolInfo + out + A_ARG_TYPE_ProtocolInfo + + + PeerConnectionManager + out + A_ARG_TYPE_ConnectionManager + + + PeerConnectionID + out + A_ARG_TYPE_ConnectionID + + + Direction + out + A_ARG_TYPE_Direction + + + Status + out + A_ARG_TYPE_ConnectionStatus + + + + + + + SourceProtocolInfo + string + + + SinkProtocolInfo + string + + + CurrentConnectionIDs + string + + + A_ARG_TYPE_ConnectionStatus + string + + OK + ContentFormatMismatch + InsufficientBandwidth + UnreliableChannel + Unknown + + + + A_ARG_TYPE_ConnectionManager + string + + + A_ARG_TYPE_Direction + string + + Input + Output + + + + A_ARG_TYPE_ProtocolInfo + string + + + A_ARG_TYPE_ConnectionID + i4 + + + A_ARG_TYPE_AVTransportID + i4 + + + A_ARG_TYPE_RcsID + i4 + + + \ No newline at end of file diff --git a/dlna/static/ContentDirectory.xml b/dlna/static/ContentDirectory.xml new file mode 100644 index 000000000..12fddb98d --- /dev/null +++ b/dlna/static/ContentDirectory.xml @@ -0,0 +1,504 @@ + + + + 1 + 0 + + + + GetSearchCapabilities + + + SearchCaps + out + SearchCapabilities + + + + + GetSortCapabilities + + + SortCaps + out + SortCapabilities + + + + + GetSortExtensionCapabilities + + + SortExtensionCaps + out + SortExtensionCapabilities + + + + + GetFeatureList + + + FeatureList + out + FeatureList + + + + + GetSystemUpdateID + + + Id + out + SystemUpdateID + + + + + Browse + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + BrowseFlag + in + A_ARG_TYPE_BrowseFlag + + + Filter + in + A_ARG_TYPE_Filter + + + StartingIndex + in + A_ARG_TYPE_Index + + + RequestedCount + in + A_ARG_TYPE_Count + + + SortCriteria + in + A_ARG_TYPE_SortCriteria + + + Result + out + A_ARG_TYPE_Result + + + NumberReturned + out + A_ARG_TYPE_Count + + + TotalMatches + out + A_ARG_TYPE_Count + + + UpdateID + out + A_ARG_TYPE_UpdateID + + + + + Search + + + ContainerID + in + A_ARG_TYPE_ObjectID + + + SearchCriteria + in + A_ARG_TYPE_SearchCriteria + + + Filter + in + A_ARG_TYPE_Filter + + + StartingIndex + in + A_ARG_TYPE_Index + + + RequestedCount + in + A_ARG_TYPE_Count + + + SortCriteria + in + A_ARG_TYPE_SortCriteria + + + Result + out + A_ARG_TYPE_Result + + + NumberReturned + out + A_ARG_TYPE_Count + + + TotalMatches + out + A_ARG_TYPE_Count + + + UpdateID + out + A_ARG_TYPE_UpdateID + + + + + CreateObject + + + ContainerID + in + A_ARG_TYPE_ObjectID + + + Elements + in + A_ARG_TYPE_Result + + + ObjectID + out + A_ARG_TYPE_ObjectID + + + Result + out + A_ARG_TYPE_Result + + + + + DestroyObject + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + + + UpdateObject + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + CurrentTagValue + in + A_ARG_TYPE_TagValueList + + + NewTagValue + in + A_ARG_TYPE_TagValueList + + + + + MoveObject + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + NewParentID + in + A_ARG_TYPE_ObjectID + + + NewObjectID + out + A_ARG_TYPE_ObjectID + + + + + ImportResource + + + SourceURI + in + A_ARG_TYPE_URI + + + DestinationURI + in + A_ARG_TYPE_URI + + + TransferID + out + A_ARG_TYPE_TransferID + + + + + ExportResource + + + SourceURI + in + A_ARG_TYPE_URI + + + DestinationURI + in + A_ARG_TYPE_URI + + + TransferID + out + A_ARG_TYPE_TransferID + + + + + StopTransferResource + + + TransferID + in + A_ARG_TYPE_TransferID + + + + + DeleteResource + + + ResourceURI + in + A_ARG_TYPE_URI + + + + + GetTransferProgress + + + TransferID + in + A_ARG_TYPE_TransferID + + + TransferStatus + out + A_ARG_TYPE_TransferStatus + + + TransferLength + out + A_ARG_TYPE_TransferLength + + + TransferTotal + out + A_ARG_TYPE_TransferTotal + + + + + CreateReference + + + ContainerID + in + A_ARG_TYPE_ObjectID + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + NewID + out + A_ARG_TYPE_ObjectID + + + + + X_GetFeatureList + + + FeatureList + out + A_ARG_TYPE_Featurelist + + + + + X_SetBookmark + + + CategoryType + in + A_ARG_TYPE_CategoryType + + + RID + in + A_ARG_TYPE_RID + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + PosSecond + in + A_ARG_TYPE_PosSec + + + + + + + SearchCapabilities + string + + + SortCapabilities + string + + + SortExtensionCapabilities + string + + + SystemUpdateID + ui4 + + + ContainerUpdateIDs + string + + + TransferIDs + string + + + FeatureList + string + + + A_ARG_TYPE_ObjectID + string + + + A_ARG_TYPE_Result + string + + + A_ARG_TYPE_SearchCriteria + string + + + A_ARG_TYPE_BrowseFlag + string + + BrowseMetadata + BrowseDirectChildren + + + + A_ARG_TYPE_Filter + string + + + A_ARG_TYPE_SortCriteria + string + + + A_ARG_TYPE_Index + ui4 + + + A_ARG_TYPE_Count + ui4 + + + A_ARG_TYPE_UpdateID + ui4 + + + A_ARG_TYPE_TransferID + ui4 + + + A_ARG_TYPE_TransferStatus + string + + COMPLETED + ERROR + IN_PROGRESS + STOPPED + + + + A_ARG_TYPE_TransferLength + string + + + A_ARG_TYPE_TransferTotal + string + + + A_ARG_TYPE_TagValueList + string + + + A_ARG_TYPE_URI + uri + + + A_ARG_TYPE_CategoryType + ui4 + + + + A_ARG_TYPE_RID + ui4 + + + + A_ARG_TYPE_PosSec + ui4 + + + + A_ARG_TYPE_Featurelist + string + + + + \ No newline at end of file diff --git a/dlna/static/X_MS_MediaReceiverRegistrar.xml b/dlna/static/X_MS_MediaReceiverRegistrar.xml new file mode 100644 index 000000000..4aecdff00 --- /dev/null +++ b/dlna/static/X_MS_MediaReceiverRegistrar.xml @@ -0,0 +1,88 @@ + + + + 1 + 0 + + + + IsAuthorized + + + DeviceID + in + A_ARG_TYPE_DeviceID + + + Result + out + A_ARG_TYPE_Result + + + + + RegisterDevice + + + RegistrationReqMsg + in + A_ARG_TYPE_RegistrationReqMsg + + + RegistrationRespMsg + out + A_ARG_TYPE_RegistrationRespMsg + + + + + IsValidated + + + DeviceID + in + A_ARG_TYPE_DeviceID + + + Result + out + A_ARG_TYPE_Result + + + + + + + A_ARG_TYPE_DeviceID + string + + + A_ARG_TYPE_Result + int + + + A_ARG_TYPE_RegistrationReqMsg + bin.base64 + + + A_ARG_TYPE_RegistrationRespMsg + bin.base64 + + + AuthorizationGrantedUpdateID + ui4 + + + AuthorizationDeniedUpdateID + ui4 + + + ValidationSucceededUpdateID + ui4 + + + ValidationRevokedUpdateID + ui4 + + + \ No newline at end of file From d64c765262d6a4410af3e25a450106a89631ac0f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 11:09:11 +0000 Subject: [PATCH 19/87] /static now rendered out --- dlna/dlnaserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index cd5704104..485f13f29 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -92,7 +92,7 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { r := http.NewServeMux() r.Handle(resPath, http.StripPrefix(resPath, http.HandlerFunc(s.ssdp.resourceHandler))) - r.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticContent)))) + r.Handle("/static/", http.FileServer(http.FS(staticContent))) r.HandleFunc(rootDescPath, s.ssdp.rootDescHandler) r.HandleFunc(serviceControlURL, s.ssdp.serviceControlHandler) From 28ca4a4fe79b6d3829825d29645057a5a7a23f78 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 15:51:07 +0000 Subject: [PATCH 20/87] Hooking callbacks up so we can populate the Navidrome bits --- dlna/contenddirectoryservice.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 4e1ecdce0..11643712b 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -11,7 +11,6 @@ import ( "os" "path" "path/filepath" - "regexp" "time" "github.com/anacrolix/dms/dlna" @@ -28,8 +27,6 @@ func (cds *contentDirectoryService) updateIDString() string { return fmt.Sprintf("%d", uint32(os.Getpid())) } -var mediaMimeTypeRegexp = regexp.MustCompile("^(video|audio|image)/") - // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, host string) (ret interface{}, err error) { @@ -43,13 +40,8 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, ho // otherwise fall back to working out what it is from the file path. var mimeType = "audio/mp3" //TODO - mediaType := mediaMimeTypeRegexp.FindStringSubmatch(mimeType) - if mediaType == nil { - return - } - - obj.Class = "object.item." + mediaType[1] + "Item" - obj.Title = "TITLE" + obj.Class = "object.item.audioItem" + obj.Title = cdsObject.Path obj.Date = upnpav.Timestamp{Time: time.Now()} item := upnpav.Item{ @@ -75,7 +67,12 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, ho // Returns all the upnpav objects in a directory. func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { - + switch o.Path { + case "/": + newObject := object{Path: "/Music",} + thisObject, _ := cds.cdsObjectToUpnpavObject(newObject, host) + ret = append(ret, thisObject) + } return } From fb3b3989c67cb0739b30a3c6607fb5e24ec24dbe Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 16:15:44 +0000 Subject: [PATCH 21/87] Adding logs --- dlna/contenddirectoryservice.go | 1 + 1 file changed, 1 insertion(+) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 11643712b..0b08f63f8 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -86,6 +86,7 @@ type browse struct { // ContentDirectory object from ObjectID. func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) { + log.Printf("objectFromID Called: %+v", id) o.Path, err = url.QueryUnescape(id) if err != nil { return From 6164187d16fcbd5519ce98b75b9c16529fa1ccaf Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 00:07:19 +0000 Subject: [PATCH 22/87] Directories and stuff --- dlna/contenddirectoryservice.go | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 0b08f63f8..30f88d736 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -15,6 +15,7 @@ import ( "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/upnp" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/dlna/upnpav" ) @@ -29,17 +30,25 @@ func (cds *contentDirectoryService) updateIDString() string { // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. -func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, host string) (ret interface{}, err error) { +func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, isContainer bool, host string) (ret interface{}, err error) { obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, ParentID: cdsObject.ParentID(), } - + if isContainer { + defaultChildCount := 1 + obj.Class = "object.container.storageFolder" + obj.Title = cdsObject.Path + return upnpav.Container{ + Object: obj, + ChildCount: &defaultChildCount, + }, nil + } // Read the mime type from the fs.Object if possible, // otherwise fall back to working out what it is from the file path. var mimeType = "audio/mp3" //TODO - + obj.Class = "object.item.audioItem" obj.Title = cdsObject.Path obj.Date = upnpav.Timestamp{Time: time.Now()} @@ -69,9 +78,19 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, ho func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { switch o.Path { case "/": - newObject := object{Path: "/Music",} - thisObject, _ := cds.cdsObjectToUpnpavObject(newObject, host) + newObject := object{Path: "/Music"} + thisObject, _ := cds.cdsObjectToUpnpavObject(newObject, true, host) ret = append(ret, thisObject) + case "/Music/": + + files, _ := os.ReadDir(conf.Server.MusicFolder) //TODO + for _, file := range files { + child := object{ + path.Join(o.Path, file.Name()), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) + ret = append(ret, convObj) + } } return } From cfc7663f5f109b9f5ec5202c4e6cd59776a7b549 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 10:52:42 +0000 Subject: [PATCH 23/87] Making it look nicer --- dlna/contenddirectoryservice.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 30f88d736..2639f8bed 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -35,11 +35,12 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is ID: cdsObject.ID(), Restricted: 1, ParentID: cdsObject.ParentID(), + Title: filepath.Base(cdsObject.Path), } + if isContainer { defaultChildCount := 1 obj.Class = "object.container.storageFolder" - obj.Title = cdsObject.Path return upnpav.Container{ Object: obj, ChildCount: &defaultChildCount, @@ -50,7 +51,6 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is var mimeType = "audio/mp3" //TODO obj.Class = "object.item.audioItem" - obj.Title = cdsObject.Path obj.Date = upnpav.Timestamp{Time: time.Now()} item := upnpav.Item{ @@ -76,14 +76,24 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is // Returns all the upnpav objects in a directory. func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { + log.Printf("ReadContainer called with : %+v", o) switch o.Path { case "/": newObject := object{Path: "/Music"} thisObject, _ := cds.cdsObjectToUpnpavObject(newObject, true, host) ret = append(ret, thisObject) - case "/Music/": - - files, _ := os.ReadDir(conf.Server.MusicFolder) //TODO + case "/Music": + thisObject, _ := cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Tracks"}, true, host) + ret = append(ret, thisObject) + case "/Music/Files": + log.Printf("calling for /Music/Files") + files, _ := os.ReadDir(conf.Server.MusicFolder) for _, file := range files { child := object{ path.Join(o.Path, file.Name()), @@ -91,6 +101,9 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) ret = append(ret, convObj) } + case "/Music/Artists": + case "/Music/Albums": + case "/Music/Tracks": } return } From 9881a5ff107476d2efd618b9e4318125ec94d2ba Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 11:02:53 +0000 Subject: [PATCH 24/87] Making folder structure more like minidlna --- dlna/contenddirectoryservice.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 2639f8bed..a430c2d59 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -89,10 +89,13 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] ret = append(ret, thisObject) thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host) ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Tracks"}, true, host) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genre"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host) ret = append(ret, thisObject) case "/Music/Files": - log.Printf("calling for /Music/Files") files, _ := os.ReadDir(conf.Server.MusicFolder) for _, file := range files { child := object{ @@ -102,8 +105,9 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] ret = append(ret, convObj) } case "/Music/Artists": + case "/Music/Albums": - case "/Music/Tracks": + } return } From 01626f2ac994e62295d6530680c46530af663f30 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 13:51:06 +0000 Subject: [PATCH 25/87] Hack hack hack, playback of files now works --- dlna/contenddirectoryservice.go | 17 ++++++++++++++++- dlna/dlnaserver.go | 27 ++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index a430c2d59..8ffe7e225 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "strings" "time" "github.com/anacrolix/dms/dlna" @@ -77,6 +78,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is // Returns all the upnpav objects in a directory. func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { log.Printf("ReadContainer called with : %+v", o) + //TODO implement HTTP routing in a way that isn't awful switch o.Path { case "/": newObject := object{Path: "/Music"} @@ -109,6 +111,19 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] case "/Music/Albums": } + + if strings.HasPrefix(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 { + child := object{ + path.Join(o.Path, file.Name()), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) + ret = append(ret, convObj) + } + } return } @@ -140,7 +155,7 @@ func (cds *contentDirectoryService) objectFromID(id string) (o object, err error func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) { host := r.Host - + log.Printf("Handle called with action: %s", action) switch action { case "GetSystemUpdateID": return map[string]string{ diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 485f13f29..80746e77a 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -12,6 +12,8 @@ import ( "net" "net/http" "net/url" + "os" + "path" "strconv" "strings" "text/template" @@ -21,6 +23,7 @@ import ( "github.com/anacrolix/dms/soap" "github.com/anacrolix/dms/ssdp" "github.com/anacrolix/dms/upnp" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/events" ) @@ -264,9 +267,19 @@ func isAppropriatelyConfigured(intf net.Interface) bool { //handler for all paths under `/r` func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { - //remotePath := r.URL.Path + remotePath := r.URL.Path - w.Header().Set("Content-Length", strconv.FormatInt(1024, 10)) //TODO + 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) + if err != nil { + http.NotFound(w,r) + return + } + w.Header().Set("Content-Length", strconv.FormatInt(fileStats.Size(), 10)) // add some DLNA specific headers if r.Header.Get("getContentFeatures.dlna.org") != "" { @@ -276,7 +289,15 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("transferMode.dlna.org", "Streaming") - //http.ServeContent(w, r, remotePath, time.Now(), in) + os.Open(localFilePath) + fileHandle,err := os.Open(localFilePath) + if err != nil { + fmt.Printf("file streaming error: %+v\n", err) + return + } + defer fileHandle.Close() + + http.ServeContent(w, r, remotePath, time.Now(), fileHandle) } // returns /rootDesc.xml templated From a424a007186a7097af1be7caa04f987fc3f2cb8b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 14:18:02 +0000 Subject: [PATCH 26/87] Hackety hack hack in more endpoints --- cmd/root.go | 2 +- dlna/contenddirectoryservice.go | 75 +++++++++++++++++++++++++++------ dlna/dlnaserver.go | 7 +-- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5980fea12..144ca4d5f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -146,7 +146,7 @@ func startServer(ctx context.Context) func() error { func startDLNAServer(ctx context.Context) func() error { return func() error { a := CreateDLNAServer() - return a.Run(ctx, conf.Server.Address, conf.Server.Port, conf.Server.TLSCert, conf.Server.TLSKey) + return a.Run(ctx, conf.Server.Address, conf.Server.Port) } } diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 8ffe7e225..ee2ae863e 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -33,7 +33,7 @@ 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), @@ -43,7 +43,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is 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{ @@ -78,7 +78,8 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is // Returns all the upnpav objects in a directory. func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { log.Printf("ReadContainer called with : %+v", o) - //TODO implement HTTP routing in a way that isn't awful + + //TODO implement HTTP routing rather than this switch o.Path { case "/": newObject := object{Path: "/Music"} @@ -91,7 +92,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] ret = append(ret, thisObject) thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host) ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genre"}, true, host) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host) ret = append(ret, thisObject) thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host) ret = append(ret, thisObject) @@ -107,11 +108,61 @@ 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 { + 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 + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } case "/Music/Albums": - + indexes,err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + case "/Music/Genres": + indexes,err := cds.ds.Genre(cds.ctx).GetAll() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + case "/Music/Playlists": + indexes,err := cds.ds.Playlist(cds.ctx).GetAll() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } } + + if strings.HasPrefix(o.Path, "/Music/Files/") { libraryPath,_ := strings.CutPrefix(o.Path, "/Music/Files") log.Printf("library path: %s", libraryPath) @@ -128,9 +179,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 } @@ -198,8 +249,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 80746e77a..5c271e359 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -41,6 +41,7 @@ type DLNAServer struct { ds model.DataStore broker events.Broker ssdp SSDPServer + ctx context.Context } type SSDPServer struct { @@ -104,9 +105,9 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { return s } -// Run starts the server with the given address, and if specified, with TLS enabled. -func (s *DLNAServer) Run(ctx context.Context, addr string, port int, tlsCert string, tlsKey string) (err error) { - +// 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) { + s.ctx = ctx if s.ssdp.HTTPConn == nil { network := "tcp4" if strings.Count(s.ssdp.httpListenAddr, ":") > 1 { From 151092661c5c627ac1d728f2abf4d3f2a6a595c6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 12:29:29 +0000 Subject: [PATCH 27/87] 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 +} From 03eae403b496faeeba6d38e28809f9fecb99c5c9 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 12:35:57 +0000 Subject: [PATCH 28/87] Log -> Navidrome.Log --- dlna/contenddirectoryservice.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 33e13b26c..88aeb68f2 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -5,7 +5,6 @@ package dlna import ( "encoding/xml" "fmt" - "log" "net/http" "net/url" "os" @@ -18,6 +17,7 @@ import ( "github.com/anacrolix/dms/upnp" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/dlna/upnpav" + "github.com/navidrome/navidrome/log" ) type contentDirectoryService struct { @@ -77,7 +77,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is // Returns all the upnpav objects in a directory. func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { - log.Printf("ReadContainer called with : %+v", o) + log.Info(fmt.Sprintf("ReadContainer called with : %+v", o)) //TODO implement HTTP routing rather than this switch o.Path { @@ -163,7 +163,6 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] if strings.HasPrefix(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 { child := object{ @@ -186,7 +185,8 @@ type browse struct { // ContentDirectory object from ObjectID. func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) { - log.Printf("objectFromID Called: %+v", id) + log.Info(fmt.Sprintf("objectFromID called with : %+v", id)) + o.Path, err = url.QueryUnescape(id) if err != nil { return @@ -204,7 +204,8 @@ func (cds *contentDirectoryService) objectFromID(id string) (o object, err error func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) { host := r.Host - log.Printf("Handle called with action: %s", action) + log.Info(fmt.Sprintf("Handle called with action: %s", action)) + switch action { case "GetSystemUpdateID": return map[string]string{ @@ -293,7 +294,7 @@ func (o *object) FilePath() string { // Returns the ObjectID for the object. This is used in various ContentDirectory actions. func (o object) ID() string { if !path.IsAbs(o.Path) { - log.Panicf("Relative object path: %s", o.Path) + log.Fatal(fmt.Sprintf("Relative object path used with ID: $s", o.Path)) } if len(o.Path) == 1 { return "0" From c5bcdc370131891e1d6797b8de2463a6bebf380f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 15:49:04 +0000 Subject: [PATCH 29/87] Version number --- dlna/dlnaserver.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index b985c2c2a..bd9b88c19 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -23,6 +23,7 @@ import ( "github.com/anacrolix/dms/ssdp" "github.com/anacrolix/dms/upnp" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/events" @@ -75,7 +76,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: consts.Version, RootDeviceUUID: makeDeviceUUID("Navidrome"), waitChan: make(chan struct{}), }, From 4b9e254ae4b103621742316c0cd62aec454f5c61 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 16:31:01 +0000 Subject: [PATCH 30/87] Swapping strings for substrings --- dlna/contenddirectoryservice.go | 181 +++++++++++++++++--------------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 88aeb68f2..d8635de8f 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -77,99 +77,108 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is // Returns all the upnpav objects in a directory. func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { - log.Info(fmt.Sprintf("ReadContainer called with : %+v", o)) + log.Debug(fmt.Sprintf("ReadContainer called '%s'", o)) - //TODO implement HTTP routing rather than this - switch o.Path { - case "/": + if(o.Path == "/" || o.Path == "") { + log.Debug("ReadContainer default route"); newObject := object{Path: "/Music"} thisObject, _ := cds.cdsObjectToUpnpavObject(newObject, true, host) ret = append(ret, thisObject) - case "/Music": - thisObject, _ := cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host) - ret = append(ret, thisObject) - case "/Music/Files": - files, _ := os.ReadDir(conf.Server.MusicFolder) - for _, file := range files { - child := object{ - path.Join(o.Path, file.Name()), - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) - ret = append(ret, convObj) - } - case "/Music/Artists": - 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 - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } - case "/Music/Albums": - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() - 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].Name), - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } - case "/Music/Genres": - indexes, err := cds.ds.Genre(cds.ctx).GetAll() - 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].Name), - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } - case "/Music/Playlists": - indexes, err := cds.ds.Playlist(cds.ctx).GetAll() - 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].Name), - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } + return ret, nil } + + pathComponents := strings.Split(o.Path, "/") + log.Debug(fmt.Sprintf("ReadContainer pathComponents %+v %d", pathComponents, len(pathComponents))) - if strings.HasPrefix(o.Path, "/Music/Files/") { - libraryPath, _ := strings.CutPrefix(o.Path, "/Music/Files") - files, _ := os.ReadDir(path.Join(conf.Server.MusicFolder, libraryPath)) - for _, file := range files { - child := object{ - path.Join(o.Path, file.Name()), + //TODO something other than this. + switch pathComponents[1] { + case "": + log.Fatal("This should never happen?"); + case "Music": + + if( len(pathComponents) == 2) { + thisObject, _ := cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host) + ret = append(ret, thisObject) + thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host) + ret = append(ret, thisObject) + return ret, nil + } + + switch pathComponents[2] { + case "Files": + files, _ := os.ReadDir(conf.Server.MusicFolder) + for _, file := range files { + child := object{ + path.Join(o.Path, file.Name()), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) + ret = append(ret, convObj) + return ret, nil } - convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) - ret = append(ret, convObj) + case "Artists": + 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 + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + return ret, nil + case "Albums": + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + return ret, nil + case "Genres": + indexes, err := cds.ds.Genre(cds.ctx).GetAll() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + return ret, nil + case "Playlists": + indexes, err := cds.ds.Playlist(cds.ctx).GetAll() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + return ret, nil } } return From 22b058f54fb9457febc6f1de58cdf129c376dab6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 17:33:46 +0000 Subject: [PATCH 31/87] OK file browsing and playback now works --- dlna/contenddirectoryservice.go | 173 +++++++++++++++++++------------- 1 file changed, 102 insertions(+), 71 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index d8635de8f..8d3c2c509 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -90,13 +90,11 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] pathComponents := strings.Split(o.Path, "/") log.Debug(fmt.Sprintf("ReadContainer pathComponents %+v %d", pathComponents, len(pathComponents))) - //TODO something other than this. - switch pathComponents[1] { - case "": - log.Fatal("This should never happen?"); - case "Music": - - if( len(pathComponents) == 2) { + //TODO: something other than this + switch(len(pathComponents)) { + case 2: + switch(pathComponents[1]) { + case "Music": thisObject, _ := cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host) ret = append(ret, thisObject) thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host) @@ -111,79 +109,112 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] ret = append(ret, thisObject) return ret, nil } - - switch pathComponents[2] { - case "Files": - files, _ := os.ReadDir(conf.Server.MusicFolder) - for _, file := range files { - child := object{ - path.Join(o.Path, file.Name()), + case 3: + switch(pathComponents[1]) { + case "Music": + switch(pathComponents[2]) { + case "Files": + return cds.doFiles(ret, o.Path, host) + case "Artists": + 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 + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + return ret, nil + case "Albums": + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + return ret, nil + case "Genres": + indexes, err := cds.ds.Genre(cds.ctx).GetAll() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) + } + return ret, nil + case "Playlists": + indexes, err := cds.ds.Playlist(cds.ctx).GetAll() + 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].Name), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) + ret = append(ret, convObj) } - convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) - ret = append(ret, convObj) return ret, nil } - case "Artists": - indexes, err := cds.ds.Artist(cds.ctx).GetIndex() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err + } + default: + if(len(pathComponents) >= 4) { + switch(pathComponents[2]) { + case "Files": + return cds.doFiles(ret, o.Path, host) + case "Artists": + + case "Albums": + + case "Genres": + + case "Playlists": + } - 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 - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } - return ret, nil - case "Albums": - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() - 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].Name), - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } - return ret, nil - case "Genres": - indexes, err := cds.ds.Genre(cds.ctx).GetAll() - 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].Name), - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } - return ret, nil - case "Playlists": - indexes, err := cds.ds.Playlist(cds.ctx).GetAll() - 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].Name), - } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) - } - return ret, nil } } + return } +func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, host string) ([]interface{}, error) { + pathUnderFiles := strings.TrimPrefix(oPath, "/Music/Files") + //TODO make not terrible + if(strings.Contains(pathUnderFiles, "/..")) { + return ret, nil + } + + pathComponents := strings.Split(pathUnderFiles, "/") + totalPathArrayBits := append([]string{conf.Server.MusicFolder}, pathComponents...) + localFilePath := filepath.Join(totalPathArrayBits...) + + files, _ := os.ReadDir(localFilePath) + for _, file := range files { + child := object{ + path.Join(oPath, file.Name()), + } + convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) + ret = append(ret, convObj) + } + return ret, nil +} + type browse struct { ObjectID string BrowseFlag string From 2be9d5c13492cf88698da5b1616210bef9996a28 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 17:38:05 +0000 Subject: [PATCH 32/87] Tightening up the .. and . protection a smidge --- dlna/contenddirectoryservice.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 8d3c2c509..8072880c6 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -10,6 +10,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "time" @@ -194,13 +195,11 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, host string) ([]interface{}, error) { - pathUnderFiles := strings.TrimPrefix(oPath, "/Music/Files") - //TODO make not terrible - if(strings.Contains(pathUnderFiles, "/..")) { + pathComponents := strings.Split(strings.TrimPrefix(oPath, "/Music/Files"), "/") + if(slices.Contains(pathComponents, "..") || slices.Contains(pathComponents, ".")) { + log.Error("Attempt to use .. or . detected", oPath, host) return ret, nil } - - pathComponents := strings.Split(pathUnderFiles, "/") totalPathArrayBits := append([]string{conf.Server.MusicFolder}, pathComponents...) localFilePath := filepath.Join(totalPathArrayBits...) From 788f3b3c5866f1abad982a953266a0f567fb0460 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 17:43:47 +0000 Subject: [PATCH 33/87] err never used, simplifiers appending --- dlna/contenddirectoryservice.go | 44 ++++++++++++--------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 8072880c6..2139837bf 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -32,7 +32,7 @@ func (cds *contentDirectoryService) updateIDString() string { // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. -func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, isContainer bool, host string) (ret interface{}, err error) { +func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, isContainer bool, host string) (ret interface{}) { obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, @@ -46,7 +46,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is return upnpav.Container{ Object: obj, ChildCount: &defaultChildCount, - }, nil + } } // Read the mime type from the fs.Object if possible, // otherwise fall back to working out what it is from the file path. @@ -73,7 +73,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is }) ret = item - return + return ret } // Returns all the upnpav objects in a directory. @@ -83,8 +83,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] if(o.Path == "/" || o.Path == "") { log.Debug("ReadContainer default route"); newObject := object{Path: "/Music"} - thisObject, _ := cds.cdsObjectToUpnpavObject(newObject, true, host) - ret = append(ret, thisObject) + ret = append(ret, cds.cdsObjectToUpnpavObject(newObject, true, host)) return ret, nil } @@ -96,18 +95,12 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] case 2: switch(pathComponents[1]) { case "Music": - thisObject, _ := cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host) - ret = append(ret, thisObject) - thisObject, _ = cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host) - ret = append(ret, thisObject) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host)) return ret, nil } case 3: @@ -126,8 +119,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] child := object{ 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) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil case "Albums": @@ -140,8 +132,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] child := object{ path.Join(o.Path, indexes[indexItem].Name), } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil case "Genres": @@ -154,8 +145,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] child := object{ path.Join(o.Path, indexes[indexItem].Name), } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil case "Playlists": @@ -168,8 +158,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] child := object{ path.Join(o.Path, indexes[indexItem].Name), } - convObj, _ := cds.cdsObjectToUpnpavObject(child, true, host) - ret = append(ret, convObj) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil } @@ -180,7 +169,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] case "Files": return cds.doFiles(ret, o.Path, host) case "Artists": - + case "Albums": case "Genres": @@ -208,8 +197,7 @@ func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, hos child := object{ path.Join(oPath, file.Name()), } - convObj, _ := cds.cdsObjectToUpnpavObject(child, file.IsDir(), host) - ret = append(ret, convObj) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, file.IsDir(), host)) } return ret, nil } From c3a606fb67872b6c1d13afe9b2f4d4d8f22ac0de Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 18:12:10 +0000 Subject: [PATCH 34/87] We need to be able to pass the artist and album etc id rather than the name, so we need to expose that back on the SOAP interface --- dlna/contenddirectoryservice.go | 38 ++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 2139837bf..f1c5180ab 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -111,15 +111,20 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return cds.doFiles(ret, o.Path, host) case "Artists": indexes, err := cds.ds.Artist(cds.ctx).GetIndex() + log.Debug(fmt.Sprintf("Artist indexes: %+v", indexes)) 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 + for letterIndex := range indexes { + for artist := range indexes[letterIndex].Artists { + artistId := indexes[letterIndex].Artists[artist].ID + child := object{ + Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), + Id: artistId, + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil case "Albums": @@ -130,7 +135,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } for indexItem := range indexes { child := object{ - path.Join(o.Path, indexes[indexItem].Name), + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: indexes[indexItem].ID, } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -143,7 +149,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } for indexItem := range indexes { child := object{ - path.Join(o.Path, indexes[indexItem].Name), + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: indexes[indexItem].ID, } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -156,7 +163,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } for indexItem := range indexes { child := object{ - path.Join(o.Path, indexes[indexItem].Name), + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: indexes[indexItem].ID, } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -169,13 +177,17 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] case "Files": return cds.doFiles(ret, o.Path, host) case "Artists": - + x, xerr := cds.ds.Artist(cds.ctx).Get(pathComponents[3]) + log.Debug(x, xerr) case "Albums": - + x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) + log.Debug(x, xerr) case "Genres": - + x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) + log.Debug(x, xerr) case "Playlists": - + x, xerr := cds.ds.Playlist(cds.ctx).Get(pathComponents[3]) + log.Debug(x, xerr) } } } @@ -195,7 +207,8 @@ func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, hos files, _ := os.ReadDir(localFilePath) for _, file := range files { child := object{ - path.Join(oPath, file.Name()), + Path: path.Join(oPath, file.Name()), + Id: path.Join(oPath, file.Name()), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, file.IsDir(), host)) } @@ -311,6 +324,7 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt // Represents a ContentDirectory object. type object struct { Path string // The cleaned, absolute path for the object relative to the server. + Id string } // Returns the actual local filesystem path for the object. From 17110e8c7e64c53416ae97d8090076bdab907b30 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 18:43:41 +0000 Subject: [PATCH 35/87] routing through to all the artists repo --- dlna/contenddirectoryservice.go | 45 +++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index f1c5180ab..2032767ae 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -14,11 +14,13 @@ import ( "strings" "time" + "github.com/Masterminds/squirrel" "github.com/anacrolix/dms/dlna" "github.com/anacrolix/dms/upnp" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/dlna/upnpav" "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" ) type contentDirectoryService struct { @@ -111,7 +113,6 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return cds.doFiles(ret, o.Path, host) case "Artists": indexes, err := cds.ds.Artist(cds.ctx).GetIndex() - log.Debug(fmt.Sprintf("Artist indexes: %+v", indexes)) if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) return nil, err @@ -121,7 +122,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] artistId := indexes[letterIndex].Artists[artist].ID child := object{ Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), - Id: artistId, + Id: path.Join(o.Path, artistId), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -136,7 +137,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] for indexItem := range indexes { child := object{ Path: path.Join(o.Path, indexes[indexItem].Name), - Id: indexes[indexItem].ID, + Id: path.Join(o.Path, indexes[indexItem].ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -150,7 +151,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] for indexItem := range indexes { child := object{ Path: path.Join(o.Path, indexes[indexItem].Name), - Id: indexes[indexItem].ID, + Id: path.Join(o.Path, indexes[indexItem].ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -164,7 +165,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] for indexItem := range indexes { child := object{ Path: path.Join(o.Path, indexes[indexItem].Name), - Id: indexes[indexItem].ID, + Id: path.Join(o.Path, indexes[indexItem].ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -172,22 +173,43 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } } default: + +/* + deluan + — +Today at 18:30 +ds.Album(ctx).GetAll(FIlter: Eq{"albumArtistId": artistID}) +Or something like that 😛 +Mintsoft + — +Today at 18:30 +For other examples, how do I know what the right magic string for "albumArtistId" is? +kgarner7 + — +Today at 18:31 +album_artist_id +Look at the model structs names +deluan + — +Today at 18:31 +This is a limitation of Squirrel. It is string based. YOu have to use the name of the columns in the DB + */ if(len(pathComponents) >= 4) { switch(pathComponents[2]) { case "Files": return cds.doFiles(ret, o.Path, host) case "Artists": - x, xerr := cds.ds.Artist(cds.ctx).Get(pathComponents[3]) - log.Debug(x, xerr) + allAlbumsForThisArtist, getErr := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": pathComponents[3]}}) + log.Debug(fmt.Sprintf("AllAlbums: %+v", allAlbumsForThisArtist),getErr) case "Albums": x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) - log.Debug(x, xerr) + log.Debug(fmt.Sprintf("Album: %+v", x), xerr) case "Genres": x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) - log.Debug(x, xerr) + log.Debug(fmt.Sprintf("Genre: %+v", x), xerr) case "Playlists": x, xerr := cds.ds.Playlist(cds.ctx).Get(pathComponents[3]) - log.Debug(x, xerr) + log.Debug(fmt.Sprintf("Playlist: %+v", x), xerr) } } } @@ -334,6 +356,9 @@ func (o *object) FilePath() string { // Returns the ObjectID for the object. This is used in various ContentDirectory actions. func (o object) ID() string { + if o.Id != "" { + return o.Id + } if !path.IsAbs(o.Path) { log.Fatal(fmt.Sprintf("Relative object path used with ID: $s", o.Path)) } From 7fedc2c1dd7c504665311ade7ae5003cd89a81ac Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 18:51:16 +0000 Subject: [PATCH 36/87] Loginfo -> log debug --- dlna/contenddirectoryservice.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 2032767ae..2d5367d38 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -201,6 +201,7 @@ This is a limitation of Squirrel. It is string based. YOu have to use the name o case "Artists": allAlbumsForThisArtist, getErr := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": pathComponents[3]}}) log.Debug(fmt.Sprintf("AllAlbums: %+v", allAlbumsForThisArtist),getErr) + case "Albums": x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) log.Debug(fmt.Sprintf("Album: %+v", x), xerr) @@ -247,7 +248,7 @@ type browse struct { // ContentDirectory object from ObjectID. func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) { - log.Info(fmt.Sprintf("objectFromID called with : %+v", id)) + log.Debug("objectFromID called","id", id) o.Path, err = url.QueryUnescape(id) if err != nil { From 42bae7e924d61980c4455f551f10feac129e5dda Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 12 Jan 2025 17:39:45 +0000 Subject: [PATCH 37/87] albums under artist --- dlna/contenddirectoryservice.go | 105 ++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 2d5367d38..444dd665f 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -82,21 +82,21 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { log.Debug(fmt.Sprintf("ReadContainer called '%s'", o)) - if(o.Path == "/" || o.Path == "") { - log.Debug("ReadContainer default route"); + if o.Path == "/" || o.Path == "" { + log.Debug("ReadContainer default route") newObject := object{Path: "/Music"} ret = append(ret, cds.cdsObjectToUpnpavObject(newObject, true, host)) - return ret, nil + return ret, nil } - + pathComponents := strings.Split(o.Path, "/") log.Debug(fmt.Sprintf("ReadContainer pathComponents %+v %d", pathComponents, len(pathComponents))) //TODO: something other than this - switch(len(pathComponents)) { + switch len(pathComponents) { case 2: - switch(pathComponents[1]) { - case "Music": + switch pathComponents[1] { + case "Music": ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host)) @@ -106,9 +106,9 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } case 3: - switch(pathComponents[1]) { + switch pathComponents[1] { case "Music": - switch(pathComponents[2]) { + switch pathComponents[2] { case "Files": return cds.doFiles(ret, o.Path, host) case "Artists": @@ -121,8 +121,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] for artist := range indexes[letterIndex].Artists { artistId := indexes[letterIndex].Artists[artist].ID child := object{ - Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), - Id: path.Join(o.Path, artistId), + Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), + Id: path.Join(o.Path, artistId), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -137,7 +137,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] for indexItem := range indexes { child := object{ Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), + Id: path.Join(o.Path, indexes[indexItem].ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -151,7 +151,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] for indexItem := range indexes { child := object{ Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), + Id: path.Join(o.Path, indexes[indexItem].ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -165,7 +165,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] for indexItem := range indexes { child := object{ Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), + Id: path.Join(o.Path, indexes[indexItem].ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } @@ -174,37 +174,36 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } default: -/* - deluan - — -Today at 18:30 -ds.Album(ctx).GetAll(FIlter: Eq{"albumArtistId": artistID}) -Or something like that 😛 -Mintsoft - — -Today at 18:30 -For other examples, how do I know what the right magic string for "albumArtistId" is? -kgarner7 - — -Today at 18:31 -album_artist_id -Look at the model structs names -deluan - — -Today at 18:31 -This is a limitation of Squirrel. It is string based. YOu have to use the name of the columns in the DB + /* + deluan + — + Today at 18:30 + ds.Album(ctx).GetAll(FIlter: Eq{"albumArtistId": artistID}) + Or something like that 😛 + Mintsoft + — + Today at 18:30 + For other examples, how do I know what the right magic string for "albumArtistId" is? + kgarner7 + — + Today at 18:31 + album_artist_id + Look at the model structs names + deluan + — + Today at 18:31 + This is a limitation of Squirrel. It is string based. YOu have to use the name of the columns in the DB */ - if(len(pathComponents) >= 4) { - switch(pathComponents[2]) { + if len(pathComponents) >= 4 { + switch pathComponents[2] { case "Files": return cds.doFiles(ret, o.Path, host) case "Artists": - allAlbumsForThisArtist, getErr := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": pathComponents[3]}}) - log.Debug(fmt.Sprintf("AllAlbums: %+v", allAlbumsForThisArtist),getErr) - + allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": pathComponents[3]}}) + return cds.doAlbums(allAlbumsForThisArtist, ret, host) case "Albums": - x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) - log.Debug(fmt.Sprintf("Album: %+v", x), xerr) + x, _ := cds.ds.Album(cds.ctx).Get(pathComponents[3]) + return cds.doAlbum(x, ret, host) case "Genres": x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) log.Debug(fmt.Sprintf("Genre: %+v", x), xerr) @@ -218,20 +217,36 @@ This is a limitation of Squirrel. It is string based. YOu have to use the name o return } +func (cds *contentDirectoryService) doAlbum(x *model.Album, ret []interface{}, host string) ([]interface{}, error) { + //TODO + return ret, nil +} + +func (cds *contentDirectoryService) doAlbums(albums model.Albums, ret []interface{}, host string) ([]interface{}, error) { + for _, album := range albums { + child := object { + Path: path.Join("/Music/Albums", album.Name), + Id: album.ID, + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil +} + func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, host string) ([]interface{}, error) { pathComponents := strings.Split(strings.TrimPrefix(oPath, "/Music/Files"), "/") - if(slices.Contains(pathComponents, "..") || slices.Contains(pathComponents, ".")) { + if slices.Contains(pathComponents, "..") || slices.Contains(pathComponents, ".") { log.Error("Attempt to use .. or . detected", oPath, host) return ret, nil } totalPathArrayBits := append([]string{conf.Server.MusicFolder}, pathComponents...) localFilePath := filepath.Join(totalPathArrayBits...) - + files, _ := os.ReadDir(localFilePath) for _, file := range files { child := object{ Path: path.Join(oPath, file.Name()), - Id: path.Join(oPath, file.Name()), + Id: path.Join(oPath, file.Name()), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, file.IsDir(), host)) } @@ -248,7 +263,7 @@ type browse struct { // ContentDirectory object from ObjectID. func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) { - log.Debug("objectFromID called","id", id) + log.Debug("objectFromID called", "id", id) o.Path, err = url.QueryUnescape(id) if err != nil { @@ -347,7 +362,7 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt // Represents a ContentDirectory object. type object struct { Path string // The cleaned, absolute path for the object relative to the server. - Id string + Id string } // Returns the actual local filesystem path for the object. From 4960f206b4d24224f130d314f34fec5a9999dec4 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 17:23:44 +0000 Subject: [PATCH 38/87] Rough regex routing working, needs .. much improvement also. Is this better than before? I dunno --- dlna/contenddirectoryservice.go | 245 ++++++++++++++++++-------------- go.mod | 1 + go.sum | 8 ++ 3 files changed, 148 insertions(+), 106 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 444dd665f..f2da92029 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -21,6 +21,7 @@ import ( "github.com/navidrome/navidrome/dlna/upnpav" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/oriser/regroup" ) type contentDirectoryService struct { @@ -89,91 +90,143 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } - pathComponents := strings.Split(o.Path, "/") - log.Debug(fmt.Sprintf("ReadContainer pathComponents %+v %d", pathComponents, len(pathComponents))) + filesRegex := regroup.MustCompile("\\/Music\\/Files[\\/]?((?P.+))?") + artistRegex := regroup.MustCompile("\\/Music\\/Artists[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?[\\/]?(?[^\\/]+)?") + albumRegex := regroup.MustCompile("\\/Music\\/Albums[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?") + genresRegex := regroup.MustCompile("\\/Music\\/Genres[\\/]?(?P[^\\/]+)?[\\/]?(?P[^/]+)?[\\/]?(?P[^\\/]+)?") + recentRegex := regroup.MustCompile("\\/Music\\/Recently Added[\\/]?(?P[^\\/]+)?") + playlistRegex := regroup.MustCompile("\\/Music\\/Playlist[\\/]?(?P[^\\/]+)?[\\/]?(?P[^\\/]+)?") - //TODO: something other than this - switch len(pathComponents) { - case 2: - switch pathComponents[1] { - case "Music": - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host)) + if o.Path == "/Music" { + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host)) + return ret, nil + } else if _, err := filesRegex.Groups(o.Path); err == nil { + return cds.doFiles(ret, o.Path, host) + } else if matchResults, err := artistRegex.Groups(o.Path); err == nil { + log.Debug(fmt.Sprintf("Artist MATCH: %+v", matchResults)) + if matchResults["ArtistAlbumTrack"] != "" { + //TODO + log.Debug("Artist Get a track ") + } else if matchResults["ArtistAlbum"] != "" { + log.Debug("Artist Get an album ") + album := matchResults["ArtistAlbum"] + + albumResponse, _ := cds.ds.Album(cds.ctx).Get(album) + log.Debug(fmt.Sprintf("Album Returned: %+v for %s", albumResponse, album)) + basePath := path.Join("/Music/Artists", matchResults["Artist"], matchResults["ArtistAlbum"]) + return cds.doAlbum(albumResponse, basePath, ret, host) + + } else if matchResults["Artist"] != "" { + log.Debug(fmt.Sprintf("Artist Get an Artist: %s", matchResults["Artist"])) + allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) + basePath := path.Join("/Music/Artists", matchResults["Artist"]) + return cds.doAlbums(allAlbumsForThisArtist, basePath, ret, host) + + } else { + indexes, err := cds.ds.Artist(cds.ctx).GetIndex() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for letterIndex := range indexes { + for artist := range indexes[letterIndex].Artists { + artistId := indexes[letterIndex].Artists[artist].ID + child := object{ + Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), + Id: path.Join(o.Path, artistId), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + } return ret, nil } - case 3: - switch pathComponents[1] { - case "Music": - switch pathComponents[2] { - case "Files": - return cds.doFiles(ret, o.Path, host) - case "Artists": - indexes, err := cds.ds.Artist(cds.ctx).GetIndex() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for letterIndex := range indexes { - for artist := range indexes[letterIndex].Artists { - artistId := indexes[letterIndex].Artists[artist].ID - child := object{ - Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), - Id: path.Join(o.Path, artistId), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - } - return ret, nil - case "Albums": - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil - case "Genres": - indexes, err := cds.ds.Genre(cds.ctx).GetAll() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil - case "Playlists": - indexes, err := cds.ds.Playlist(cds.ctx).GetAll() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil + } else if matchResults, err := albumRegex.Groups(o.Path); err == nil { + log.Debug("Album MATCH") + if matchResults["AlbumTrack"] != "" { + log.Debug("AlbumTrack MATCH") + //TODO + } else if matchResults["AlbumTitle"] != "" { + log.Debug("AlbumTitle MATCH") + x, _ := cds.ds.Album(cds.ctx).Get(matchResults["AlbumTitle"]) + basePath := "/Music/Albums" + return cds.doAlbum(x, basePath, ret, host) + } else { + log.Debug("albumRegex else MATCH") + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil } - default: - + } else if matchResults, err := genresRegex.Groups(o.Path); err == nil { + log.Debug("Genre MATCH") + if _, exists := matchResults["GenreTrack"]; exists { + log.Debug("GenreTrack MATCH") + //TODO + } else if _, exists := matchResults["GenreArtist"]; exists { + log.Debug("GenreArtist MATCH") + //TODO + } else if genre, exists := matchResults["Genre"]; exists { + log.Debug("Genre only MATCH") + x, xerr := cds.ds.Album(cds.ctx).Get(genre) + log.Debug(fmt.Sprintf("Genre: %+v", x), xerr) + } else { + log.Debug("Genre else MATCH") + indexes, err := cds.ds.Genre(cds.ctx).GetAll() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil + } + } else if matchResults, err := recentRegex.Groups(o.Path); err == nil { + log.Debug("recent MATCH") + fmt.Printf("%+v",matchResults) + } else if matchResults, err := playlistRegex.Groups(o.Path); err == nil { + log.Debug("Playlist MATCH") + if _, exists := matchResults["PlaylistTrack"]; exists { + log.Debug("PlaylistTrack MATCH") + } else if playlist, exists := matchResults["Playlist"]; exists { + log.Debug("Playlist only MATCH") + x, xerr := cds.ds.Playlist(cds.ctx).Get(playlist) + log.Debug(fmt.Sprintf("Playlist: %+v", x), xerr) + } else { + log.Debug("Playlist else MATCH") + indexes, err := cds.ds.Playlist(cds.ctx).GetAll() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil + } + } /* deluan — @@ -194,39 +247,19 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] Today at 18:31 This is a limitation of Squirrel. It is string based. YOu have to use the name of the columns in the DB */ - if len(pathComponents) >= 4 { - switch pathComponents[2] { - case "Files": - return cds.doFiles(ret, o.Path, host) - case "Artists": - allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": pathComponents[3]}}) - return cds.doAlbums(allAlbumsForThisArtist, ret, host) - case "Albums": - x, _ := cds.ds.Album(cds.ctx).Get(pathComponents[3]) - return cds.doAlbum(x, ret, host) - case "Genres": - x, xerr := cds.ds.Album(cds.ctx).Get(pathComponents[3]) - log.Debug(fmt.Sprintf("Genre: %+v", x), xerr) - case "Playlists": - x, xerr := cds.ds.Playlist(cds.ctx).Get(pathComponents[3]) - log.Debug(fmt.Sprintf("Playlist: %+v", x), xerr) - } - } - } - - return + return } -func (cds *contentDirectoryService) doAlbum(x *model.Album, ret []interface{}, host string) ([]interface{}, error) { - //TODO +func (cds *contentDirectoryService) doAlbum(album *model.Album, basepath string, ret []interface{}, host string) ([]interface{}, error) { + log.Debug(fmt.Sprintf("TODO: doAlbum Called with : '%+v', '%s'", album, basepath)) return ret, nil } -func (cds *contentDirectoryService) doAlbums(albums model.Albums, ret []interface{}, host string) ([]interface{}, error) { +func (cds *contentDirectoryService) doAlbums(albums model.Albums, basepath string, ret []interface{}, host string) ([]interface{}, error) { for _, album := range albums { child := object { - Path: path.Join("/Music/Albums", album.Name), - Id: album.ID, + Path: path.Join(basepath, album.Name), + Id: path.Join(basepath, album.ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } diff --git a/go.mod b/go.mod index 383aaee53..f8c3a93c1 100644 --- a/go.mod +++ b/go.mod @@ -121,6 +121,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ogier/pflag v0.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect diff --git a/go.sum b/go.sum index 47f2211e3..3dfb10ac2 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/anacrolix/generics v0.0.1 h1:4WVhK6iLb3UAAAQP6I3uYlMOHcp9FqJC9j4n81Wv github.com/anacrolix/generics v0.0.1/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= github.com/anacrolix/log v0.15.2 h1:LTSf5Wm6Q4GNWPFMBP7NPYV6UBVZzZLKckL+/Lj72Oo= github.com/anacrolix/log v0.15.2/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -207,6 +209,12 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e h1:cL0lMYYEbfEUBghQd4ytnl8B8Ktdm+JremTyAagegZ0= +github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e/go.mod h1:tUOeYZJlwO7jSmM5ko1jTCiQaWQMvh58IENEfjwYzh8= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= From e4bad316c338e01b35f706deed436569ec0bd7af Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 17:44:57 +0000 Subject: [PATCH 39/87] Rendering out leaf nodes for tracks --- dlna/contenddirectoryservice.go | 75 ++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index f2da92029..896737729 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -119,7 +119,10 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] albumResponse, _ := cds.ds.Album(cds.ctx).Get(album) log.Debug(fmt.Sprintf("Album Returned: %+v for %s", albumResponse, album)) basePath := path.Join("/Music/Artists", matchResults["Artist"], matchResults["ArtistAlbum"]) - return cds.doAlbum(albumResponse, basePath, ret, host) + + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": albumResponse.ID}}) + + return cds.doMediaFiles(tracks, basePath, ret, host) } else if matchResults["Artist"] != "" { log.Debug(fmt.Sprintf("Artist Get an Artist: %s", matchResults["Artist"])) @@ -152,9 +155,10 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] //TODO } else if matchResults["AlbumTitle"] != "" { log.Debug("AlbumTitle MATCH") - x, _ := cds.ds.Album(cds.ctx).Get(matchResults["AlbumTitle"]) - basePath := "/Music/Albums" - return cds.doAlbum(x, basePath, ret, host) + albumResponse, _ := cds.ds.Album(cds.ctx).Get(matchResults["AlbumTitle"]) + basePath := path.Join("/Music/Albums", matchResults["AlbumTitle"]) + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": albumResponse.ID}}) + return cds.doMediaFiles(tracks, basePath, ret, host) } else { log.Debug("albumRegex else MATCH") indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() @@ -201,7 +205,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } } else if matchResults, err := recentRegex.Groups(o.Path); err == nil { log.Debug("recent MATCH") - fmt.Printf("%+v",matchResults) + fmt.Printf("%+v", matchResults) } else if matchResults, err := playlistRegex.Groups(o.Path); err == nil { log.Debug("Playlist MATCH") if _, exists := matchResults["PlaylistTrack"]; exists { @@ -209,7 +213,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } else if playlist, exists := matchResults["Playlist"]; exists { log.Debug("Playlist only MATCH") x, xerr := cds.ds.Playlist(cds.ctx).Get(playlist) - log.Debug(fmt.Sprintf("Playlist: %+v", x), xerr) + log.Debug(fmt.Sprintf("Playlist: %+v", x), xerr) } else { log.Debug("Playlist else MATCH") indexes, err := cds.ds.Playlist(cds.ctx).GetAll() @@ -227,27 +231,40 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } - /* - deluan - — - Today at 18:30 - ds.Album(ctx).GetAll(FIlter: Eq{"albumArtistId": artistID}) - Or something like that 😛 - Mintsoft - — - Today at 18:30 - For other examples, how do I know what the right magic string for "albumArtistId" is? - kgarner7 - — - Today at 18:31 - album_artist_id - Look at the model structs names - deluan - — - Today at 18:31 - This is a limitation of Squirrel. It is string based. YOu have to use the name of the columns in the DB - */ - return + /* + deluan + — + Today at 18:30 + ds.Album(ctx).GetAll(FIlter: Eq{"albumArtistId": artistID}) + Or something like that 😛 + Mintsoft + — + Today at 18:30 + For other examples, how do I know what the right magic string for "albumArtistId" is? + kgarner7 + — + Today at 18:31 + album_artist_id + Look at the model structs names + deluan + — + Today at 18:31 + This is a limitation of Squirrel. It is string based. YOu have to use the name of the columns in the DB + */ + return +} + +func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePath string, ret []interface{}, host string) ([]interface{}, error) { + //TODO flesh object out with actually useful metadata about the track + for _, track := range tracks { + child := object{ + Path: path.Join(basePath, track.Title), + Id: path.Join(basePath, track.ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, false, host)) + } + + return ret, nil } func (cds *contentDirectoryService) doAlbum(album *model.Album, basepath string, ret []interface{}, host string) ([]interface{}, error) { @@ -257,9 +274,9 @@ func (cds *contentDirectoryService) doAlbum(album *model.Album, basepath string, func (cds *contentDirectoryService) doAlbums(albums model.Albums, basepath string, ret []interface{}, host string) ([]interface{}, error) { for _, album := range albums { - child := object { + child := object{ Path: path.Join(basepath, album.Name), - Id: path.Join(basepath, album.ID), + Id: path.Join(basepath, album.ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } From b7a765b19a4967457f01763d6b5d2f05b80b82f3 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 18:09:33 +0000 Subject: [PATCH 40/87] Cleaning up a bit --- dlna/contenddirectoryservice.go | 78 ++++++++++----------------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 896737729..902cc570b 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -108,26 +108,22 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } else if _, err := filesRegex.Groups(o.Path); err == nil { return cds.doFiles(ret, o.Path, host) } else if matchResults, err := artistRegex.Groups(o.Path); err == nil { - log.Debug(fmt.Sprintf("Artist MATCH: %+v", matchResults)) if matchResults["ArtistAlbumTrack"] != "" { //TODO log.Debug("Artist Get a track ") } else if matchResults["ArtistAlbum"] != "" { - log.Debug("Artist Get an album ") - album := matchResults["ArtistAlbum"] - - albumResponse, _ := cds.ds.Album(cds.ctx).Get(album) - log.Debug(fmt.Sprintf("Album Returned: %+v for %s", albumResponse, album)) basePath := path.Join("/Music/Artists", matchResults["Artist"], matchResults["ArtistAlbum"]) - tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": albumResponse.ID}}) + //albumResponse, _ := cds.ds.Album(cds.ctx).Get(matchResults["ArtistAlbum"]) + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) return cds.doMediaFiles(tracks, basePath, ret, host) } else if matchResults["Artist"] != "" { - log.Debug(fmt.Sprintf("Artist Get an Artist: %s", matchResults["Artist"])) - allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) basePath := path.Join("/Music/Artists", matchResults["Artist"]) + + allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) + return cds.doAlbums(allAlbumsForThisArtist, basePath, ret, host) } else { @@ -149,18 +145,15 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } else if matchResults, err := albumRegex.Groups(o.Path); err == nil { - log.Debug("Album MATCH") if matchResults["AlbumTrack"] != "" { - log.Debug("AlbumTrack MATCH") - //TODO + log.Debug("TODO AlbumTrack MATCH") } else if matchResults["AlbumTitle"] != "" { - log.Debug("AlbumTitle MATCH") - albumResponse, _ := cds.ds.Album(cds.ctx).Get(matchResults["AlbumTitle"]) basePath := path.Join("/Music/Albums", matchResults["AlbumTitle"]) - tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": albumResponse.ID}}) + + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) + return cds.doMediaFiles(tracks, basePath, ret, host) } else { - log.Debug("albumRegex else MATCH") indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) @@ -177,18 +170,14 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } } else if matchResults, err := genresRegex.Groups(o.Path); err == nil { log.Debug("Genre MATCH") - if _, exists := matchResults["GenreTrack"]; exists { - log.Debug("GenreTrack MATCH") - //TODO - } else if _, exists := matchResults["GenreArtist"]; exists { - log.Debug("GenreArtist MATCH") - //TODO - } else if genre, exists := matchResults["Genre"]; exists { - log.Debug("Genre only MATCH") - x, xerr := cds.ds.Album(cds.ctx).Get(genre) - log.Debug(fmt.Sprintf("Genre: %+v", x), xerr) + if matchResults["GenreTrack"] != "" { + log.Debug("TODO GenreTrack MATCH") + } else if matchResults["GenreArtist"] != "" { + log.Debug("TODO GenreArtist MATCH") + } else if matchResults["Genre"] != "" { + //x, xerr := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{}}) + log.Debug("TODO Get albums for Genre X") } else { - log.Debug("Genre else MATCH") indexes, err := cds.ds.Genre(cds.ctx).GetAll() if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) @@ -204,16 +193,16 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } else if matchResults, err := recentRegex.Groups(o.Path); err == nil { - log.Debug("recent MATCH") + log.Debug("TODO recent MATCH") fmt.Printf("%+v", matchResults) } else if matchResults, err := playlistRegex.Groups(o.Path); err == nil { - log.Debug("Playlist MATCH") - if _, exists := matchResults["PlaylistTrack"]; exists { - log.Debug("PlaylistTrack MATCH") - } else if playlist, exists := matchResults["Playlist"]; exists { + log.Debug("TODO Playlist MATCH") + if matchResults["PlaylistTrack"] != "" { + log.Debug("TODO PlaylistTrack MATCH") + } else if matchResults["Playlist"] != "" { log.Debug("Playlist only MATCH") - x, xerr := cds.ds.Playlist(cds.ctx).Get(playlist) - log.Debug(fmt.Sprintf("Playlist: %+v", x), xerr) + x, xerr := cds.ds.Playlist(cds.ctx).Get(matchResults["Playlist"]) + log.Debug(fmt.Sprintf("TODO Playlist: %+v", x), xerr) } else { log.Debug("Playlist else MATCH") indexes, err := cds.ds.Playlist(cds.ctx).GetAll() @@ -231,26 +220,6 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } - /* - deluan - — - Today at 18:30 - ds.Album(ctx).GetAll(FIlter: Eq{"albumArtistId": artistID}) - Or something like that 😛 - Mintsoft - — - Today at 18:30 - For other examples, how do I know what the right magic string for "albumArtistId" is? - kgarner7 - — - Today at 18:31 - album_artist_id - Look at the model structs names - deluan - — - Today at 18:31 - This is a limitation of Squirrel. It is string based. YOu have to use the name of the columns in the DB - */ return } @@ -269,6 +238,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa func (cds *contentDirectoryService) doAlbum(album *model.Album, basepath string, ret []interface{}, host string) ([]interface{}, error) { log.Debug(fmt.Sprintf("TODO: doAlbum Called with : '%+v', '%s'", album, basepath)) + panic("doAlbum Called!") return ret, nil } From c360f248f7017de41139147d9e3661c3edd1abe1 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 18:16:48 +0000 Subject: [PATCH 41/87] Might as well compile these once and only once --- dlna/contenddirectoryservice.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 902cc570b..b7865ff72 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -29,6 +29,22 @@ type contentDirectoryService struct { upnp.Eventing } +var filesRegex *regroup.ReGroup +var artistRegex *regroup.ReGroup +var albumRegex *regroup.ReGroup +var genresRegex *regroup.ReGroup +var recentRegex *regroup.ReGroup +var playlistRegex *regroup.ReGroup + +func init() { + filesRegex = regroup.MustCompile("\\/Music\\/Files[\\/]?((?P.+))?") + artistRegex = regroup.MustCompile("\\/Music\\/Artists[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?[\\/]?(?[^\\/]+)?") + albumRegex = regroup.MustCompile("\\/Music\\/Albums[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?") + genresRegex = regroup.MustCompile("\\/Music\\/Genres[\\/]?(?P[^\\/]+)?[\\/]?(?P[^/]+)?[\\/]?(?P[^\\/]+)?") + recentRegex = regroup.MustCompile("\\/Music\\/Recently Added[\\/]?(?P[^\\/]+)?") + playlistRegex = regroup.MustCompile("\\/Music\\/Playlist[\\/]?(?P[^\\/]+)?[\\/]?(?P[^\\/]+)?") +} + func (cds *contentDirectoryService) updateIDString() string { return fmt.Sprintf("%d", uint32(os.Getpid())) } @@ -90,13 +106,6 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } - filesRegex := regroup.MustCompile("\\/Music\\/Files[\\/]?((?P.+))?") - artistRegex := regroup.MustCompile("\\/Music\\/Artists[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?[\\/]?(?[^\\/]+)?") - albumRegex := regroup.MustCompile("\\/Music\\/Albums[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?") - genresRegex := regroup.MustCompile("\\/Music\\/Genres[\\/]?(?P[^\\/]+)?[\\/]?(?P[^/]+)?[\\/]?(?P[^\\/]+)?") - recentRegex := regroup.MustCompile("\\/Music\\/Recently Added[\\/]?(?P[^\\/]+)?") - playlistRegex := regroup.MustCompile("\\/Music\\/Playlist[\\/]?(?P[^\\/]+)?[\\/]?(?P[^\\/]+)?") - if o.Path == "/Music" { ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) From eb341d11dc06a32fc02806bf9b517eb2bca3901b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 19 Jan 2025 18:00:50 +0000 Subject: [PATCH 42/87] Example XML blob of a track with metadata --- dlna/contenddirectoryservice.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index b7865ff72..9aff8a0ec 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -234,6 +234,21 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePath string, ret []interface{}, host string) ([]interface{}, error) { //TODO flesh object out with actually useful metadata about the track + /* + + Love Takes Time + object.item.audioItem.musicTrack + + Mariah Carey + 2000-01-01 + Mariah Carey + #1's + Pop + 2 + http://172.30.0.4:8200/MediaItems/17759.mp3 + http://172.30.0.4:8200/AlbumArt/24179-17759.jpg + + */ for _, track := range tracks { child := object{ Path: path.Join(basePath, track.Title), From ac84e1ca5a55ca6a57dff77b312e06bf4a50fbd2 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 19 Jan 2025 18:23:11 +0000 Subject: [PATCH 43/87] Some metadata returned --- dlna/contenddirectoryservice.go | 49 +++++++++++++++++++++++++++++---- dlna/upnpav/upnpav.go | 4 +-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 9aff8a0ec..af469df95 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -71,7 +71,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is // otherwise fall back to working out what it is from the file path. var mimeType = "audio/mp3" //TODO - obj.Class = "object.item.audioItem" + obj.Class = "object.item.audioItem.musicTrack" obj.Date = upnpav.Timestamp{Time: time.Now()} item := upnpav.Item{ @@ -237,16 +237,16 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa /* Love Takes Time - object.item.audioItem.musicTrack Mariah Carey 2000-01-01 - Mariah Carey + object.item.audioItem.musicTrack + Mariah Carey #1's Pop 2 - http://172.30.0.4:8200/MediaItems/17759.mp3 http://172.30.0.4:8200/AlbumArt/24179-17759.jpg + http://172.30.0.4:8200/MediaItems/17759.mp3 */ for _, track := range tracks { @@ -254,7 +254,46 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa Path: path.Join(basePath, track.Title), Id: path.Join(basePath, track.ID), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, false, host)) + title := track.Title + artist := track.Artist + //date := track.Date //TODO + album := track.Album + genre := track.Genre + trackNo := track.TrackNumber + + obj := upnpav.Object{ + ID: child.Id, + Restricted: 1, + ParentID: child.ParentID(), + Title: title, + } + + var mimeType = "audio/mp3" //TODO + + obj.Class = "object.item.audioItem.musicTrack" + obj.Date = upnpav.Timestamp{Time: time.Now()} + obj.Artist = artist + obj.Album = album + obj.Genre = genre + obj.OriginalTrackNumber = trackNo + + item := upnpav.Item{ + Object: obj, + Res: make([]upnpav.Resource, 0, 1), + } + + item.Res = append(item.Res, upnpav.Resource{ + URL: (&url.URL{ + Scheme: "http", + Host: host, + Path: path.Join(resPath, child.Path), + }).String(), + ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ + SupportRange: false, + }.String()), + Size: uint64(1048576), //TODO TRACKSIZE + }) + ret = append(ret, item) } return ret, nil diff --git a/dlna/upnpav/upnpav.go b/dlna/upnpav/upnpav.go index c6dc9dc4f..88790f1be 100644 --- a/dlna/upnpav/upnpav.go +++ b/dlna/upnpav/upnpav.go @@ -50,9 +50,9 @@ type Object struct { Album string `xml:"upnp:album,omitempty"` Genre string `xml:"upnp:genre,omitempty"` AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` - Searchable int `xml:"searchable,attr"` + Searchable int `xml:"searchable,attr"` // + OriginalTrackNumber int `xml:"originalTrackNumber,omitempty"` } - // Timestamp wraps time.Time for formatting purposes type Timestamp struct { time.Time From 980de390d8a749810f14cff675e8f14a6566c14a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 19 Jan 2025 19:27:14 +0000 Subject: [PATCH 44/87] Track numbers populated correctly --- dlna/contenddirectoryservice.go | 9 ++++++--- dlna/upnpav/upnpav.go | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index af469df95..c512928b0 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -11,6 +11,7 @@ import ( "path" "path/filepath" "slices" + "strconv" "strings" "time" @@ -256,10 +257,10 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa } title := track.Title artist := track.Artist - //date := track.Date //TODO album := track.Album genre := track.Genre trackNo := track.TrackNumber + trackDuration := strconv.FormatFloat(float64(track.Duration), 'f', -1, 64) obj := upnpav.Object{ ID: child.Id, @@ -271,7 +272,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa var mimeType = "audio/mp3" //TODO obj.Class = "object.item.audioItem.musicTrack" - obj.Date = upnpav.Timestamp{Time: time.Now()} + obj.Date = upnpav.Timestamp{Time:time.Now()} //TODO obj.Artist = artist obj.Album = album obj.Genre = genre @@ -291,7 +292,8 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: false, }.String()), - Size: uint64(1048576), //TODO TRACKSIZE + Size: uint64(track.Size), + Duration: trackDuration, }) ret = append(ret, item) } @@ -403,6 +405,7 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt objs = objs[:browse.RequestedCount] } result, err := xml.Marshal(objs) + log.Debug(fmt.Sprintf("XMLResponse: '%s'", result)) if err != nil { return nil, err } diff --git a/dlna/upnpav/upnpav.go b/dlna/upnpav/upnpav.go index 88790f1be..a3f5c15e4 100644 --- a/dlna/upnpav/upnpav.go +++ b/dlna/upnpav/upnpav.go @@ -50,8 +50,8 @@ type Object struct { Album string `xml:"upnp:album,omitempty"` Genre string `xml:"upnp:genre,omitempty"` AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` - Searchable int `xml:"searchable,attr"` // - OriginalTrackNumber int `xml:"originalTrackNumber,omitempty"` + OriginalTrackNumber int `xml:"upnp:originalTrackNumber,omitempty"` + Searchable int `xml:"searchable,attr"` } // Timestamp wraps time.Time for formatting purposes type Timestamp struct { From d56832577759ee325be25443dab10893a05f5eb3 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 16:35:06 +0000 Subject: [PATCH 45/87] OK can't find any library to do this, so had to do it by hand --- dlna/contenddirectoryservice.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index c512928b0..d75dc8cb4 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -5,13 +5,13 @@ package dlna import ( "encoding/xml" "fmt" + "math" "net/http" "net/url" "os" "path" "path/filepath" "slices" - "strconv" "strings" "time" @@ -260,7 +260,8 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa album := track.Album genre := track.Genre trackNo := track.TrackNumber - trackDuration := strconv.FormatFloat(float64(track.Duration), 'f', -1, 64) + + trackDurStr := floatToDurationString(track.Duration) obj := upnpav.Object{ ID: child.Id, @@ -293,7 +294,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa SupportRange: false, }.String()), Size: uint64(track.Size), - Duration: trackDuration, + Duration: trackDurStr, }) ret = append(ret, item) } @@ -301,6 +302,19 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa return ret, nil } +func floatToDurationString(totalSeconds32 float32) string { + totalSeconds := float64(totalSeconds32) + secondsInAnHour := float64(60*60) + secondsInAMinute := float64(60) + + hours := int(math.Floor(totalSeconds / secondsInAnHour)) + minutes := int(math.Floor(math.Mod(totalSeconds, secondsInAnHour) / secondsInAMinute)) + seconds := int(math.Floor(math.Mod(totalSeconds, secondsInAMinute))) + ms := int(math.Floor(math.Mod(totalSeconds,1) * 1000)) + + return fmt.Sprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, ms) +} + func (cds *contentDirectoryService) doAlbum(album *model.Album, basepath string, ret []interface{}, host string) ([]interface{}, error) { log.Debug(fmt.Sprintf("TODO: doAlbum Called with : '%+v', '%s'", album, basepath)) panic("doAlbum Called!") From 760bf95de3a69b3201235ceecf0c4be6a79339a6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 16:57:52 +0000 Subject: [PATCH 46/87] Now lists all artists for a given genre --- dlna/contenddirectoryservice.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index d75dc8cb4..98f581f93 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -185,8 +185,18 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } else if matchResults["GenreArtist"] != "" { log.Debug("TODO GenreArtist MATCH") } else if matchResults["Genre"] != "" { - //x, xerr := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{}}) - log.Debug("TODO Get albums for Genre X") + artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{ "genre.id": matchResults["Genre"]}}) + if err != nil { + fmt.Printf("Error retrieving artists for genre: %+v", err) + return nil, err + } + for artistIndex := range artists { + child := object{ + Path: path.Join(o.Path, artists[artistIndex].Name), + Id: path.Join(o.Path, artists[artistIndex].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } } else { indexes, err := cds.ds.Genre(cds.ctx).GetAll() if err != nil { From 657d2e6e6501133da0244272ed05bf2421d5abbf Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:07:18 +0000 Subject: [PATCH 47/87] Now returns all tracks for a given genre by a given artist --- dlna/contenddirectoryservice.go | 37 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 98f581f93..324aa249a 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -183,19 +183,38 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] if matchResults["GenreTrack"] != "" { log.Debug("TODO GenreTrack MATCH") } else if matchResults["GenreArtist"] != "" { - log.Debug("TODO GenreArtist MATCH") - } else if matchResults["Genre"] != "" { - artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{ "genre.id": matchResults["Genre"]}}) + tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ + squirrel.Eq{"genre.id": matchResults["Genre"],}, + squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, + }, + }) if err != nil { - fmt.Printf("Error retrieving artists for genre: %+v", err) + fmt.Printf("Error retrieving tracks for artist and genre: %+v", err) return nil, err } - for artistIndex := range artists { - child := object{ - Path: path.Join(o.Path, artists[artistIndex].Name), - Id: path.Join(o.Path, artists[artistIndex].ID), + + //TODO do the metadata and stuff here + for trackIndex := range tracks { + child := object { + Path: path.Join(o.Path, tracks[trackIndex].Title), + Id: path.Join(o.Path, tracks[trackIndex].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, false, host)) + } + } else if matchResults["Genre"] != "" { + if matchResults["GenreArtist"] == "" { + artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{ "genre.id": matchResults["Genre"]}}) + if err != nil { + fmt.Printf("Error retrieving artists for genre: %+v", err) + return nil, err + } + for artistIndex := range artists { + child := object{ + Path: path.Join(o.Path, artists[artistIndex].Name), + Id: path.Join(o.Path, artists[artistIndex].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } } else { indexes, err := cds.ds.Genre(cds.ctx).GetAll() From 99dfaf307d7d67f80f955a5f734f721f1378faf6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:13:01 +0000 Subject: [PATCH 48/87] Outputting a track object with metadata, we should use the trackid in the ID rather than the title --- dlna/contenddirectoryservice.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 324aa249a..a1f1313b9 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -192,7 +192,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] fmt.Printf("Error retrieving tracks for artist and genre: %+v", err) return nil, err } - + return cds.doMediaFiles(tracks, o.Path, ret, host) + /* //TODO do the metadata and stuff here for trackIndex := range tracks { child := object { @@ -200,7 +201,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] Id: path.Join(o.Path, tracks[trackIndex].ID), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, false, host)) - } + }*/ } else if matchResults["Genre"] != "" { if matchResults["GenreArtist"] == "" { artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{ "genre.id": matchResults["Genre"]}}) @@ -281,7 +282,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa */ for _, track := range tracks { child := object{ - Path: path.Join(basePath, track.Title), + Path: path.Join(basePath, track.ID), Id: path.Join(basePath, track.ID), } title := track.Title From 2c926106eb4dbbd02f38851c7a74297fe06958ed Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:49:21 +0000 Subject: [PATCH 49/87] o.Path is always the current urlpath, so we can use that instead of reassembling --- dlna/contenddirectoryservice.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index a1f1313b9..9fa4d3b0b 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -122,19 +122,14 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] //TODO log.Debug("Artist Get a track ") } else if matchResults["ArtistAlbum"] != "" { - basePath := path.Join("/Music/Artists", matchResults["Artist"], matchResults["ArtistAlbum"]) - - //albumResponse, _ := cds.ds.Album(cds.ctx).Get(matchResults["ArtistAlbum"]) tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) - return cds.doMediaFiles(tracks, basePath, ret, host) + return cds.doMediaFiles(tracks, o.Path, ret, host) } else if matchResults["Artist"] != "" { - basePath := path.Join("/Music/Artists", matchResults["Artist"]) - allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) - return cds.doAlbums(allAlbumsForThisArtist, basePath, ret, host) + return cds.doAlbums(allAlbumsForThisArtist, o.Path, ret, host) } else { indexes, err := cds.ds.Artist(cds.ctx).GetIndex() @@ -158,11 +153,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] if matchResults["AlbumTrack"] != "" { log.Debug("TODO AlbumTrack MATCH") } else if matchResults["AlbumTitle"] != "" { - basePath := path.Join("/Music/Albums", matchResults["AlbumTitle"]) - tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) - - return cds.doMediaFiles(tracks, basePath, ret, host) + return cds.doMediaFiles(tracks, o.Path, ret, host) } else { indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() if err != nil { From 1dd36fd685a837e11ccf72857f2ecb5b9cff8750 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:50:50 +0000 Subject: [PATCH 50/87] cleaning out --- dlna/contenddirectoryservice.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 9fa4d3b0b..a56399680 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -123,14 +123,10 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] log.Debug("Artist Get a track ") } else if matchResults["ArtistAlbum"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) - return cds.doMediaFiles(tracks, o.Path, ret, host) - } else if matchResults["Artist"] != "" { allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) - return cds.doAlbums(allAlbumsForThisArtist, o.Path, ret, host) - } else { indexes, err := cds.ds.Artist(cds.ctx).GetIndex() if err != nil { @@ -185,15 +181,6 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return nil, err } return cds.doMediaFiles(tracks, o.Path, ret, host) - /* - //TODO do the metadata and stuff here - for trackIndex := range tracks { - child := object { - Path: path.Join(o.Path, tracks[trackIndex].Title), - Id: path.Join(o.Path, tracks[trackIndex].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, false, host)) - }*/ } else if matchResults["Genre"] != "" { if matchResults["GenreArtist"] == "" { artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{ "genre.id": matchResults["Genre"]}}) From 4449ee295254ca44ffea040dd50d5c02fb0360d5 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:59:43 +0000 Subject: [PATCH 51/87] Fixing the parent path, the path to the resource would only work if the filename was passed as the path, but we want to use the streaming methods, so we'll have to come up with something else --- dlna/contenddirectoryservice.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index a56399680..7633f1834 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -275,7 +275,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa obj := upnpav.Object{ ID: child.Id, Restricted: 1, - ParentID: child.ParentID(), + ParentID: basePath, Title: title, } @@ -297,7 +297,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa URL: (&url.URL{ Scheme: "http", Host: host, - Path: path.Join(resPath, child.Path), + Path: child.Path, }).String(), ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: false, From 0a6e04cc7598dcbfc1bf0e35a5a183566897587f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 18:19:38 +0000 Subject: [PATCH 52/87] Date now set --- dlna/contenddirectoryservice.go | 20 +++++++++++++------- dlna/dlnaserver.go | 4 ++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 7633f1834..d0bf9b6c8 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -84,7 +84,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is URL: (&url.URL{ Scheme: "http", Host: host, - Path: path.Join(resPath, cdsObject.Path), + Path: path.Join(resourcePath, cdsObject.Path), }).String(), ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: true, @@ -119,7 +119,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return cds.doFiles(ret, o.Path, host) } else if matchResults, err := artistRegex.Groups(o.Path); err == nil { if matchResults["ArtistAlbumTrack"] != "" { - //TODO + //This is never hit as the URL is direct to the resourcePath log.Debug("Artist Get a track ") } else if matchResults["ArtistAlbum"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) @@ -148,6 +148,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } else if matchResults, err := albumRegex.Groups(o.Path); err == nil { if matchResults["AlbumTrack"] != "" { log.Debug("TODO AlbumTrack MATCH") + //This is never hit as the URL is direct to the resourcePath } else if matchResults["AlbumTitle"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) @@ -278,11 +279,13 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa ParentID: basePath, Title: title, } - - var mimeType = "audio/mp3" //TODO - + + //TODO figure out how this fits with transcoding etc + var mimeType = "audio/mp3" obj.Class = "object.item.audioItem.musicTrack" - obj.Date = upnpav.Timestamp{Time:time.Now()} //TODO + + trackDate, _ := time.Parse(time.DateOnly, track.Date) + obj.Date = upnpav.Timestamp{Time:trackDate} obj.Artist = artist obj.Album = album obj.Genre = genre @@ -293,11 +296,14 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa Res: make([]upnpav.Resource, 0, 1), } + //TODO replace this with a streaming path + directFileAccessPath := path.Join(resourcePath, strings.TrimPrefix(track.Path, conf.Server.MusicFolder)) + item.Res = append(item.Res, upnpav.Resource{ URL: (&url.URL{ Scheme: "http", Host: host, - Path: child.Path, + Path: directFileAccessPath, }).String(), ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: false, diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index bd9b88c19..52c35fac3 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -32,7 +32,7 @@ import ( const ( serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0" rootDescPath = "/rootDesc.xml" - resPath = "/r/" + resourcePath = "/r/" serviceControlURL = "/ctl" ) @@ -96,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(resourcePath, http.StripPrefix(resourcePath, http.HandlerFunc(s.ssdp.resourceHandler))) r.Handle("/static/", http.FileServer(http.FS(staticContent))) r.HandleFunc(rootDescPath, s.ssdp.rootDescHandler) From ac04ea38347be006e2d30345dada1b7f5ce8b5df Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 18:23:53 +0000 Subject: [PATCH 53/87] Bit of a tidy up of objects and stuff --- dlna/contenddirectoryservice.go | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index d0bf9b6c8..509ad6dbf 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -261,35 +261,23 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa */ for _, track := range tracks { - child := object{ - Path: path.Join(basePath, track.ID), - Id: path.Join(basePath, track.ID), - } - title := track.Title - artist := track.Artist - album := track.Album - genre := track.Genre - trackNo := track.TrackNumber - - trackDurStr := floatToDurationString(track.Duration) + trackDateAsTimeObject, _ := time.Parse(time.DateOnly, track.Date) obj := upnpav.Object{ - ID: child.Id, + ID: path.Join(basePath, track.ID), Restricted: 1, ParentID: basePath, - Title: title, + Title: track.Title, + Class: "object.item.audioItem.musicTrack", + Artist: track.Artist, + Album: track.Album, + Genre: track.Genre, + OriginalTrackNumber: track.TrackNumber, + Date: upnpav.Timestamp{Time:trackDateAsTimeObject}, } //TODO figure out how this fits with transcoding etc var mimeType = "audio/mp3" - obj.Class = "object.item.audioItem.musicTrack" - - trackDate, _ := time.Parse(time.DateOnly, track.Date) - obj.Date = upnpav.Timestamp{Time:trackDate} - obj.Artist = artist - obj.Album = album - obj.Genre = genre - obj.OriginalTrackNumber = trackNo item := upnpav.Item{ Object: obj, @@ -309,7 +297,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa SupportRange: false, }.String()), Size: uint64(track.Size), - Duration: trackDurStr, + Duration: floatToDurationString(track.Duration), }) ret = append(ret, item) } From 0ee848edfedd33d853cd0cdbbcdc0dc35c7d2e67 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 3 Feb 2025 16:21:52 +0000 Subject: [PATCH 54/87] Moving direct file access under /r/f/ so we can add /r/s/ for streaming --- dlna/contenddirectoryservice.go | 8 ++--- dlna/dlnaserver.go | 63 +++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 509ad6dbf..850ca68ad 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -84,7 +84,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is URL: (&url.URL{ Scheme: "http", Host: host, - Path: path.Join(resourcePath, cdsObject.Path), + Path: path.Join(resourcePath, resourceFilePath, cdsObject.Path), }).String(), ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: true, @@ -285,13 +285,13 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa } //TODO replace this with a streaming path - directFileAccessPath := path.Join(resourcePath, strings.TrimPrefix(track.Path, conf.Server.MusicFolder)) - + //directFileAccessPath := path.Join(resourcePath, resourceFilePath, "Music/Files", strings.TrimPrefix(track.Path, conf.Server.MusicFolder)) + streamAccessPath := path.Join(resourcePath, resourceStreamPath, track.ID) item.Res = append(item.Res, upnpav.Resource{ URL: (&url.URL{ Scheme: "http", Host: host, - Path: directFileAccessPath, + Path: streamAccessPath, }).String(), ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: false, diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 52c35fac3..7fe4de6d3 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -33,6 +33,8 @@ const ( serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0" rootDescPath = "/rootDesc.xml" resourcePath = "/r/" + resourceFilePath = "f" + resourceStreamPath = "s" serviceControlURL = "/ctl" ) @@ -275,35 +277,44 @@ func isAppropriatelyConfigured(intf net.Interface) bool { func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { remotePath := r.URL.Path - localFile, _ := strings.CutPrefix(remotePath, "Music/Files/") - localFilePath := path.Join(conf.Server.MusicFolder, localFile) + components := strings.Split(remotePath, "/") + switch components[0] { + case resourceFilePath: + localFile, _ := strings.CutPrefix(remotePath, path.Join(resourceFilePath,"Music/Files")) + localFilePath := path.Join(conf.Server.MusicFolder, localFile) - log.Info(fmt.Sprintf("resource handler Executed with remote path: %s, localpath: %s", remotePath, 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) - return + fileStats, err := os.Stat(localFilePath) + if err != nil { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Length", strconv.FormatInt(fileStats.Size(), 10)) + + // add some DLNA specific headers + if r.Header.Get("getContentFeatures.dlna.org") != "" { + w.Header().Set("contentFeatures.dlna.org", dms_dlna.ContentFeatures{ + SupportRange: true, + }.String()) + } + w.Header().Set("transferMode.dlna.org", "Streaming") + + os.Open(localFilePath) + fileHandle, err := os.Open(localFilePath) + if err != nil { + fmt.Printf("file streaming error: %+v\n", err) + return + } + defer fileHandle.Close() + + http.ServeContent(w, r, remotePath, time.Now(), fileHandle) + break; + case resourceStreamPath: + //TODO streaming endpoint + log.Debug(fmt.Sprintf("TODO IMPLEMENT THIS: %+v", r)) + break; } - w.Header().Set("Content-Length", strconv.FormatInt(fileStats.Size(), 10)) - - // add some DLNA specific headers - if r.Header.Get("getContentFeatures.dlna.org") != "" { - w.Header().Set("contentFeatures.dlna.org", dms_dlna.ContentFeatures{ - SupportRange: true, - }.String()) - } - w.Header().Set("transferMode.dlna.org", "Streaming") - - os.Open(localFilePath) - fileHandle, err := os.Open(localFilePath) - if err != nil { - fmt.Printf("file streaming error: %+v\n", err) - return - } - defer fileHandle.Close() - - http.ServeContent(w, r, remotePath, time.Now(), fileHandle) } // returns /rootDesc.xml templated From dff87dc8ce878f23c59253d3751a574a17327a87 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 3 Feb 2025 16:57:45 +0000 Subject: [PATCH 55/87] Doesn't work, but barebones --- cmd/wire_gen.go | 13 ++++++++--- dlna/dlnaserver.go | 55 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 9f8a72c33..25d5c7585 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -59,10 +59,17 @@ func CreateServer() *server.Server { } func CreateDLNAServer() *dlna.DLNAServer { - dbDB := db.Db() - dataStore := persistence.New(dbDB) + sqlDB := db.Db() + dataStore := persistence.New(sqlDB) broker := events.GetBroker() - dlnaServer := dlna.New(dataStore, broker) + fFmpeg := ffmpeg.New() + transcodingCache := core.GetTranscodingCache() + mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache) + fileCache := artwork.GetImageCache() + agentsAgents := agents.New(dataStore) + externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents) + artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata) + dlnaServer := dlna.New(dataStore, broker, mediaStreamer, artworkArtwork) return dlnaServer } diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 7fe4de6d3..06f1ed04e 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -24,6 +24,8 @@ import ( "github.com/anacrolix/dms/upnp" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/core" + "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/events" @@ -35,6 +37,7 @@ const ( resourcePath = "/r/" resourceFilePath = "f" resourceStreamPath = "s" + resourceArtPath = "a" serviceControlURL = "/ctl" ) @@ -46,6 +49,8 @@ type DLNAServer struct { broker events.Broker ssdp SSDPServer ctx context.Context + ms core.MediaStreamer + art artwork.Artwork } type SSDPServer struct { @@ -68,9 +73,12 @@ type SSDPServer struct { // Time interval between SSPD announces AnnounceInterval time.Duration + + ms core.MediaStreamer + art artwork.Artwork } -func New(ds model.DataStore, broker events.Broker) *DLNAServer { +func New(ds model.DataStore, broker events.Broker, mediastreamer core.MediaStreamer, artwork artwork.Artwork) *DLNAServer { s := &DLNAServer{ ds: ds, broker: broker, @@ -82,6 +90,8 @@ func New(ds model.DataStore, broker events.Broker) *DLNAServer { RootDeviceUUID: makeDeviceUUID("Navidrome"), waitChan: make(chan struct{}), }, + ms: mediastreamer, + art: artwork, } s.ssdp.services = map[string]UPnPService{ @@ -313,6 +323,49 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { case resourceStreamPath: //TODO streaming endpoint log.Debug(fmt.Sprintf("TODO IMPLEMENT THIS: %+v", r)) + + //Copypasta stream.go:52 + + id := components[1] + + ctx := r.Context() + /* + maxBitRate := p.IntOr("maxBitRate", 0) + format, _ := p.String("format") + timeOffset := p.IntOr("timeOffset", 0) +*/ //TODO figure out format, bitrate +log.Debug(fmt.Sprintf("1 %+v | comp: %+v",id, components)) + stream, err := s.ms.NewStream(ctx, id, "mp3", 0, 0) + if err != nil { + //eturn nil, err + } + log.Debug("2") + // Make sure the stream will be closed at the end, to avoid leakage + defer func() { + if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) { + log.Error("Error closing stream", "id", id, "file", stream.Name(), err) + } + }() + log.Debug("3") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) + log.Debug("4") + http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) + log.Debug("5") + break; + case resourceArtPath: + log.Debug("1a") + //copy pasta handle_images.go:39 + artId, _ := model.ParseArtworkID(components[1]) + imgReader, lastUpdate, _ := s.art.Get(r.Context(), artId, 250, true) + log.Debug("2a") + defer imgReader.Close() + log.Debug("3a") + w.Header().Set("Cache-Control", "public, max-age=315360000") + w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123)) + log.Debug("4a") + io.Copy(w, imgReader) + log.Debug("5a") break; } } From f7dbad2570fd9989c945ab4c8d343d19b2a36c59 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 16:52:15 +0000 Subject: [PATCH 56/87] Redirecting away from raw file paths to stream endpoints --- dlna/contenddirectoryservice.go | 8 -------- dlna/dlnaserver.go | 26 ++++++++------------------ 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 850ca68ad..ef2e0bb54 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -284,8 +284,6 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa Res: make([]upnpav.Resource, 0, 1), } - //TODO replace this with a streaming path - //directFileAccessPath := path.Join(resourcePath, resourceFilePath, "Music/Files", strings.TrimPrefix(track.Path, conf.Server.MusicFolder)) streamAccessPath := path.Join(resourcePath, resourceStreamPath, track.ID) item.Res = append(item.Res, upnpav.Resource{ URL: (&url.URL{ @@ -318,12 +316,6 @@ func floatToDurationString(totalSeconds32 float32) string { return fmt.Sprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, ms) } -func (cds *contentDirectoryService) doAlbum(album *model.Album, basepath string, ret []interface{}, host string) ([]interface{}, error) { - log.Debug(fmt.Sprintf("TODO: doAlbum Called with : '%+v', '%s'", album, basepath)) - panic("doAlbum Called!") - return ret, nil -} - func (cds *contentDirectoryService) doAlbums(albums model.Albums, basepath string, ret []interface{}, host string) ([]interface{}, error) { for _, album := range albums { child := object{ diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 06f1ed04e..fd0ecbcb5 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -89,6 +89,8 @@ func New(ds model.DataStore, broker events.Broker, mediastreamer core.MediaStrea ModelNumber: consts.Version, RootDeviceUUID: makeDeviceUUID("Navidrome"), waitChan: make(chan struct{}), + ms: mediastreamer, + art: artwork, }, ms: mediastreamer, art: artwork, @@ -321,37 +323,25 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, remotePath, time.Now(), fileHandle) break; case resourceStreamPath: - //TODO streaming endpoint - log.Debug(fmt.Sprintf("TODO IMPLEMENT THIS: %+v", r)) - //Copypasta stream.go:52 - id := components[1] + fileId := components[1] - ctx := r.Context() - /* - maxBitRate := p.IntOr("maxBitRate", 0) - format, _ := p.String("format") - timeOffset := p.IntOr("timeOffset", 0) -*/ //TODO figure out format, bitrate -log.Debug(fmt.Sprintf("1 %+v | comp: %+v",id, components)) - stream, err := s.ms.NewStream(ctx, id, "mp3", 0, 0) + //TODO figure out format, bitrate + stream, err := s.ms.NewStream(r.Context(), fileId, "mp3", 0, 0) if err != nil { + log.Error("Error streaming file", "id", fileId, err) + //TODO throw 500 //eturn nil, err } - log.Debug("2") - // Make sure the stream will be closed at the end, to avoid leakage defer func() { if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) { - log.Error("Error closing stream", "id", id, "file", stream.Name(), err) + log.Error("Error closing stream", "id", fileId, "file", stream.Name(), err) } }() - log.Debug("3") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) - log.Debug("4") http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) - log.Debug("5") break; case resourceArtPath: log.Debug("1a") From c6dd9732e57bc77c5ef7f0c5e878430ae28630f3 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 17:27:26 +0000 Subject: [PATCH 57/87] Fixing artwork urls --- dlna/contenddirectoryservice.go | 8 ++++++++ dlna/dlnaserver.go | 27 ++++++++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index ef2e0bb54..1463d63c0 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -275,6 +275,14 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa OriginalTrackNumber: track.TrackNumber, Date: upnpav.Timestamp{Time:trackDateAsTimeObject}, } + + if(track.HasCoverArt) { + obj.AlbumArtURI = (&url.URL{ + Scheme: "http", + Host: host, + Path: path.Join(resourcePath, resourceArtPath, track.CoverArtID().String()), + }).String() + } //TODO figure out how this fits with transcoding etc var mimeType = "audio/mp3" diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index fd0ecbcb5..4f88aefc5 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -322,8 +322,7 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, remotePath, time.Now(), fileHandle) break; - case resourceStreamPath: - //Copypasta stream.go:52 + case resourceStreamPath: //TODO refactor this with stream.go:52? fileId := components[1] @@ -331,8 +330,7 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { stream, err := s.ms.NewStream(r.Context(), fileId, "mp3", 0, 0) if err != nil { log.Error("Error streaming file", "id", fileId, err) - //TODO throw 500 - //eturn nil, err + return } defer func() { if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) { @@ -343,19 +341,22 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) break; - case resourceArtPath: - log.Debug("1a") - //copy pasta handle_images.go:39 - artId, _ := model.ParseArtworkID(components[1]) - imgReader, lastUpdate, _ := s.art.Get(r.Context(), artId, 250, true) - log.Debug("2a") + case resourceArtPath: //TODO refactor this with handle_images.go:39? + artId, err := model.ParseArtworkID(components[1]) + if err != nil { + log.Error("Failure to parse ArtworkId", "inputString", components[1], err) + return + } + //TODO size (250) + imgReader, lastUpdate, err := s.art.Get(r.Context(), artId, 250, true) + if err != nil { + log.Error("Failure to retrieve artwork", "artid", artId, err) + return + } defer imgReader.Close() - log.Debug("3a") w.Header().Set("Cache-Control", "public, max-age=315360000") w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123)) - log.Debug("4a") io.Copy(w, imgReader) - log.Debug("5a") break; } } From e9c4fa7892630a35c58ac419f4ebc65c9865a4ae Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 18:01:02 +0000 Subject: [PATCH 58/87] Implementing recently added --- dlna/contenddirectoryservice.go | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 1463d63c0..9d250ee3c 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -42,7 +42,7 @@ func init() { artistRegex = regroup.MustCompile("\\/Music\\/Artists[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?[\\/]?(?[^\\/]+)?") albumRegex = regroup.MustCompile("\\/Music\\/Albums[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?") genresRegex = regroup.MustCompile("\\/Music\\/Genres[\\/]?(?P[^\\/]+)?[\\/]?(?P[^/]+)?[\\/]?(?P[^\\/]+)?") - recentRegex = regroup.MustCompile("\\/Music\\/Recently Added[\\/]?(?P[^\\/]+)?") + recentRegex = regroup.MustCompile("\\/Music\\/Recently Added[\\/]?(?P[^\\/]+)?") playlistRegex = regroup.MustCompile("\\/Music\\/Playlist[\\/]?(?P[^\\/]+)?[\\/]?(?P[^\\/]+)?") } @@ -147,8 +147,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } } else if matchResults, err := albumRegex.Groups(o.Path); err == nil { if matchResults["AlbumTrack"] != "" { - log.Debug("TODO AlbumTrack MATCH") - //This is never hit as the URL is direct to the resourcePath + //This is never hit as the URL is direct to the streamPath } else if matchResults["AlbumTitle"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) @@ -168,9 +167,8 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } else if matchResults, err := genresRegex.Groups(o.Path); err == nil { - log.Debug("Genre MATCH") if matchResults["GenreTrack"] != "" { - log.Debug("TODO GenreTrack MATCH") + //This is never hit as the URL is direct to the streamPath } else if matchResults["GenreArtist"] != "" { tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ squirrel.Eq{"genre.id": matchResults["Genre"],}, @@ -213,12 +211,31 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } else if matchResults, err := recentRegex.Groups(o.Path); err == nil { - log.Debug("TODO recent MATCH") + + log.Debug("TODO recent MATCH") //ROB YOU ARE fmt.Printf("%+v", matchResults) + + if matchResults["RecentAlbum"] != "" { + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["RecentAlbum"]}}) + return cds.doMediaFiles(tracks, o.Path, ret, host) + } else { + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres(model.QueryOptions{Sort: "recently_added", Order: "desc", Max: 25}) + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil + } } else if matchResults, err := playlistRegex.Groups(o.Path); err == nil { - log.Debug("TODO Playlist MATCH") if matchResults["PlaylistTrack"] != "" { - log.Debug("TODO PlaylistTrack MATCH") + //This is never hit as the URL is direct to the streamPath } else if matchResults["Playlist"] != "" { log.Debug("Playlist only MATCH") x, xerr := cds.ds.Playlist(cds.ctx).Get(matchResults["Playlist"]) From bc38481a759a0886f46b84eea1bfc67f95aad75e Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 18:45:24 +0000 Subject: [PATCH 59/87] Tidying up lints all over the place --- cmd/root.go | 1 - dlna/contenddirectoryservice.go | 4 +- dlna/dlnaserver.go | 70 ++++++++++++++++----------------- 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 4261a7a0f..115d5adf8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -85,7 +85,6 @@ func runNavidrome(ctx context.Context) { g.Go(startServer(ctx)) if conf.Server.DLNAServer.Enabled { g.Go(startDLNAServer(ctx)) - } g.Go(startSignaller(ctx)) g.Go(startScheduler(ctx)) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 9d250ee3c..0c1a07145 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -211,7 +211,6 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } else if matchResults, err := recentRegex.Groups(o.Path); err == nil { - log.Debug("TODO recent MATCH") //ROB YOU ARE fmt.Printf("%+v", matchResults) @@ -496,7 +495,8 @@ func (o object) ID() string { return o.Id } if !path.IsAbs(o.Path) { - log.Fatal(fmt.Sprintf("Relative object path used with ID: $s", o.Path)) + log.Fatal("Relative object path used", "path", o.Path) + return "-1" } if len(o.Path) == 1 { return "0" diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 4f88aefc5..ca6b2a64e 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -140,7 +140,10 @@ func (s *DLNAServer) Run(ctx context.Context, addr string, port int) (err error) s.ssdp.startSSDP() }() go func() { - s.ssdp.serveHTTP() + err := s.ssdp.serveHTTP() + if err != nil { + log.Error("Error starting ssdp HTTP server", err) + } }() return nil } @@ -245,7 +248,7 @@ func (s *SSDPServer) ssdpInterface(intf net.Interface) { // good. return } - log.Error(fmt.Sprintf("Error creating ssdp server on %s: %s", intf.Name), err) + log.Error("Error creating ssdp server", "intf.Name", intf.Name, err) return } defer ssdpServer.Close() @@ -312,7 +315,6 @@ 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 { fmt.Printf("file streaming error: %+v\n", err) @@ -321,7 +323,6 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { defer fileHandle.Close() http.ServeContent(w, r, remotePath, time.Now(), fileHandle) - break; case resourceStreamPath: //TODO refactor this with stream.go:52? fileId := components[1] @@ -340,24 +341,26 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) - break; - case resourceArtPath: //TODO refactor this with handle_images.go:39? - artId, err := model.ParseArtworkID(components[1]) - if err != nil { - log.Error("Failure to parse ArtworkId", "inputString", components[1], err) - return - } - //TODO size (250) - imgReader, lastUpdate, err := s.art.Get(r.Context(), artId, 250, true) - if err != nil { - log.Error("Failure to retrieve artwork", "artid", artId, err) - return - } - defer imgReader.Close() - w.Header().Set("Cache-Control", "public, max-age=315360000") - w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123)) - io.Copy(w, imgReader) - break; + case resourceArtPath: //TODO refactor this with handle_images.go:39? + artId, err := model.ParseArtworkID(components[1]) + if err != nil { + log.Error("Failure to parse ArtworkId", "inputString", components[1], err) + return + } + //TODO size (250) + imgReader, lastUpdate, err := s.art.Get(r.Context(), artId, 250, true) + if err != nil { + log.Error("Failure to retrieve artwork", "artid", artId, err) + return + } + defer imgReader.Close() + w.Header().Set("Cache-Control", "public, max-age=315360000") + w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123)) + _, err = io.Copy(w, imgReader) + if err != nil { + log.Error("Error writing Artwork Response stream", err) + return + } } } @@ -371,7 +374,10 @@ func (s *SSDPServer) rootDescHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", `text/xml; charset="utf-8"`) w.Header().Set("cache-control", "private, max-age=60") w.Header().Set("content-length", strconv.FormatInt(int64(buffer.Len()), 10)) - buffer.WriteTo(w) + _, err := buffer.WriteTo(w) + if err != nil { + log.Error("Error writing rootDesc to responsebuffer", err) + } } // Handle a service control HTTP request. @@ -419,6 +425,7 @@ func (s *SSDPServer) soapActionResponse(sa upnp.SoapAction, actionRequestXML []b func (s *SSDPServer) serveHTTP() error { srv := &http.Server{ Handler: s.handler, + ReadHeaderTimeout: 10, } err := srv.Serve(s.HTTPConn) select { @@ -469,22 +476,13 @@ func makeDeviceUUID(unique string) string { return upnp.FormatUUID(buf) } -// HTTP handler that sets headers. -func withHeader(name string, value string, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(name, value) - next.ServeHTTP(w, r) - }) -} - // 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) + log.Error("serveError", "what", what, "text", text, err) } func GetTemplate() (tpl *template.Template, err error) { - templateBytes := ` ` - var templateString = string(templateBytes) - - tpl, err = template.New("rootDesc").Parse(templateString) + tpl, err = template.New("rootDesc").Parse(templateBytes) if err != nil { return nil, fmt.Errorf("get template parse: %w", err) } - return + return tpl, nil } From ea2942b73bc5a023c1dcc27462f594fe901ab773 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 18:50:49 +0000 Subject: [PATCH 60/87] More linting --- dlna/contenddirectoryservice.go | 2 +- dlna/dlnaserver.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 0c1a07145..e6bbc7ee1 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -256,7 +256,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } } - return + return ret, nil } func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePath string, ret []interface{}, host string) ([]interface{}, error) { diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index ca6b2a64e..587def9fa 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -253,13 +253,12 @@ func (s *SSDPServer) ssdpInterface(intf net.Interface) { } defer ssdpServer.Close() - log.Info(fmt.Sprintf("Started SSDP on %v", intf.Name)) + log.Info("Started SSDP", "intf.Name", intf.Name) stopped := make(chan struct{}) go func() { defer close(stopped) if err := ssdpServer.Serve(); err != nil { - log.Error(fmt.Sprintf("Err %q", intf.Name), err) - + log.Error("Error in ssdpServer Serve", "intf.Name", intf.Name, err) } }() select { From 2696eaee64b9895c2eaeaa3d46183aa10f5c3375 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 19:05:44 +0000 Subject: [PATCH 61/87] Refactoring a bit --- dlna/contenddirectoryservice.go | 351 +++++++++++++++++--------------- 1 file changed, 188 insertions(+), 163 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index e6bbc7ee1..d3865d649 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -108,153 +108,178 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } if o.Path == "/Music" { - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host)) - return ret, nil + return handleDefault(ret, cds, o, host) } else if _, err := filesRegex.Groups(o.Path); err == nil { return cds.doFiles(ret, o.Path, host) } else if matchResults, err := artistRegex.Groups(o.Path); err == nil { - if matchResults["ArtistAlbumTrack"] != "" { - //This is never hit as the URL is direct to the resourcePath - log.Debug("Artist Get a track ") - } else if matchResults["ArtistAlbum"] != "" { - tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) - return cds.doMediaFiles(tracks, o.Path, ret, host) - } else if matchResults["Artist"] != "" { - allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) - return cds.doAlbums(allAlbumsForThisArtist, o.Path, ret, host) - } else { - indexes, err := cds.ds.Artist(cds.ctx).GetIndex() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for letterIndex := range indexes { - for artist := range indexes[letterIndex].Artists { - artistId := indexes[letterIndex].Artists[artist].ID - child := object{ - Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), - Id: path.Join(o.Path, artistId), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - } - return ret, nil - } + return handleArtist(matchResults, ret, cds, o, host) } else if matchResults, err := albumRegex.Groups(o.Path); err == nil { - if matchResults["AlbumTrack"] != "" { - //This is never hit as the URL is direct to the streamPath - } else if matchResults["AlbumTitle"] != "" { - tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) - return cds.doMediaFiles(tracks, o.Path, ret, host) - } else { - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil - } + return handleAlbum(matchResults, ret, cds, o, host) } else if matchResults, err := genresRegex.Groups(o.Path); err == nil { - if matchResults["GenreTrack"] != "" { - //This is never hit as the URL is direct to the streamPath - } else if matchResults["GenreArtist"] != "" { - tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ - squirrel.Eq{"genre.id": matchResults["Genre"],}, - squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, - }, - }) - if err != nil { - fmt.Printf("Error retrieving tracks for artist and genre: %+v", err) - return nil, err - } - return cds.doMediaFiles(tracks, o.Path, ret, host) - } else if matchResults["Genre"] != "" { - if matchResults["GenreArtist"] == "" { - artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{ "genre.id": matchResults["Genre"]}}) - if err != nil { - fmt.Printf("Error retrieving artists for genre: %+v", err) - return nil, err - } - for artistIndex := range artists { - child := object{ - Path: path.Join(o.Path, artists[artistIndex].Name), - Id: path.Join(o.Path, artists[artistIndex].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - } - } else { - indexes, err := cds.ds.Genre(cds.ctx).GetAll() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil - } + return handleGenre(matchResults, ret, cds, o, host) } else if matchResults, err := recentRegex.Groups(o.Path); err == nil { - log.Debug("TODO recent MATCH") //ROB YOU ARE - fmt.Printf("%+v", matchResults) - - if matchResults["RecentAlbum"] != "" { - tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["RecentAlbum"]}}) - return cds.doMediaFiles(tracks, o.Path, ret, host) - } else { - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres(model.QueryOptions{Sort: "recently_added", Order: "desc", Max: 25}) - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil - } + return handleRecent(matchResults, ret, cds, o, host) } else if matchResults, err := playlistRegex.Groups(o.Path); err == nil { - if matchResults["PlaylistTrack"] != "" { - //This is never hit as the URL is direct to the streamPath - } else if matchResults["Playlist"] != "" { - log.Debug("Playlist only MATCH") - x, xerr := cds.ds.Playlist(cds.ctx).Get(matchResults["Playlist"]) - log.Debug(fmt.Sprintf("TODO Playlist: %+v", x), xerr) - } else { - log.Debug("Playlist else MATCH") - indexes, err := cds.ds.Playlist(cds.ctx).GetAll() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { + return handlePlaylists(matchResults, ret, cds, o, host) + } + return ret, nil +} + +func handleDefault(ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host)) + return ret, nil +} + +func handleArtist(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { + if matchResults["ArtistAlbumTrack"] != "" { + //This is never hit as the URL is direct to the resourcePath + log.Debug("Artist Get a track ") + } else if matchResults["ArtistAlbum"] != "" { + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) + return cds.doMediaFiles(tracks, o.Path, ret, host) + } else if matchResults["Artist"] != "" { + allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) + return cds.doAlbums(allAlbumsForThisArtist, o.Path, ret, host) + } else { + indexes, err := cds.ds.Artist(cds.ctx).GetIndex() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for letterIndex := range indexes { + for artist := range indexes[letterIndex].Artists { + artistId := indexes[letterIndex].Artists[artist].ID child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), + Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), + Id: path.Join(o.Path, artistId), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } - return ret, nil } + return ret, nil + } + return ret,nil +} + +func handleAlbum(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { + if matchResults["AlbumTrack"] != "" { + //This is never hit as the URL is direct to the streamPath + } else if matchResults["AlbumTitle"] != "" { + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) + return cds.doMediaFiles(tracks, o.Path, ret, host) + } else { + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil + } + return ret, nil +} + +func handleGenre(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { + if matchResults["GenreTrack"] != "" { + //This is never hit as the URL is direct to the streamPath + } else if matchResults["GenreArtist"] != "" { + tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ + squirrel.Eq{"genre.id": matchResults["Genre"]}, + squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, + }, + }) + if err != nil { + fmt.Printf("Error retrieving tracks for artist and genre: %+v", err) + return nil, err + } + return cds.doMediaFiles(tracks, o.Path, ret, host) + } else if matchResults["Genre"] != "" { + if matchResults["GenreArtist"] == "" { + artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"genre.id": matchResults["Genre"]}}) + if err != nil { + fmt.Printf("Error retrieving artists for genre: %+v", err) + return nil, err + } + for artistIndex := range artists { + child := object{ + Path: path.Join(o.Path, artists[artistIndex].Name), + Id: path.Join(o.Path, artists[artistIndex].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + } + } else { + indexes, err := cds.ds.Genre(cds.ctx).GetAll() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil + } + return ret, nil +} + +func handleRecent(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { + if matchResults["RecentAlbum"] != "" { + tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["RecentAlbum"]}}) + return cds.doMediaFiles(tracks, o.Path, ret, host) + } else { + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres(model.QueryOptions{Sort: "recently_added", Order: "desc", Max: 25}) + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil + } +} + +func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { + if matchResults["PlaylistTrack"] != "" { + //This is never hit as the URL is direct to the streamPath + } else if matchResults["Playlist"] != "" { + log.Debug("Playlist only MATCH") + //x, xerr := cds.ds.Playlist(cds.ctx).Get(matchResults["Playlist"]) + return ret, nil + } else { + log.Debug("Playlist else MATCH") + indexes, err := cds.ds.Playlist(cds.ctx).GetAll() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil } return ret, nil } @@ -262,44 +287,44 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePath string, ret []interface{}, host string) ([]interface{}, error) { //TODO flesh object out with actually useful metadata about the track /* - - Love Takes Time - - Mariah Carey - 2000-01-01 - object.item.audioItem.musicTrack - Mariah Carey - #1's - Pop - 2 - http://172.30.0.4:8200/AlbumArt/24179-17759.jpg - http://172.30.0.4:8200/MediaItems/17759.mp3 - + + Love Takes Time + + Mariah Carey + 2000-01-01 + object.item.audioItem.musicTrack + Mariah Carey + #1's + Pop + 2 + http://172.30.0.4:8200/AlbumArt/24179-17759.jpg + http://172.30.0.4:8200/MediaItems/17759.mp3 + */ for _, track := range tracks { trackDateAsTimeObject, _ := time.Parse(time.DateOnly, track.Date) obj := upnpav.Object{ - ID: path.Join(basePath, track.ID), - Restricted: 1, - ParentID: basePath, - Title: track.Title, - Class: "object.item.audioItem.musicTrack", - Artist: track.Artist, - Album: track.Album, - Genre: track.Genre, + ID: path.Join(basePath, track.ID), + Restricted: 1, + ParentID: basePath, + Title: track.Title, + Class: "object.item.audioItem.musicTrack", + Artist: track.Artist, + Album: track.Album, + Genre: track.Genre, OriginalTrackNumber: track.TrackNumber, - Date: upnpav.Timestamp{Time:trackDateAsTimeObject}, + Date: upnpav.Timestamp{Time: trackDateAsTimeObject}, } - if(track.HasCoverArt) { + if track.HasCoverArt { obj.AlbumArtURI = (&url.URL{ Scheme: "http", Host: host, Path: path.Join(resourcePath, resourceArtPath, track.CoverArtID().String()), }).String() } - + //TODO figure out how this fits with transcoding etc var mimeType = "audio/mp3" @@ -318,7 +343,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: false, }.String()), - Size: uint64(track.Size), + Size: uint64(track.Size), Duration: floatToDurationString(track.Duration), }) ret = append(ret, item) @@ -329,13 +354,13 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa func floatToDurationString(totalSeconds32 float32) string { totalSeconds := float64(totalSeconds32) - secondsInAnHour := float64(60*60) + secondsInAnHour := float64(60 * 60) secondsInAMinute := float64(60) hours := int(math.Floor(totalSeconds / secondsInAnHour)) - minutes := int(math.Floor(math.Mod(totalSeconds, secondsInAnHour) / secondsInAMinute)) + minutes := int(math.Floor(math.Mod(totalSeconds, secondsInAnHour) / secondsInAMinute)) seconds := int(math.Floor(math.Mod(totalSeconds, secondsInAMinute))) - ms := int(math.Floor(math.Mod(totalSeconds,1) * 1000)) + ms := int(math.Floor(math.Mod(totalSeconds, 1) * 1000)) return fmt.Sprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, ms) } From 335251384f7fd77c596718c22406289194b65c3c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 19:09:57 +0000 Subject: [PATCH 62/87] 10 seconds not 10 ..very short periods of time --- dlna/dlnaserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 587def9fa..abfc6376e 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -424,7 +424,7 @@ func (s *SSDPServer) soapActionResponse(sa upnp.SoapAction, actionRequestXML []b func (s *SSDPServer) serveHTTP() error { srv := &http.Server{ Handler: s.handler, - ReadHeaderTimeout: 10, + ReadHeaderTimeout: 10 * time.Second, } err := srv.Serve(s.HTTPConn) select { From 883d65de1429d4e2b739f8ee6dd80bdc38212d5a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 19:14:59 +0000 Subject: [PATCH 63/87] Formatting --- dlna/contenddirectoryservice.go | 8 +- dlna/dlnaserver.go | 146 ++++++++++++++++---------------- dlna/upnpav/upnpav.go | 27 +++--- go.mod | 4 + go.sum | 2 + 5 files changed, 97 insertions(+), 90 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index d3865d649..7057f9a6b 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -163,7 +163,7 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten } return ret, nil } - return ret,nil + return ret, nil } func handleAlbum(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { @@ -195,9 +195,9 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content //This is never hit as the URL is direct to the streamPath } else if matchResults["GenreArtist"] != "" { tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ - squirrel.Eq{"genre.id": matchResults["Genre"]}, - squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, - }, + squirrel.Eq{"genre.id": matchResults["Genre"]}, + squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, + }, }) if err != nil { fmt.Printf("Error retrieving tracks for artist and genre: %+v", err) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index abfc6376e..73a270719 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -32,13 +32,13 @@ import ( ) const ( - serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0" - rootDescPath = "/rootDesc.xml" - resourcePath = "/r/" - resourceFilePath = "f" + serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0" + rootDescPath = "/rootDesc.xml" + resourcePath = "/r/" + resourceFilePath = "f" resourceStreamPath = "s" - resourceArtPath = "a" - serviceControlURL = "/ctl" + resourceArtPath = "a" + serviceControlURL = "/ctl" ) //go:embed static/* @@ -49,8 +49,8 @@ type DLNAServer struct { broker events.Broker ssdp SSDPServer ctx context.Context - ms core.MediaStreamer - art artwork.Artwork + ms core.MediaStreamer + art artwork.Artwork } type SSDPServer struct { @@ -74,8 +74,8 @@ type SSDPServer struct { // Time interval between SSPD announces AnnounceInterval time.Duration - ms core.MediaStreamer - art artwork.Artwork + ms core.MediaStreamer + art artwork.Artwork } func New(ds model.DataStore, broker events.Broker, mediastreamer core.MediaStreamer, artwork artwork.Artwork) *DLNAServer { @@ -89,10 +89,10 @@ func New(ds model.DataStore, broker events.Broker, mediastreamer core.MediaStrea ModelNumber: consts.Version, RootDeviceUUID: makeDeviceUUID("Navidrome"), waitChan: make(chan struct{}), - ms: mediastreamer, - art: artwork, + ms: mediastreamer, + art: artwork, }, - ms: mediastreamer, + ms: mediastreamer, art: artwork, } @@ -293,73 +293,73 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { components := strings.Split(remotePath, "/") switch components[0] { - case resourceFilePath: - localFile, _ := strings.CutPrefix(remotePath, path.Join(resourceFilePath,"Music/Files")) - localFilePath := path.Join(conf.Server.MusicFolder, localFile) + case resourceFilePath: + localFile, _ := strings.CutPrefix(remotePath, path.Join(resourceFilePath, "Music/Files")) + localFilePath := path.Join(conf.Server.MusicFolder, localFile) - log.Info(fmt.Sprintf("resource handler Executed with remote path: %s, localpath: %s", remotePath, 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) - return - } - w.Header().Set("Content-Length", strconv.FormatInt(fileStats.Size(), 10)) + fileStats, err := os.Stat(localFilePath) + if err != nil { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Length", strconv.FormatInt(fileStats.Size(), 10)) - // add some DLNA specific headers - if r.Header.Get("getContentFeatures.dlna.org") != "" { - w.Header().Set("contentFeatures.dlna.org", dms_dlna.ContentFeatures{ - SupportRange: true, - }.String()) - } - w.Header().Set("transferMode.dlna.org", "Streaming") + // add some DLNA specific headers + if r.Header.Get("getContentFeatures.dlna.org") != "" { + w.Header().Set("contentFeatures.dlna.org", dms_dlna.ContentFeatures{ + SupportRange: true, + }.String()) + } + w.Header().Set("transferMode.dlna.org", "Streaming") - fileHandle, err := os.Open(localFilePath) - if err != nil { - fmt.Printf("file streaming error: %+v\n", err) - return - } - defer fileHandle.Close() + fileHandle, err := os.Open(localFilePath) + if err != nil { + fmt.Printf("file streaming error: %+v\n", err) + return + } + defer fileHandle.Close() - http.ServeContent(w, r, remotePath, time.Now(), fileHandle) - case resourceStreamPath: //TODO refactor this with stream.go:52? + http.ServeContent(w, r, remotePath, time.Now(), fileHandle) + case resourceStreamPath: //TODO refactor this with stream.go:52? - fileId := components[1] + fileId := components[1] - //TODO figure out format, bitrate - stream, err := s.ms.NewStream(r.Context(), fileId, "mp3", 0, 0) - if err != nil { - log.Error("Error streaming file", "id", fileId, err) - return - } - defer func() { - if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) { - log.Error("Error closing stream", "id", fileId, "file", stream.Name(), err) - } - }() - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) - http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) - case resourceArtPath: //TODO refactor this with handle_images.go:39? - artId, err := model.ParseArtworkID(components[1]) - if err != nil { - log.Error("Failure to parse ArtworkId", "inputString", components[1], err) - return - } - //TODO size (250) - imgReader, lastUpdate, err := s.art.Get(r.Context(), artId, 250, true) - if err != nil { - log.Error("Failure to retrieve artwork", "artid", artId, err) - return - } - defer imgReader.Close() - w.Header().Set("Cache-Control", "public, max-age=315360000") - w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123)) - _, err = io.Copy(w, imgReader) - if err != nil { - log.Error("Error writing Artwork Response stream", err) - return + //TODO figure out format, bitrate + stream, err := s.ms.NewStream(r.Context(), fileId, "mp3", 0, 0) + if err != nil { + log.Error("Error streaming file", "id", fileId, err) + return + } + defer func() { + if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) { + log.Error("Error closing stream", "id", fileId, "file", stream.Name(), err) } + }() + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32)) + http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream) + case resourceArtPath: //TODO refactor this with handle_images.go:39? + artId, err := model.ParseArtworkID(components[1]) + if err != nil { + log.Error("Failure to parse ArtworkId", "inputString", components[1], err) + return + } + //TODO size (250) + imgReader, lastUpdate, err := s.art.Get(r.Context(), artId, 250, true) + if err != nil { + log.Error("Failure to retrieve artwork", "artid", artId, err) + return + } + defer imgReader.Close() + w.Header().Set("Cache-Control", "public, max-age=315360000") + w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123)) + _, err = io.Copy(w, imgReader) + if err != nil { + log.Error("Error writing Artwork Response stream", err) + return + } } } @@ -423,7 +423,7 @@ func (s *SSDPServer) soapActionResponse(sa upnp.SoapAction, actionRequestXML []b func (s *SSDPServer) serveHTTP() error { srv := &http.Server{ - Handler: s.handler, + Handler: s.handler, ReadHeaderTimeout: 10 * time.Second, } err := srv.Serve(s.HTTPConn) diff --git a/dlna/upnpav/upnpav.go b/dlna/upnpav/upnpav.go index a3f5c15e4..3a9616fad 100644 --- a/dlna/upnpav/upnpav.go +++ b/dlna/upnpav/upnpav.go @@ -39,20 +39,21 @@ type Item struct { // Object description type Object struct { - ID string `xml:"id,attr"` - ParentID string `xml:"parentID,attr"` - Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable - Class string `xml:"upnp:class"` - Icon string `xml:"upnp:icon,omitempty"` - Title string `xml:"dc:title"` - Date Timestamp `xml:"dc:date"` - Artist string `xml:"upnp:artist,omitempty"` - Album string `xml:"upnp:album,omitempty"` - Genre string `xml:"upnp:genre,omitempty"` - AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` - OriginalTrackNumber int `xml:"upnp:originalTrackNumber,omitempty"` - Searchable int `xml:"searchable,attr"` + ID string `xml:"id,attr"` + ParentID string `xml:"parentID,attr"` + Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable + Class string `xml:"upnp:class"` + Icon string `xml:"upnp:icon,omitempty"` + Title string `xml:"dc:title"` + Date Timestamp `xml:"dc:date"` + Artist string `xml:"upnp:artist,omitempty"` + Album string `xml:"upnp:album,omitempty"` + Genre string `xml:"upnp:genre,omitempty"` + AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` + OriginalTrackNumber int `xml:"upnp:originalTrackNumber,omitempty"` + Searchable int `xml:"searchable,attr"` } + // Timestamp wraps time.Time for formatting purposes type Timestamp struct { time.Time diff --git a/go.mod b/go.mod index f8c3a93c1..f20c30297 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ replace ( require ( github.com/Masterminds/squirrel v1.5.4 github.com/RaveNoX/go-jsoncommentstrip v1.0.0 + github.com/anacrolix/dms v1.7.1 github.com/andybalholm/cascadia v1.3.3 github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 @@ -51,6 +52,9 @@ require ( github.com/mileusna/useragent v1.3.5 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 + github.com/onsi/ginkgo/v2 v2.23.4 + github.com/onsi/gomega v1.37.0 + github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e github.com/pelletier/go-toml/v2 v2.2.4 github.com/pocketbase/dbx v1.11.0 github.com/pressly/goose/v3 v3.26.0 diff --git a/go.sum b/go.sum index 3dfb10ac2..3cd34a889 100644 --- a/go.sum +++ b/go.sum @@ -121,6 +121,8 @@ github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66S github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno= github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From 56baf82a4d7315faecec71fc58283eebd1b7d585 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 20:00:10 +0000 Subject: [PATCH 64/87] Fixing playlist tracks, we also don't need the tracks on the end of the regex because they will link to /s/trackid instead --- dlna/contenddirectoryservice.go | 57 +++++++++------------------------ 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 7057f9a6b..82b6980fc 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -39,11 +39,11 @@ var playlistRegex *regroup.ReGroup func init() { filesRegex = regroup.MustCompile("\\/Music\\/Files[\\/]?((?P.+))?") - artistRegex = regroup.MustCompile("\\/Music\\/Artists[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?[\\/]?(?[^\\/]+)?") - albumRegex = regroup.MustCompile("\\/Music\\/Albums[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?") - genresRegex = regroup.MustCompile("\\/Music\\/Genres[\\/]?(?P[^\\/]+)?[\\/]?(?P[^/]+)?[\\/]?(?P[^\\/]+)?") + artistRegex = regroup.MustCompile("\\/Music\\/Artists[\\/]?(?P[^\\/]+)?[\\/]?(?[^\\/]+)?[\\/]?") + albumRegex = regroup.MustCompile("\\/Music\\/Albums[\\/]?(?P[^\\/]+)?[\\/]?") + genresRegex = regroup.MustCompile("\\/Music\\/Genres[\\/]?(?P[^\\/]+)?[\\/]?(?P[^/]+)?[\\/]?") recentRegex = regroup.MustCompile("\\/Music\\/Recently Added[\\/]?(?P[^\\/]+)?") - playlistRegex = regroup.MustCompile("\\/Music\\/Playlist[\\/]?(?P[^\\/]+)?[\\/]?(?P[^\\/]+)?") + playlistRegex = regroup.MustCompile("\\/Music\\/Playlists[\\/]?(?P[^\\/]+)?[\\/]?") } func (cds *contentDirectoryService) updateIDString() string { @@ -136,10 +136,7 @@ func handleDefault(ret []interface{}, cds *contentDirectoryService, o object, ho } func handleArtist(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { - if matchResults["ArtistAlbumTrack"] != "" { - //This is never hit as the URL is direct to the resourcePath - log.Debug("Artist Get a track ") - } else if matchResults["ArtistAlbum"] != "" { + if matchResults["ArtistAlbum"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) } else if matchResults["Artist"] != "" { @@ -167,9 +164,7 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten } func handleAlbum(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { - if matchResults["AlbumTrack"] != "" { - //This is never hit as the URL is direct to the streamPath - } else if matchResults["AlbumTitle"] != "" { + if matchResults["AlbumTitle"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) } else { @@ -191,9 +186,7 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content } func handleGenre(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { - if matchResults["GenreTrack"] != "" { - //This is never hit as the URL is direct to the streamPath - } else if matchResults["GenreArtist"] != "" { + if matchResults["GenreArtist"] != "" { tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ squirrel.Eq{"genre.id": matchResults["Genre"]}, squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, @@ -259,15 +252,15 @@ func handleRecent(matchResults map[string]string, ret []interface{}, cds *conten } func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { - if matchResults["PlaylistTrack"] != "" { - //This is never hit as the URL is direct to the streamPath - } else if matchResults["Playlist"] != "" { - log.Debug("Playlist only MATCH") - //x, xerr := cds.ds.Playlist(cds.ctx).Get(matchResults["Playlist"]) - return ret, nil - } else { - log.Debug("Playlist else MATCH") - indexes, err := cds.ds.Playlist(cds.ctx).GetAll() + if matchResults["Playlist"] != "" { + x, err := cds.ds.Playlist(cds.ctx).GetWithTracks(matchResults["Playlist"], false) + if err != nil { + log.Error("Error fetching playlist", "playlist", matchResults["Playlist"], err) + return ret, nil + } + return cds.doMediaFiles(x.MediaFiles(), o.Path, ret, host) + } + indexes, err := cds.ds.Playlist(cds.ctx).GetAll() if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) return nil, err @@ -280,27 +273,9 @@ func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *con ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil - } - return ret, nil } func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePath string, ret []interface{}, host string) ([]interface{}, error) { - //TODO flesh object out with actually useful metadata about the track - /* - - Love Takes Time - - Mariah Carey - 2000-01-01 - object.item.audioItem.musicTrack - Mariah Carey - #1's - Pop - 2 - http://172.30.0.4:8200/AlbumArt/24179-17759.jpg - http://172.30.0.4:8200/MediaItems/17759.mp3 - - */ for _, track := range tracks { trackDateAsTimeObject, _ := time.Parse(time.DateOnly, track.Date) From 91479ee483975889c4714a68f1bb9793427556fc Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 20:28:21 +0000 Subject: [PATCH 65/87] Removing elses where they're not needed etc --- dlna/contenddirectoryservice.go | 98 ++++++++++++++++----------------- 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 82b6980fc..6feb2f18f 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -167,20 +167,18 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content if matchResults["AlbumTitle"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) - } else { - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err + } + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil } @@ -212,20 +210,18 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } } - } else { - indexes, err := cds.ds.Genre(cds.ctx).GetAll() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err + } + indexes, err := cds.ds.Genre(cds.ctx).GetAll() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } return ret, nil } @@ -234,21 +230,20 @@ func handleRecent(matchResults map[string]string, ret []interface{}, cds *conten if matchResults["RecentAlbum"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["RecentAlbum"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) - } else { - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres(model.QueryOptions{Sort: "recently_added", Order: "desc", Max: 25}) - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil } + indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres(model.QueryOptions{Sort: "recently_added", Order: "desc", Max: 25}) + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), + } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil } func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { @@ -261,18 +256,18 @@ func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *con return cds.doMediaFiles(x.MediaFiles(), o.Path, ret, host) } indexes, err := cds.ds.Playlist(cds.ctx).GetAll() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for indexItem := range indexes { + child := object{ + Path: path.Join(o.Path, indexes[indexItem].Name), + Id: path.Join(o.Path, indexes[indexItem].ID), } - for indexItem := range indexes { - child := object{ - Path: path.Join(o.Path, indexes[indexItem].Name), - Id: path.Join(o.Path, indexes[indexItem].ID), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) - } - return ret, nil + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + return ret, nil } func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePath string, ret []interface{}, host string) ([]interface{}, error) { @@ -382,7 +377,6 @@ type browse struct { // ContentDirectory object from ObjectID. func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) { log.Debug("objectFromID called", "id", id) - o.Path, err = url.QueryUnescape(id) if err != nil { return From 9164d590718e38870073e6aad9c0c290304dd923 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 20:36:07 +0000 Subject: [PATCH 66/87] Another else done --- dlna/contenddirectoryservice.go | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 6feb2f18f..96b55aeb8 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -142,23 +142,21 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten } else if matchResults["Artist"] != "" { allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) return cds.doAlbums(allAlbumsForThisArtist, o.Path, ret, host) - } else { - indexes, err := cds.ds.Artist(cds.ctx).GetIndex() - if err != nil { - fmt.Printf("Error retrieving Indexes: %+v", err) - return nil, err - } - for letterIndex := range indexes { - for artist := range indexes[letterIndex].Artists { - artistId := indexes[letterIndex].Artists[artist].ID - child := object{ - Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), - Id: path.Join(o.Path, artistId), - } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + } + indexes, err := cds.ds.Artist(cds.ctx).GetIndex() + if err != nil { + fmt.Printf("Error retrieving Indexes: %+v", err) + return nil, err + } + for letterIndex := range indexes { + for artist := range indexes[letterIndex].Artists { + artistId := indexes[letterIndex].Artists[artist].ID + child := object{ + Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), + Id: path.Join(o.Path, artistId), } + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) } - return ret, nil } return ret, nil } From 7d94f2bf42647f425637eabdf0ccd54785573e05 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 16 Feb 2025 14:27:54 +0000 Subject: [PATCH 67/87] Placeholder sizes and real file sizes where known --- dlna/contenddirectoryservice.go | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 96b55aeb8..e3d96877e 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -52,7 +52,7 @@ func (cds *contentDirectoryService) updateIDString() string { // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. -func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, isContainer bool, host string) (ret interface{}) { +func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, isContainer bool, host string, filesize int64) (ret interface{}) { obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, @@ -68,8 +68,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is ChildCount: &defaultChildCount, } } - // Read the mime type from the fs.Object if possible, - // otherwise fall back to working out what it is from the file path. + var mimeType = "audio/mp3" //TODO obj.Class = "object.item.audioItem.musicTrack" @@ -89,7 +88,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: true, }.String()), - Size: uint64(1048576), //TODO + Size: uint64(filesize), }) ret = item @@ -103,7 +102,7 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] if o.Path == "/" || o.Path == "" { log.Debug("ReadContainer default route") newObject := object{Path: "/Music"} - ret = append(ret, cds.cdsObjectToUpnpavObject(newObject, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(newObject, true, host, -1)) return ret, nil } @@ -126,12 +125,12 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] } func handleDefault(ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host, -1)) return ret, nil } @@ -155,7 +154,7 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), Id: path.Join(o.Path, artistId), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } } return ret, nil @@ -176,7 +175,7 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } return ret, nil } @@ -205,7 +204,7 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content Path: path.Join(o.Path, artists[artistIndex].Name), Id: path.Join(o.Path, artists[artistIndex].ID), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } } } @@ -219,7 +218,7 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } return ret, nil } @@ -239,7 +238,7 @@ func handleRecent(matchResults map[string]string, ret []interface{}, cds *conten Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } return ret, nil } @@ -263,7 +262,7 @@ func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *con Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } return ret, nil } @@ -339,7 +338,7 @@ func (cds *contentDirectoryService) doAlbums(albums model.Albums, basepath strin Path: path.Join(basepath, album.Name), Id: path.Join(basepath, album.ID), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host)) + ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } return ret, nil } @@ -359,7 +358,8 @@ func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, hos Path: path.Join(oPath, file.Name()), Id: path.Join(oPath, file.Name()), } - ret = append(ret, cds.cdsObjectToUpnpavObject(child, file.IsDir(), host)) + fileInfo,_ := file.Info() + ret = append(ret, cds.cdsObjectToUpnpavObject(child, file.IsDir(), host, fileInfo.Size())) } return ret, nil } From bb0cd6df9e6fcd2a801677c704fb1b794ae3bf91 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 18 Feb 2025 00:50:58 +0000 Subject: [PATCH 68/87] Format --- dlna/contenddirectoryservice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index e3d96877e..16096a43b 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -358,7 +358,7 @@ func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, hos Path: path.Join(oPath, file.Name()), Id: path.Join(oPath, file.Name()), } - fileInfo,_ := file.Info() + fileInfo, _ := file.Info() ret = append(ret, cds.cdsObjectToUpnpavObject(child, file.IsDir(), host, fileInfo.Size())) } return ret, nil From a0ef9c55732234f9b328638978498fb3d9d330fd Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 25 Feb 2025 21:25:51 +0000 Subject: [PATCH 69/87] BFR has removed a few functions --- dlna/contenddirectoryservice.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 16096a43b..8f14aa4dd 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -165,7 +165,7 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) } - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres() + indexes, err := cds.ds.Album(cds.ctx).GetAll() if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) return nil, err @@ -228,7 +228,7 @@ func handleRecent(matchResults map[string]string, ret []interface{}, cds *conten tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["RecentAlbum"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) } - indexes, err := cds.ds.Album(cds.ctx).GetAllWithoutGenres(model.QueryOptions{Sort: "recently_added", Order: "desc", Max: 25}) + indexes, err := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Sort: "recently_added", Order: "desc", Max: 25}) if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) return nil, err @@ -245,7 +245,7 @@ func handleRecent(matchResults map[string]string, ret []interface{}, cds *conten func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { if matchResults["Playlist"] != "" { - x, err := cds.ds.Playlist(cds.ctx).GetWithTracks(matchResults["Playlist"], false) + x, err := cds.ds.Playlist(cds.ctx).GetWithTracks(matchResults["Playlist"], false, false) if err != nil { log.Error("Error fetching playlist", "playlist", matchResults["Playlist"], err) return ret, nil From d36e13f0246e263fd5f28f78d778a9f31a540a3b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 10 Mar 2025 18:13:34 +0000 Subject: [PATCH 70/87] Regen wire --- cmd/wire_gen.go | 2 +- go.sum | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 25d5c7585..9d1621a06 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -66,7 +66,7 @@ func CreateDLNAServer() *dlna.DLNAServer { transcodingCache := core.GetTranscodingCache() mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache) fileCache := artwork.GetImageCache() - agentsAgents := agents.New(dataStore) + agentsAgents := agents.GetAgents(dataStore) externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata) dlnaServer := dlna.New(dataStore, broker, mediaStreamer, artworkArtwork) diff --git a/go.sum b/go.sum index 3cd34a889..35d8b9490 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,7 @@ github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmI github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From 9c068f56beeda3240d7badb4f7745188e6cf9a54 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 13 Apr 2025 19:54:11 +0100 Subject: [PATCH 71/87] regenerated after merge --- cmd/wire_gen.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 9d1621a06..0c5533332 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -67,8 +67,8 @@ func CreateDLNAServer() *dlna.DLNAServer { mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache) fileCache := artwork.GetImageCache() agentsAgents := agents.GetAgents(dataStore) - externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents) - artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata) + provider := external.NewProvider(dataStore, agentsAgents) + artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) dlnaServer := dlna.New(dataStore, broker, mediaStreamer, artworkArtwork) return dlnaServer } From ede4acf91441216940620c8a206e14d7368d7782 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 13 Apr 2025 20:08:16 +0100 Subject: [PATCH 72/87] Pretty sure we only require the audio and images types here --- dlna/connectionmanagerservice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlna/connectionmanagerservice.go b/dlna/connectionmanagerservice.go index 8f868d3d7..492720bf7 100644 --- a/dlna/connectionmanagerservice.go +++ b/dlna/connectionmanagerservice.go @@ -8,7 +8,7 @@ import ( "github.com/anacrolix/dms/upnp" ) -const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*" +const defaultProtocolInfo = "http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*" type connectionManagerService struct { *DLNAServer From 34ccd8cf20164d12d1d5a8c20b17691cec6cd879 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 13 Apr 2025 20:26:40 +0100 Subject: [PATCH 73/87] This isn't and shouldn't be used --- dlna/contenddirectoryservice.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 8f14aa4dd..c3c7100d4 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -476,11 +476,6 @@ type object struct { Id string } -// Returns the actual local filesystem path for the object. -func (o *object) FilePath() string { - return filepath.FromSlash(o.Path) -} - // Returns the ObjectID for the object. This is used in various ContentDirectory actions. func (o object) ID() string { if o.Id != "" { From a54c3eb4cfbf2eb9502b162c6e7bf5ef7a0c8dc6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 10:36:54 +0100 Subject: [PATCH 74/87] Renaming "object" to make it more obvious it's part of the CDS --- dlna/contenddirectoryservice.go | 58 ++++++++++++++++----------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index c3c7100d4..14476b7fd 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -52,7 +52,7 @@ func (cds *contentDirectoryService) updateIDString() string { // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. -func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, isContainer bool, host string, filesize int64) (ret interface{}) { +func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject contentDirectoryObject, isContainer bool, host string, filesize int64) (ret interface{}) { obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, @@ -96,12 +96,12 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, is } // Returns all the upnpav objects in a directory. -func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) { +func (cds *contentDirectoryService) readContainer(o contentDirectoryObject, host string) (ret []interface{}, err error) { log.Debug(fmt.Sprintf("ReadContainer called '%s'", o)) if o.Path == "/" || o.Path == "" { log.Debug("ReadContainer default route") - newObject := object{Path: "/Music"} + newObject := contentDirectoryObject{Path: "/Music"} ret = append(ret, cds.cdsObjectToUpnpavObject(newObject, true, host, -1)) return ret, nil } @@ -124,17 +124,17 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return ret, nil } -func handleDefault(ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Files"}, true, host, -1)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Artists"}, true, host, -1)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Albums"}, true, host, -1)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Genres"}, true, host, -1)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Recently Added"}, true, host, -1)) - ret = append(ret, cds.cdsObjectToUpnpavObject(object{Path: "/Music/Playlists"}, true, host, -1)) +func handleDefault(ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { + ret = append(ret, cds.cdsObjectToUpnpavObject(contentDirectoryObject{Path: "/Music/Files"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(contentDirectoryObject{Path: "/Music/Artists"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(contentDirectoryObject{Path: "/Music/Albums"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(contentDirectoryObject{Path: "/Music/Genres"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(contentDirectoryObject{Path: "/Music/Recently Added"}, true, host, -1)) + ret = append(ret, cds.cdsObjectToUpnpavObject(contentDirectoryObject{Path: "/Music/Playlists"}, true, host, -1)) return ret, nil } -func handleArtist(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { +func handleArtist(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["ArtistAlbum"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["ArtistAlbum"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) @@ -150,7 +150,7 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten for letterIndex := range indexes { for artist := range indexes[letterIndex].Artists { artistId := indexes[letterIndex].Artists[artist].ID - child := object{ + child := contentDirectoryObject{ Path: path.Join(o.Path, indexes[letterIndex].Artists[artist].Name), Id: path.Join(o.Path, artistId), } @@ -160,7 +160,7 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten return ret, nil } -func handleAlbum(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { +func handleAlbum(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["AlbumTitle"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["AlbumTitle"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) @@ -171,7 +171,7 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content return nil, err } for indexItem := range indexes { - child := object{ + child := contentDirectoryObject{ Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } @@ -180,7 +180,7 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content return ret, nil } -func handleGenre(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { +func handleGenre(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["GenreArtist"] != "" { tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ squirrel.Eq{"genre.id": matchResults["Genre"]}, @@ -200,7 +200,7 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content return nil, err } for artistIndex := range artists { - child := object{ + child := contentDirectoryObject{ Path: path.Join(o.Path, artists[artistIndex].Name), Id: path.Join(o.Path, artists[artistIndex].ID), } @@ -214,7 +214,7 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content return nil, err } for indexItem := range indexes { - child := object{ + child := contentDirectoryObject{ Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } @@ -223,7 +223,7 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content return ret, nil } -func handleRecent(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { +func handleRecent(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["RecentAlbum"] != "" { tracks, _ := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": matchResults["RecentAlbum"]}}) return cds.doMediaFiles(tracks, o.Path, ret, host) @@ -234,7 +234,7 @@ func handleRecent(matchResults map[string]string, ret []interface{}, cds *conten return nil, err } for indexItem := range indexes { - child := object{ + child := contentDirectoryObject{ Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } @@ -243,7 +243,7 @@ func handleRecent(matchResults map[string]string, ret []interface{}, cds *conten return ret, nil } -func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o object, host string) ([]interface{}, error) { +func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["Playlist"] != "" { x, err := cds.ds.Playlist(cds.ctx).GetWithTracks(matchResults["Playlist"], false, false) if err != nil { @@ -258,7 +258,7 @@ func handlePlaylists(matchResults map[string]string, ret []interface{}, cds *con return nil, err } for indexItem := range indexes { - child := object{ + child := contentDirectoryObject{ Path: path.Join(o.Path, indexes[indexItem].Name), Id: path.Join(o.Path, indexes[indexItem].ID), } @@ -334,7 +334,7 @@ func floatToDurationString(totalSeconds32 float32) string { func (cds *contentDirectoryService) doAlbums(albums model.Albums, basepath string, ret []interface{}, host string) ([]interface{}, error) { for _, album := range albums { - child := object{ + child := contentDirectoryObject{ Path: path.Join(basepath, album.Name), Id: path.Join(basepath, album.ID), } @@ -354,7 +354,7 @@ func (cds *contentDirectoryService) doFiles(ret []interface{}, oPath string, hos files, _ := os.ReadDir(localFilePath) for _, file := range files { - child := object{ + child := contentDirectoryObject{ Path: path.Join(oPath, file.Name()), Id: path.Join(oPath, file.Name()), } @@ -373,7 +373,7 @@ type browse struct { } // ContentDirectory object from ObjectID. -func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) { +func (cds *contentDirectoryService) objectFromID(id string) (o contentDirectoryObject, err error) { log.Debug("objectFromID called", "id", id) o.Path, err = url.QueryUnescape(id) if err != nil { @@ -470,14 +470,14 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt } } -// Represents a ContentDirectory object. -type object struct { +// Represents a ContentDirectory contentDirectoryObject. +type contentDirectoryObject struct { Path string // The cleaned, absolute path for the object relative to the server. Id string } // Returns the ObjectID for the object. This is used in various ContentDirectory actions. -func (o object) ID() string { +func (o contentDirectoryObject) ID() string { if o.Id != "" { return o.Id } @@ -491,13 +491,13 @@ func (o object) ID() string { return url.QueryEscape(o.Path) } -func (o *object) IsRoot() bool { +func (o *contentDirectoryObject) IsRoot() bool { return o.Path == "/" } // Returns the object's parent ObjectID. Fortunately it can be deduced from the // ObjectID (for now). -func (o object) ParentID() string { +func (o contentDirectoryObject) ParentID() string { if o.IsRoot() { return "-1" } From 103a0603c5e99ec12f5c51e1f30027c9dade3922 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 10:40:55 +0100 Subject: [PATCH 75/87] I can't see a way within most implementations to get the client to specify the size of artwork, so I'll const it for now --- dlna/dlnaserver.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 73a270719..92a466fc3 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -39,6 +39,7 @@ const ( resourceStreamPath = "s" resourceArtPath = "a" serviceControlURL = "/ctl" + DLNAArtSize = 250 ) //go:embed static/* @@ -346,8 +347,7 @@ func (s *SSDPServer) resourceHandler(w http.ResponseWriter, r *http.Request) { log.Error("Failure to parse ArtworkId", "inputString", components[1], err) return } - //TODO size (250) - imgReader, lastUpdate, err := s.art.Get(r.Context(), artId, 250, true) + imgReader, lastUpdate, err := s.art.Get(r.Context(), artId, DLNAArtSize, true) if err != nil { log.Error("Failure to retrieve artwork", "artid", artId, err) return From 15c49433736252b685b2aaa6daf3eafa73eb9184 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 11:11:32 +0100 Subject: [PATCH 76/87] Renaming to make the difference more obvious during refactoring --- dlna/contenddirectoryservice.go | 16 ++++++++-------- dlna/upnpav/upnpav.go | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 14476b7fd..a62c8ec5f 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -53,7 +53,7 @@ func (cds *contentDirectoryService) updateIDString() string { // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject contentDirectoryObject, isContainer bool, host string, filesize int64) (ret interface{}) { - obj := upnpav.Object{ + obj := upnpav.UpnpObject{ ID: cdsObject.ID(), Restricted: 1, ParentID: cdsObject.ParentID(), @@ -64,7 +64,7 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject contentDir defaultChildCount := 1 obj.Class = "object.container.storageFolder" return upnpav.Container{ - Object: obj, + UpnpObject: obj, ChildCount: &defaultChildCount, } } @@ -75,8 +75,8 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject contentDir obj.Date = upnpav.Timestamp{Time: time.Now()} item := upnpav.Item{ - Object: obj, - Res: make([]upnpav.Resource, 0, 1), + UpnpObject: obj, + Res: make([]upnpav.Resource, 0, 1), } item.Res = append(item.Res, upnpav.Resource{ @@ -271,7 +271,7 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa for _, track := range tracks { trackDateAsTimeObject, _ := time.Parse(time.DateOnly, track.Date) - obj := upnpav.Object{ + obj := upnpav.UpnpObject{ ID: path.Join(basePath, track.ID), Restricted: 1, ParentID: basePath, @@ -296,8 +296,8 @@ func (cds *contentDirectoryService) doMediaFiles(tracks model.MediaFiles, basePa var mimeType = "audio/mp3" item := upnpav.Item{ - Object: obj, - Res: make([]upnpav.Resource, 0, 1), + UpnpObject: obj, + Res: make([]upnpav.Resource, 0, 1), } streamAccessPath := path.Join(resourcePath, resourceStreamPath, track.ID) @@ -470,7 +470,7 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt } } -// Represents a ContentDirectory contentDirectoryObject. +// Represents a contentDirectoryObject. type contentDirectoryObject struct { Path string // The cleaned, absolute path for the object relative to the server. Id string diff --git a/dlna/upnpav/upnpav.go b/dlna/upnpav/upnpav.go index 3a9616fad..3a23c3ce5 100644 --- a/dlna/upnpav/upnpav.go +++ b/dlna/upnpav/upnpav.go @@ -24,21 +24,21 @@ type Resource struct { // Container description type Container struct { - Object + UpnpObject XMLName xml.Name `xml:"container"` ChildCount *int `xml:"childCount,attr"` } // Item description type Item struct { - Object + UpnpObject XMLName xml.Name `xml:"item"` Res []Resource InnerXML string `xml:",innerxml"` } -// Object description -type Object struct { +// UpnpObject description +type UpnpObject struct { ID string `xml:"id,attr"` ParentID string `xml:"parentID,attr"` Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable From 60b2bc41498ef19b50d5f4723ee2a8d1154e7852 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 11:20:14 +0100 Subject: [PATCH 77/87] It seems not right for calling ParentID() to be messing with o.Path? --- dlna/contenddirectoryservice.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index a62c8ec5f..da61b45d1 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -501,6 +501,6 @@ func (o contentDirectoryObject) ParentID() string { if o.IsRoot() { return "-1" } - o.Path = path.Dir(o.Path) - return o.ID() + parentObject := contentDirectoryObject{Path: path.Dir(o.Path)} + return parentObject.ID() } From 198d748b41236402551613798cc142f720ead5ba Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 20 Apr 2025 13:01:41 +0100 Subject: [PATCH 78/87] Artist for a given Genre doesn't work now, IIRC I remember there being some chatter in #dev about it --- dlna/contenddirectoryservice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index da61b45d1..740330dd6 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -193,7 +193,7 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content } return cds.doMediaFiles(tracks, o.Path, ret, host) } else if matchResults["Genre"] != "" { - if matchResults["GenreArtist"] == "" { + if matchResults["GenreArtist"] == "" { //TODO, I think this isn't possible/obvious at the moment since the bfr. artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"genre.id": matchResults["Genre"]}}) if err != nil { fmt.Printf("Error retrieving artists for genre: %+v", err) From 4a51608d38e5d37a355fdfef19880797dbf645f5 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 20 Apr 2025 17:53:18 +0100 Subject: [PATCH 79/87] Adding queries that should return the rough shape we want for refernce, if we want want to implement this --- dlna/contenddirectoryservice.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 740330dd6..4c07559d7 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -182,6 +182,17 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content func handleGenre(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["GenreArtist"] != "" { + /* + SELECT + media_file.*, + json_extract(artist.value, '$.id') AS artist_id, + json_extract(genre.value, '$.id') AS genre_id + FROM + media_file, + json_each(media_file.tags, '$.genre') AS genre, + json_each(media_file.participants, '$.artist') AS artist + WHERE genre_id = $0 AND artist_id = $1 + */ tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ squirrel.Eq{"genre.id": matchResults["Genre"]}, squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, @@ -193,7 +204,21 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content } return cds.doMediaFiles(tracks, o.Path, ret, host) } else if matchResults["Genre"] != "" { - if matchResults["GenreArtist"] == "" { //TODO, I think this isn't possible/obvious at the moment since the bfr. + if matchResults["GenreArtist"] == "" { + /* + SELECT + json_extract(artist.value, '$.name') AS artist_name, + json_extract(artist.value, '$.id') AS artist_id, + json_extract(genre.value, '$.value') AS genre_name, + json_extract(genre.value, '$.id') AS genre_id + FROM + media_file, + json_each(media_file.tags, '$.genre') AS genre, + json_each(fmedia_file.participants, '$.artist') AS artist + WHERE genre_id = $0 + GROUP BY artist_id + */ + cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{}) artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"genre.id": matchResults["Genre"]}}) if err != nil { fmt.Printf("Error retrieving artists for genre: %+v", err) From d4431cfbdeea0e74b17f252eb4ac4ded2508c24c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 21 Apr 2025 17:42:09 +0100 Subject: [PATCH 80/87] Re-implementing the genre artists/tracks, as it looks like we can push enough restrictions into the repo to get this out. I would prefer to be able to do some sort of group-by for the "get artists by genre" path though --- dlna/contenddirectoryservice.go | 85 ++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 4c07559d7..0c0985515 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -22,6 +22,7 @@ import ( "github.com/navidrome/navidrome/dlna/upnpav" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/persistence" "github.com/oriser/regroup" ) @@ -183,21 +184,30 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content func handleGenre(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["GenreArtist"] != "" { /* - SELECT - media_file.*, - json_extract(artist.value, '$.id') AS artist_id, - json_extract(genre.value, '$.id') AS genre_id - FROM - media_file, - json_each(media_file.tags, '$.genre') AS genre, - json_each(media_file.participants, '$.artist') AS artist - WHERE genre_id = $0 AND artist_id = $1 + SELECT * FROM media_file WHERE + EXISTS ( + SELECT 1 FROM json_each(tags, '$.genre') + WHERE json_extract(value, '$.id') == '7bLYq0Np81m1Wgy5N31nuG' + ) + AND EXISTS ( + SELECT 1 FROM json_each(participants, '$.artist') + WHERE json_extract(value, '$.id') == '4CBFO1ymQXgsbXQgV2aPMI' + ) + LIMIT 100 */ - tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.And{ - squirrel.Eq{"genre.id": matchResults["Genre"]}, - squirrel.Eq{"artist_id": matchResults["GenreArtist"]}, - }, - }) + + thisFilter := squirrel.And{ + persistence.Exists("json_tree(tags, '$.genre')", squirrel.And{ + squirrel.Eq{"key": "id"}, + squirrel.Eq{"value": matchResults["Genre"]}, + }), + persistence.Exists("json_tree(participants, '$.artist')", squirrel.And{ + squirrel.Eq{"key": "id"}, + squirrel.Eq{"value": matchResults["GenreArtist"]}, + }), + } + + tracks, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: thisFilter}) if err != nil { fmt.Printf("Error retrieving tracks for artist and genre: %+v", err) return nil, err @@ -206,31 +216,48 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content } else if matchResults["Genre"] != "" { if matchResults["GenreArtist"] == "" { /* + // slightly cleaner query: + SELECT - json_extract(artist.value, '$.name') AS artist_name, - json_extract(artist.value, '$.id') AS artist_id, - json_extract(genre.value, '$.value') AS genre_name, - json_extract(genre.value, '$.id') AS genre_id + json_extract(a.value, '$.name') artist_name, + json_extract(a.value, '$.id') artist_id, + COUNT(*) FROM - media_file, - json_each(media_file.tags, '$.genre') AS genre, - json_each(fmedia_file.participants, '$.artist') AS artist - WHERE genre_id = $0 - GROUP BY artist_id + media_file f, + json_each(f.tags, '$.genre') as g, + json_each(f.participants, '$.artist') as a + WHERE + json_extract(g.value, '$.id') = '7bLYq0Np81m1Wgy5N31nuG' + GROUP BY artist_id; */ - cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{}) - artists, err := cds.ds.Artist(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"genre.id": matchResults["Genre"]}}) + + thisFilter := persistence.Exists("json_tree(tags, '$.genre')", squirrel.And{ + squirrel.Eq{"key": "id"}, + squirrel.Eq{"value": matchResults["Genre"]}, + }) + mediaFiles, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: thisFilter }) + if err != nil { fmt.Printf("Error retrieving artists for genre: %+v", err) return nil, err } - for artistIndex := range artists { - child := contentDirectoryObject{ - Path: path.Join(o.Path, artists[artistIndex].Name), - Id: path.Join(o.Path, artists[artistIndex].ID), + + artistsFound := make(map[string]string) + for fileIndex := range mediaFiles { + artists := mediaFiles[fileIndex].Participants.AllArtists() + for index := range artists { + artistsFound[artists[index].ID] = artists[index].Name + } + } + + for artistId := range artistsFound { + child := contentDirectoryObject { + Path: path.Join(o.Path, artistsFound[artistId]), + Id: path.Join(o.Path, artistId), } ret = append(ret, cds.cdsObjectToUpnpavObject(child, true, host, -1)) } + return ret, nil } } indexes, err := cds.ds.Genre(cds.ctx).GetAll() From 0ccb2989fa4378d1659686b5c3c0bf22ac620b40 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 21 Apr 2025 17:48:27 +0100 Subject: [PATCH 81/87] go tidy --- dlna/contenddirectoryservice.go | 52 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 0c0985515..df62f1000 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -184,24 +184,24 @@ func handleAlbum(matchResults map[string]string, ret []interface{}, cds *content func handleGenre(matchResults map[string]string, ret []interface{}, cds *contentDirectoryService, o contentDirectoryObject, host string) ([]interface{}, error) { if matchResults["GenreArtist"] != "" { /* - SELECT * FROM media_file WHERE - EXISTS ( - SELECT 1 FROM json_each(tags, '$.genre') - WHERE json_extract(value, '$.id') == '7bLYq0Np81m1Wgy5N31nuG' + SELECT * FROM media_file WHERE + EXISTS ( + SELECT 1 FROM json_each(tags, '$.genre') + WHERE json_extract(value, '$.id') == '7bLYq0Np81m1Wgy5N31nuG' + ) + AND EXISTS ( + SELECT 1 FROM json_each(participants, '$.artist') + WHERE json_extract(value, '$.id') == '4CBFO1ymQXgsbXQgV2aPMI' ) - AND EXISTS ( - SELECT 1 FROM json_each(participants, '$.artist') - WHERE json_extract(value, '$.id') == '4CBFO1ymQXgsbXQgV2aPMI' - ) - LIMIT 100 + LIMIT 100 */ thisFilter := squirrel.And{ - persistence.Exists("json_tree(tags, '$.genre')", squirrel.And{ + persistence.Exists("json_tree(tags, '$.genre')", squirrel.And{ squirrel.Eq{"key": "id"}, squirrel.Eq{"value": matchResults["Genre"]}, }), - persistence.Exists("json_tree(participants, '$.artist')", squirrel.And{ + persistence.Exists("json_tree(participants, '$.artist')", squirrel.And{ squirrel.Eq{"key": "id"}, squirrel.Eq{"value": matchResults["GenreArtist"]}, }), @@ -216,26 +216,26 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content } else if matchResults["Genre"] != "" { if matchResults["GenreArtist"] == "" { /* - // slightly cleaner query: + // slightly cleaner query: - SELECT - json_extract(a.value, '$.name') artist_name, - json_extract(a.value, '$.id') artist_id, - COUNT(*) - FROM - media_file f, - json_each(f.tags, '$.genre') as g, - json_each(f.participants, '$.artist') as a - WHERE - json_extract(g.value, '$.id') = '7bLYq0Np81m1Wgy5N31nuG' - GROUP BY artist_id; + SELECT + json_extract(a.value, '$.name') artist_name, + json_extract(a.value, '$.id') artist_id, + COUNT(*) + FROM + media_file f, + json_each(f.tags, '$.genre') as g, + json_each(f.participants, '$.artist') as a + WHERE + json_extract(g.value, '$.id') = '7bLYq0Np81m1Wgy5N31nuG' + GROUP BY artist_id; */ - thisFilter := persistence.Exists("json_tree(tags, '$.genre')", squirrel.And{ + thisFilter := persistence.Exists("json_tree(tags, '$.genre')", squirrel.And{ squirrel.Eq{"key": "id"}, squirrel.Eq{"value": matchResults["Genre"]}, }) - mediaFiles, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: thisFilter }) + mediaFiles, err := cds.ds.MediaFile(cds.ctx).GetAll(model.QueryOptions{Filters: thisFilter}) if err != nil { fmt.Printf("Error retrieving artists for genre: %+v", err) @@ -251,7 +251,7 @@ func handleGenre(matchResults map[string]string, ret []interface{}, cds *content } for artistId := range artistsFound { - child := contentDirectoryObject { + child := contentDirectoryObject{ Path: path.Join(o.Path, artistsFound[artistId]), Id: path.Join(o.Path, artistId), } From c8938da42d3e039355251304c67e4de780180148 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 22 May 2025 18:10:07 +0100 Subject: [PATCH 82/87] Signature changed --- dlna/contenddirectoryservice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index df62f1000..34852dc1e 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -143,7 +143,7 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) return cds.doAlbums(allAlbumsForThisArtist, o.Path, ret, host) } - indexes, err := cds.ds.Artist(cds.ctx).GetIndex() + indexes, err := cds.ds.Artist(cds.ctx).GetIndex(false) if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) return nil, err From 4e0d6a4306ba259f5d34f175b3a37afd185bf3b7 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 13 Jul 2025 17:32:54 +0100 Subject: [PATCH 83/87] Should be an info, not warn --- dlna/dlnaserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlna/dlnaserver.go b/dlna/dlnaserver.go index 92a466fc3..2a7d5e997 100644 --- a/dlna/dlnaserver.go +++ b/dlna/dlnaserver.go @@ -124,7 +124,7 @@ func New(ds model.DataStore, broker events.Broker, mediastreamer core.MediaStrea // 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") + log.Info("Starting DLNA Server") s.ctx = ctx if s.ssdp.HTTPConn == nil { From 99228474472b46f1d8a7ebda23b6b87a2ec3d28a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 12 Oct 2025 18:43:49 +0100 Subject: [PATCH 84/87] Fixing screwups during rebase --- cmd/wire_gen.go | 9 +++++++-- go.mod | 3 ++- go.sum | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 0c5533332..0708455e1 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -8,7 +8,6 @@ package cmd import ( "context" - "github.com/google/wire" "github.com/navidrome/navidrome/adapters/lastfm" "github.com/navidrome/navidrome/adapters/listenbrainz" @@ -66,7 +65,9 @@ func CreateDLNAServer() *dlna.DLNAServer { transcodingCache := core.GetTranscodingCache() mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache) fileCache := artwork.GetImageCache() - agentsAgents := agents.GetAgents(dataStore) + metricsMetrics := metrics.GetPrometheusInstance(dataStore) + manager := plugins.GetManager(dataStore, metricsMetrics) + agentsAgents := agents.GetAgents(dataStore, manager) provider := external.NewProvider(dataStore, agentsAgents) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) dlnaServer := dlna.New(dataStore, broker, mediaStreamer, artworkArtwork) @@ -222,7 +223,11 @@ func getPluginManager() *plugins.Manager { // wire_injectors.go: +<<<<<<< HEAD var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(core.PluginUnloader), new(*plugins.Manager)), wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) +======= +var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, dlna.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, plugins.GetManager, metrics.GetPrometheusInstance, db.Db, wire.Bind(new(agents.PluginLoader), new(plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)), wire.Bind(new(metrics.PluginLoader), new(plugins.Manager)), wire.Bind(new(core.Scanner), new(scanner.Scanner)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) +>>>>>>> e237eb27 (Fixing screwups during rebase) func GetPluginManager(ctx context.Context) *plugins.Manager { manager := getPluginManager() diff --git a/go.mod b/go.mod index f20c30297..1c159e8af 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/onsi/gomega v1.39.1 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.37.0 + github.com/onsi/gomega v1.38.0 github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e github.com/pelletier/go-toml/v2 v2.2.4 github.com/pocketbase/dbx v1.11.0 @@ -89,6 +90,7 @@ require ( github.com/anacrolix/dms v1.7.1 // indirect github.com/anacrolix/generics v0.0.1 // indirect github.com/anacrolix/log v0.15.2 // indirect + github.com/atombender/go-jsonschema v0.20.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/reflex v0.3.1 // indirect @@ -125,7 +127,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ogier/pflag v0.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect diff --git a/go.sum b/go.sum index 35d8b9490..16355b67a 100644 --- a/go.sum +++ b/go.sum @@ -214,6 +214,8 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= +github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e h1:cL0lMYYEbfEUBghQd4ytnl8B8Ktdm+JremTyAagegZ0= From 3636c3a513228cc5e2e3999f8457a8ab7c552dca Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 12 Oct 2025 23:18:00 +0100 Subject: [PATCH 85/87] Only the Artists collection requires a library id? --- dlna/contenddirectoryservice.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dlna/contenddirectoryservice.go b/dlna/contenddirectoryservice.go index 34852dc1e..16af0e9d0 100644 --- a/dlna/contenddirectoryservice.go +++ b/dlna/contenddirectoryservice.go @@ -143,7 +143,8 @@ func handleArtist(matchResults map[string]string, ret []interface{}, cds *conten allAlbumsForThisArtist, _ := cds.ds.Album(cds.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": matchResults["Artist"]}}) return cds.doAlbums(allAlbumsForThisArtist, o.Path, ret, host) } - indexes, err := cds.ds.Artist(cds.ctx).GetIndex(false) + libraries, _ := cds.ds.Library(cds.ctx).GetAll() + indexes, err := cds.ds.Artist(cds.ctx).GetIndex(false, libraries.IDs()) if err != nil { fmt.Printf("Error retrieving Indexes: %+v", err) return nil, err From c2068c6025fc9899015e23f987458b41308f33bd Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 13 Oct 2025 17:24:52 +0100 Subject: [PATCH 86/87] go mod tidy --- Makefile | 4 ++-- go.sum | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 81aef6f10..f436c95e3 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ test-js: ##@Development Run JS tests .PHONY: test-js test-i18n: ##@Development Validate all translations files - ./.github/workflows/validate-translations.sh + ./.github/workflows/validate-translations.sh .PHONY: test-i18n install-golangci-lint: ##@Development Install golangci-lint if not present @@ -146,7 +146,7 @@ setup-git: ##@Development Setup Git hooks (pre-commit and pre-push) build: check_go_env buildjs buildgo ##@Build Build the project .PHONY: build -buildgo: +buildgo: go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)" -tags=netgo .PHONY: buildgo diff --git a/go.sum b/go.sum index 16355b67a..72e2d61a3 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,6 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= -github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY= -github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8= github.com/anacrolix/dms v1.7.1 h1:XVOpT3eoO5Ds34B1X+TE3R2ApfqGGeqotEoCVNP8BaI= github.com/anacrolix/dms v1.7.1/go.mod h1:excFJW5MKBhn5yt5ZMyeE9iFVqnO6tEGQl7YG/2tUoQ= github.com/anacrolix/generics v0.0.1 h1:4WVhK6iLb3UAAAQP6I3uYlMOHcp9FqJC9j4n81Wv9Ks= @@ -20,6 +16,8 @@ github.com/anacrolix/log v0.15.2 h1:LTSf5Wm6Q4GNWPFMBP7NPYV6UBVZzZLKckL+/Lj72Oo= github.com/anacrolix/log v0.15.2/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY= +github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -121,9 +119,6 @@ github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66S github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno= github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -216,8 +211,6 @@ github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e h1:cL0lMYYEbfEUBghQd4ytnl8B8Ktdm+JremTyAagegZ0= github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e/go.mod h1:tUOeYZJlwO7jSmM5ko1jTCiQaWQMvh58IENEfjwYzh8= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= From 28127b6c1b51f89d52adbff4f0babad11bf8c1f7 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 15 Feb 2026 19:38:06 +0000 Subject: [PATCH 87/87] go mod tidy --- go.mod | 5 ----- go.sum | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/go.mod b/go.mod index 1c159e8af..a41942da6 100644 --- a/go.mod +++ b/go.mod @@ -52,9 +52,6 @@ require ( github.com/mileusna/useragent v1.3.5 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 - github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.37.0 - github.com/onsi/gomega v1.38.0 github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e github.com/pelletier/go-toml/v2 v2.2.4 github.com/pocketbase/dbx v1.11.0 @@ -86,8 +83,6 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/atombender/go-jsonschema v0.20.0 // indirect - github.com/anacrolix/dms v1.7.1 // indirect github.com/anacrolix/generics v0.0.1 // indirect github.com/anacrolix/log v0.15.2 // indirect github.com/atombender/go-jsonschema v0.20.0 // indirect diff --git a/go.sum b/go.sum index 72e2d61a3..63ea251cb 100644 --- a/go.sum +++ b/go.sum @@ -207,10 +207,6 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= -github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e h1:cL0lMYYEbfEUBghQd4ytnl8B8Ktdm+JremTyAagegZ0= github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e/go.mod h1:tUOeYZJlwO7jSmM5ko1jTCiQaWQMvh58IENEfjwYzh8= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -345,12 +341,6 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=