From e311f2b44ffa6f359dfe167dd1c869dfa372725b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 19:49:38 +0000 Subject: [PATCH 01/83] BUilds --- Makefile | 7 +++++-- cmd/root.go | 8 ++++++++ cmd/wire_gen.go | 11 ++++++++++- cmd/wire_injectors.go | 8 ++++++++ go.sum | 5 +++-- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index c6ff60c97..dbcb44b94 100644 --- a/Makefile +++ b/Makefile @@ -90,10 +90,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 e63b52bdd..21dfc5d71 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -76,6 +76,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)) @@ -134,6 +135,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 d44b78ed8..0452e3d0a 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -20,6 +20,7 @@ import ( "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/dlna" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server" @@ -50,6 +51,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() *nativeapi.Router { sqlDB := db.Db() dataStore := persistence.New(sqlDB) @@ -170,4 +179,4 @@ func GetPlaybackServer() playback.PlaybackServer { // wire_injectors.go: -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.NewWatcher, metrics.NewPrometheusInstance, db.Db) +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.NewWatcher, metrics.NewPrometheusInstance, db.Db) diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go index c431945dc..e10467d98 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -13,6 +13,7 @@ import ( "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/db" + "github.com/navidrome/navidrome/dlna" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/scanner" @@ -27,6 +28,7 @@ var allProviders = wire.NewSet( core.Set, artwork.Set, server.New, + dlna.New, subsonic.New, nativeapi.New, public.New, @@ -52,6 +54,12 @@ func CreateServer() *server.Server { )) } +func CreateDLNAServer() *dlna.DLNAServer { + panic(wire.Build( + allProviders, + )) +} + func CreateNativeAPIRouter() *nativeapi.Router { panic(wire.Build( allProviders, diff --git a/go.sum b/go.sum index 198379a28..2295638f3 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,7 @@ github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdx github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc= github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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= @@ -250,8 +251,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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 de44696976e840a7b3a412067a89da036b58a424 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 19:49:49 +0000 Subject: [PATCH 02/83] 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 c7eaa7ab26222b532c4f549c508c8c7d7fa9846a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 20:11:22 +0000 Subject: [PATCH 03/83] Compiles, ssdp --- dlna/dlnaserver.go | 214 ++++++++++++++++++++++++++++++++++++++++++++- go.mod | 3 + go.sum | 9 ++ 3 files changed, 225 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 edd5006ec..91e14f8ff 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,9 @@ require ( ) 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/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index 2295638f3..90ba0726b 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,17 @@ 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= +<<<<<<< HEAD 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/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= +>>>>>>> 8b4e4a9a (Compiles, ssdp) 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 12600792b1c5757806a67bda1b4164ff7b5c5a30 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 20:20:20 +0000 Subject: [PATCH 04/83] 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 b7617f63e91d187ea83f3f7a126e04a86deca1a0 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Wed, 1 Jan 2025 20:33:49 +0000 Subject: [PATCH 05/83] 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 3924c11f5aa8df8f91b35c473cfe4c3941efb533 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:04:40 +0000 Subject: [PATCH 06/83] 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 e1a4f1c0627ea48d6a5329376913923b4d2aa739 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:09:54 +0000 Subject: [PATCH 07/83] 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 7df8dbd99281639d86e912127ffe87bbdfa56ce1 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:22:06 +0000 Subject: [PATCH 08/83] 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 e8ebf0683141f59ad230af086f8cc70d7d300c5b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Thu, 2 Jan 2025 10:40:43 +0000 Subject: [PATCH 09/83] 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/83] 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/83] 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/83] 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 6c6afd544b369cc15aff0ea039e8d0602e50f4a2 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 00:31:30 +0000 Subject: [PATCH 13/83] 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 09:26:27 +0000 Subject: [PATCH 14/83] Fixing merge screwup --- go.sum | 3 --- 1 file changed, 3 deletions(-) diff --git a/go.sum b/go.sum index 90ba0726b..78122dfdd 100644 --- a/go.sum +++ b/go.sum @@ -4,17 +4,14 @@ 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= -<<<<<<< HEAD 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/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= ->>>>>>> 8b4e4a9a (Compiles, ssdp) 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 70c10930904371aca90df730b03943931e91c5c8 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 10:26:23 +0000 Subject: [PATCH 15/83] 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 0f22d09ddd01bcb8965d3ce5eeade90dd0b24ef2 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 10:27:29 +0000 Subject: [PATCH 16/83] 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 c83224e847b415b0f263839e351107b55f8ac008 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 10:30:41 +0000 Subject: [PATCH 17/83] 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 583566ed167bf745fc516769f43b1ed5fce7bce3 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 10:48:45 +0000 Subject: [PATCH 18/83] 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 09412ac131886ed3c2ebb91fcb5249ff15162b0b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 11:06:17 +0000 Subject: [PATCH 19/83] 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 169b3242df65a289d9b24fcfe3f0494cfd77fd0d Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 11:09:11 +0000 Subject: [PATCH 20/83] /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 c73ca60d3cc15617fa2c1b795ad21b448f2873d1 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 15:51:07 +0000 Subject: [PATCH 21/83] 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 c0df96c6d9c5907c39c666de192e1672e12e85fd Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 3 Jan 2025 16:15:44 +0000 Subject: [PATCH 22/83] 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 317605050f1295f4d60b8b5be3403387291d4074 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 00:07:19 +0000 Subject: [PATCH 23/83] 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 88ae58d8a5d8f5765f785a3cce32f060eb8661e8 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 10:52:42 +0000 Subject: [PATCH 24/83] 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 175991beebe80d6dbe331a9a338c695bed11c904 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 11:02:53 +0000 Subject: [PATCH 25/83] 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 7509c0f564f1d5ee4bbb255ee2f424fbc2a59a68 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 13:51:06 +0000 Subject: [PATCH 26/83] 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 570d7063ff94e776fd8b2a5201a845ea50104563 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 5 Jan 2025 14:18:02 +0000 Subject: [PATCH 27/83] 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 21dfc5d71..6e160e14e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -138,7 +138,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 5e9779aa6950d222b825d511d4f90fa9e0021c58 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 12:29:29 +0000 Subject: [PATCH 28/83] 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 6e160e14e..4a88fd450 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -76,7 +76,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)) @@ -372,6 +375,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")) @@ -386,6 +391,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("transcodingcachesize", rootCmd.Flags().Lookup("transcodingcachesize")) _ = viper.BindPFlag("imagecachesize", rootCmd.Flags().Lookup("imagecachesize")) diff --git a/conf/configuration.go b/conf/configuration.go index 93388ee8c..a8a0eaf9c 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -95,6 +95,7 @@ type configOptions struct { Backup backupOptions PID pidOptions Inspect inspectOptions + DLNAServer dlnaServerOptions Agents string LastFM lastfmOptions @@ -197,6 +198,10 @@ type inspectOptions struct { BacklogTimeout int } +type dlnaServerOptions struct { + Enabled bool +} + var ( Server = &configOptions{} hooks []func() @@ -517,6 +522,8 @@ func init() { viper.SetDefault("inspect.backloglimit", consts.RequestThrottleBacklogLimit) viper.SetDefault("inspect.backlogtimeout", consts.RequestThrottleBacklogTimeout) + 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 5b721085380b20d264a267ca150aabb3f5b33942 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 12:35:57 +0000 Subject: [PATCH 29/83] 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 e932e0e38b4edce0b1307bb989aafff5527708e3 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 15:49:04 +0000 Subject: [PATCH 30/83] 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 713b6c575be114d45444825a3da9663c8f4c4bcb Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 16:31:01 +0000 Subject: [PATCH 31/83] 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 77cf5ccacf06dc7b6a19863bf1bcaa908c3c1d5a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 17:33:46 +0000 Subject: [PATCH 32/83] 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 bb3cbce72a1bda199624fcc53d7e311ec6c3c319 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 17:38:05 +0000 Subject: [PATCH 33/83] 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 96503694f3855d0befa75f3b307677e5c197aa99 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 17:43:47 +0000 Subject: [PATCH 34/83] 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 23bb6ea712216592ca36852fb6ce36aeddd2e98c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 18:12:10 +0000 Subject: [PATCH 35/83] 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 cecf054c202362fc22d8be1fbf2319284c84d244 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 18:43:41 +0000 Subject: [PATCH 36/83] 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 becbdedff0062fbf8c6589d1b34e3394241b4b2b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 11 Jan 2025 18:51:16 +0000 Subject: [PATCH 37/83] 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 5c405a24f81b053258d9fd75c595ec22295a5fa9 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 12 Jan 2025 17:39:45 +0000 Subject: [PATCH 38/83] 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 bd7df889bbcd615bed55ea06a4cbe94e7c50a4de Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 17:23:44 +0000 Subject: [PATCH 39/83] 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, 146 insertions(+), 108 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 91e14f8ff..9a848b742 100644 --- a/go.mod +++ b/go.mod @@ -97,6 +97,7 @@ require ( github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.1 // indirect github.com/prometheus/common v0.62.0 // indirect diff --git a/go.sum b/go.sum index 78122dfdd..7af6924bb 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,14 @@ 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/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/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= @@ -160,6 +160,8 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +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.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -257,6 +259,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= From cdb40ecd3e085a245b48cfaccfa2b62631c54065 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 17:44:57 +0000 Subject: [PATCH 40/83] 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 9e72bc3c0a7f83896d5a9ff1476c9c114a3461fe Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 18:09:33 +0000 Subject: [PATCH 41/83] 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 8c56815e7a18940937d81696f101cc21554b2e89 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 18 Jan 2025 18:16:48 +0000 Subject: [PATCH 42/83] 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 3a65833b578bc82490de278342d0bcf496c15b8b Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 19 Jan 2025 18:00:50 +0000 Subject: [PATCH 43/83] 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 537f4a149097f85a9da3d9d27758582c20b82389 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 19 Jan 2025 18:23:11 +0000 Subject: [PATCH 44/83] 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 19bd6aefa25e94e9a2b9d170f96f88837241b286 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 19 Jan 2025 19:27:14 +0000 Subject: [PATCH 45/83] 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 0ff8cbfd17fcca5aff4d4b1e09c9b419f6bca24a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 16:35:06 +0000 Subject: [PATCH 46/83] 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 9078ed764206e8bcaa5fcbaec3b527b1ae96a14c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 16:57:52 +0000 Subject: [PATCH 47/83] 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 e8301cf864b87cf7f619406f86a3d7673de60a13 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:07:18 +0000 Subject: [PATCH 48/83] 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 8c89957826dc360fced36f846aee352380c24b25 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:13:01 +0000 Subject: [PATCH 49/83] 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 c8ab92b94252e6d5309984eaa95d2aa36163de54 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:49:21 +0000 Subject: [PATCH 50/83] 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 ca38e079867d3e3d16612393e23de25522ea5531 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:50:50 +0000 Subject: [PATCH 51/83] 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 0ffb8a90919bf3fdcf5bd43bd6e7a046580ad4eb Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 17:59:43 +0000 Subject: [PATCH 52/83] 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 1b74a2b37f7aad6e1b93c15b877127e3707c5aef Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 18:19:38 +0000 Subject: [PATCH 53/83] 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 5b6869e6fe5df05b1fb4754b8fac11c8820598aa Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 2 Feb 2025 18:23:53 +0000 Subject: [PATCH 54/83] 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 849dcdd85da11e81a84c3964acb594d3439d0157 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 3 Feb 2025 16:21:52 +0000 Subject: [PATCH 55/83] 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 fd2456d63e000a0ba8636f83e6fe5acc087f3e7a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 3 Feb 2025 16:57:45 +0000 Subject: [PATCH 56/83] 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 0452e3d0a..aad9b4ba3 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -52,10 +52,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 545b266c7f67e7dc274099cf9c4509fc1d533c24 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 16:52:15 +0000 Subject: [PATCH 57/83] 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 36dafe4889e9fc65c419d04931e5c892d420cb35 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 17:27:26 +0000 Subject: [PATCH 58/83] 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 9b32a233983e49b1127fb25cf8d0c1b4db1c9f0c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 18:01:02 +0000 Subject: [PATCH 59/83] 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 746efc266e36f8ef3f80d8e216292c79cded27b0 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 18:45:24 +0000 Subject: [PATCH 60/83] 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 4a88fd450..b709d1be4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -78,7 +78,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 42e7545dc726c02f47a596eb249f1f809d05c6dd Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 18:50:49 +0000 Subject: [PATCH 61/83] 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 98d2b9558e2b0275457df4375911c2cff0c2fb91 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 19:05:44 +0000 Subject: [PATCH 62/83] 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 8e9a4c9e4558af20d40969c9a91636acf1db0b29 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 19:09:57 +0000 Subject: [PATCH 63/83] 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 473d96edda6d69ff734ea03df04b03f2f6b0cd85 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 19:14:59 +0000 Subject: [PATCH 64/83] Formatting --- dlna/contenddirectoryservice.go | 8 +- dlna/dlnaserver.go | 146 ++++++++++++++++---------------- dlna/upnpav/upnpav.go | 27 +++--- go.mod | 4 +- go.sum | 3 - 5 files changed, 93 insertions(+), 95 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 9a848b742..0b6398126 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ replace github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/ 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.8.1 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 @@ -41,6 +42,7 @@ require ( github.com/mileusna/useragent v1.3.5 github.com/onsi/ginkgo/v2 v2.22.2 github.com/onsi/gomega v1.36.2 + github.com/oriser/regroup v0.0.0-20240925165441-f6bb0e08289e github.com/pelletier/go-toml/v2 v2.2.3 github.com/pocketbase/dbx v1.11.0 github.com/pressly/goose/v3 v3.24.1 @@ -66,7 +68,6 @@ require ( ) 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/aymerick/douceur v0.2.0 // indirect @@ -97,7 +98,6 @@ require ( github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.1 // indirect github.com/prometheus/common v0.62.0 // indirect diff --git a/go.sum b/go.sum index 7af6924bb..5ba66f1d9 100644 --- a/go.sum +++ b/go.sum @@ -83,7 +83,6 @@ github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdx github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc= github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -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= @@ -261,8 +260,6 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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 f29db5c9dcadd46d2c09936c48e004ac1e591d89 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 20:00:10 +0000 Subject: [PATCH 65/83] 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 c36b6d59e87224f42429c2a80f0044df0705ee53 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 20:28:21 +0000 Subject: [PATCH 66/83] 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 1fe148dd4b488872176ff8daba87f442a7ec35d6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 8 Feb 2025 20:36:07 +0000 Subject: [PATCH 67/83] 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 c4678791040cc535faa36002d3500984dd879c41 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 16 Feb 2025 14:27:54 +0000 Subject: [PATCH 68/83] 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 93b645237939e46d1105177249e0b4622a37f667 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 18 Feb 2025 00:50:58 +0000 Subject: [PATCH 69/83] 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 95ad710a3bd2368f913f82314f56177742a4901a Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 25 Feb 2025 21:25:51 +0000 Subject: [PATCH 70/83] 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 ec530f9c3f12c930b72d78c4c3d2c5e6695ba7a2 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 10 Mar 2025 18:13:34 +0000 Subject: [PATCH 71/83] Regen wire --- cmd/wire_gen.go | 4 ++-- go.sum | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 4fb1f0d79..4f6b1e52d 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -19,8 +19,8 @@ import ( "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/db" - "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/dlna" + "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server" @@ -59,7 +59,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 19e870c13..1c4060ecd 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,7 @@ github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdx github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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= @@ -261,6 +262,8 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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 6b970bd0fa7723eeec04836d3b6fc1585f067afe Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 10 Mar 2025 19:11:10 +0000 Subject: [PATCH 72/83] make format --- go.sum | 3 --- 1 file changed, 3 deletions(-) diff --git a/go.sum b/go.sum index 1c4060ecd..19e870c13 100644 --- a/go.sum +++ b/go.sum @@ -84,7 +84,6 @@ github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdx github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -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= @@ -262,8 +261,6 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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 528a5818da41b95d0c4d584273baebe7e68a436d Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 13 Apr 2025 19:54:11 +0100 Subject: [PATCH 73/83] 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 3ccd9b67a..3636bf7af 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -61,8 +61,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 405096fa824a6bc344f33a9c86bcfd0086a09a5f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 13 Apr 2025 20:08:16 +0100 Subject: [PATCH 74/83] 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 e33bf62c12ad04bf71743744befe23e5ee495e66 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 13 Apr 2025 20:26:40 +0100 Subject: [PATCH 75/83] 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 9892be3e361bc6fdbb1cf8937275ecab801b973e Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 10:36:54 +0100 Subject: [PATCH 76/83] 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 713895696fb2461585a9015d37d8db7d6835018d Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 10:40:55 +0100 Subject: [PATCH 77/83] 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 b535847ba9360b3ccb99408097ba092f597635a1 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 11:11:32 +0100 Subject: [PATCH 78/83] 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 c1fde0f4e4145143adcf2be0eb5bad598bde5e1f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 19 Apr 2025 11:20:14 +0100 Subject: [PATCH 79/83] 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 9befc4abab0c0d767a30a5212e605a41fcd6654c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 20 Apr 2025 13:01:41 +0100 Subject: [PATCH 80/83] 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 441439fb679e9916cf179a6f0b69bbec53720785 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 20 Apr 2025 17:53:18 +0100 Subject: [PATCH 81/83] 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 5a41fb13b865e6878ab2f6a80176a2dcae8d9a02 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 21 Apr 2025 17:42:09 +0100 Subject: [PATCH 82/83] 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 6bbb0dae15c736f8a58b218168f8a68e971c1de5 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 21 Apr 2025 17:48:27 +0100 Subject: [PATCH 83/83] 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), }