Changes to be committed:
modified: cmd/pmomusic/main.go modified: go.mod modified: go.sum modified: internal/ssdp/responder.go modified: internal/ssdp/ssdp.go new file: internal/ssdp/uuid.go renamed: internal/upnp/http.go -> internal/upnp/description_xml.go modified: internal/upnp/device.go renamed: web/index.html -> internal/upnp/html/index.html modified: internal/upnp/server.go modified: internal/upnp/service.go new file: internal/upnp/xml/Info.xml new file: internal/upnp/xml/Playlist.xml new file: internal/upnp/xml/Product.xml
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/netutils"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/ssdp"
|
||||
@@ -15,14 +15,27 @@ func main() {
|
||||
log.Fatalf("Could not guess local IP: %v", err)
|
||||
}
|
||||
|
||||
location := "http://" + localIP + ":1400/description.xml"
|
||||
usn := "uuid:pmomusic-renderer-001"
|
||||
usn := ssdp.GetUUID()
|
||||
|
||||
desc := upnp.NewDevice(localIP, 1400, usn, "pmomusic Fake Renderer", "pmomusic")
|
||||
|
||||
desc.RegisterService("urn:schemas-upnp-org:service:AVTransport:1")
|
||||
desc.RegisterService("urn:schemas-upnp-org:service:RenderingControl:1")
|
||||
desc.RegisterService("urn:schemas-upnp-org:service:ConnectionManager:1")
|
||||
|
||||
desc.RegisterService("urn:av-openhome-org:service:Product:1")
|
||||
desc.RegisterService("urn:av-openhome-org:service:Playlist:1")
|
||||
desc.RegisterService("urn:av-openhome-org:service:Info:1")
|
||||
|
||||
location := "http://" + localIP + ":1400/description.xml"
|
||||
log.Printf("Server UUID: %s", usn)
|
||||
|
||||
log.Info(desc.GenerateXML())
|
||||
// Démarre la diffusion SSDP
|
||||
go ssdp.AnnounceRenderer(usn, location)
|
||||
go ssdp.StartSSDPResponder(usn, location)
|
||||
go ssdp.AnnounceRenderer(desc)
|
||||
go ssdp.StartSSDPResponder(desc)
|
||||
|
||||
// Lance le serveur HTTP
|
||||
log.Println("Starting HTTP server at:", location)
|
||||
upnp.StartHTTPServer(usn, localIP, 1400)
|
||||
upnp.StartHTTPServer(desc)
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -2,7 +2,11 @@ module gargoton.petite-maison-orange.fr/eric/pmomusic
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require github.com/koron/go-ssdp v0.0.6
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/koron/go-ssdp v0.0.6
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,6 +1,22 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU=
|
||||
github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/upnp"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -13,8 +16,19 @@ const (
|
||||
serviceType = "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
)
|
||||
|
||||
func fromUDPAddress(src *net.UDPAddr) string {
|
||||
from := src.String()
|
||||
|
||||
hostnames, err := net.LookupAddr(src.IP.String())
|
||||
if err == nil {
|
||||
from = strings.TrimSuffix(hostnames[0], ".") + ":" + strconv.Itoa(src.Port)
|
||||
}
|
||||
|
||||
return from
|
||||
}
|
||||
|
||||
// StartSSDPResponder listens for M-SEARCH requests and responds if they match
|
||||
func StartSSDPResponder(usn, location string) {
|
||||
func StartSSDPResponder(device *upnp.DeviceDescription) {
|
||||
addr, err := net.ResolveUDPAddr("udp4", ssdpAddr)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to resolve SSDP address:", err)
|
||||
@@ -41,7 +55,8 @@ func StartSSDPResponder(usn, location string) {
|
||||
data := string(buf[:n])
|
||||
if strings.HasPrefix(data, "M-SEARCH * HTTP/1.1") {
|
||||
st := extractHeader(data, "ST")
|
||||
log.Printf("🔎 M-SEARCH from %s, ST=%s\n", src.String(), st)
|
||||
|
||||
log.Printf("🔎 M-SEARCH from %s, ST=%s\n", fromUDPAddress(src), st)
|
||||
|
||||
if st == "ssdp:all" ||
|
||||
st == serviceType ||
|
||||
@@ -49,27 +64,27 @@ func StartSSDPResponder(usn, location string) {
|
||||
strings.HasPrefix(st, "urn:av-openhome-org:service:") ||
|
||||
strings.HasPrefix(st, "urn:bubblesoftapps-com:service:") {
|
||||
|
||||
go sendSSDPResponse(src, usn, location, st)
|
||||
go sendSSDPResponse(src, device, st)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendSSDPResponse(dst *net.UDPAddr, usn, location, st string) {
|
||||
func sendSSDPResponse(dst *net.UDPAddr, device *upnp.DeviceDescription, st string) {
|
||||
resp := fmt.Sprintf(
|
||||
"HTTP/1.1 200 OK\r\n"+
|
||||
"CACHE-CONTROL: max-age=1800\r\n"+
|
||||
"DATE: %s\r\n"+
|
||||
"EXT:\r\n"+
|
||||
"LOCATION: %s\r\n"+
|
||||
"LOCATION: %s:%d\r\n"+
|
||||
"SERVER: pmomusic/1.0 UPnP/1.1 DLNARenderer/1.0\r\n"+
|
||||
"ST: %s\r\n"+
|
||||
"USN: %s::%s\r\n"+
|
||||
"\r\n",
|
||||
time.Now().Format(time.RFC1123),
|
||||
location,
|
||||
device.IP, device.Port,
|
||||
st,
|
||||
usn,
|
||||
device.USN,
|
||||
st,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@ package ssdp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/internal/upnp"
|
||||
"github.com/koron/go-ssdp"
|
||||
)
|
||||
|
||||
func AnnounceRenderer(usn, location string) {
|
||||
func AnnounceRenderer(device *upnp.DeviceDescription) {
|
||||
st := "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
server := "pmomusic/1.0 UPnP/1.1 DLNARenderer/1.0"
|
||||
maxAge := 1800
|
||||
|
||||
_, err := ssdp.Advertise(st, usn+"::"+st, location, server, maxAge)
|
||||
_, err := ssdp.Advertise(st, device.USN+"::"+st, device.IP+":"+strconv.Itoa(int(device.Port)), server, maxAge)
|
||||
if err != nil {
|
||||
log.Println("SSDP advertise error:", err)
|
||||
return
|
||||
|
||||
42
internal/ssdp/uuid.go
Normal file
42
internal/ssdp/uuid.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package ssdp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var __UUID = ""
|
||||
|
||||
func loadOrCreateUUID() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath := filepath.Join(configDir, "pmomusic", "uuid.txt")
|
||||
os.MkdirAll(filepath.Dir(filePath), 0755)
|
||||
|
||||
if data, err := os.ReadFile(filePath); err == nil && len(data) > 0 {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
id := "uuid:pmo-" + uuid.New().String()
|
||||
if err := os.WriteFile(filePath, []byte(id), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func GetUUID() string {
|
||||
if __UUID == "" {
|
||||
u, err := loadOrCreateUUID()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
__UUID = u
|
||||
}
|
||||
return __UUID
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateDeviceDescription(usn, ip string, port uint) string {
|
||||
func generateDeviceDescription(device *DeviceDescription) string {
|
||||
return fmt.Sprintf(`<?xml version="1.0"?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
@@ -45,7 +45,28 @@ func generateDeviceDescription(usn, ip string, port uint) string {
|
||||
<eventSubURL>/upnp/event/ConnectionManager</eventSubURL>
|
||||
<SCPDURL>/scpd/ConnectionManager.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:av-openhome-org:service:Product:1</serviceType>
|
||||
<serviceId>urn:av-openhome-org:serviceId:Product</serviceId>
|
||||
<controlURL>/upnp/control/Product</controlURL>
|
||||
<eventSubURL>/upnp/event/Product</eventSubURL>
|
||||
<SCPDURL>/scpd/Product.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:av-openhome-org:service:Playlist:1</serviceType>
|
||||
<serviceId>urn:av-openhome-org:serviceId:Playlist</serviceId>
|
||||
<controlURL>/upnp/control/Playlist</controlURL>
|
||||
<eventSubURL>/upnp/event/Playlist</eventSubURL>
|
||||
<SCPDURL>/scpd/Playlist.xml</SCPDURL>
|
||||
</service>
|
||||
<service>
|
||||
<serviceType>urn:av-openhome-org:service:Info:1</serviceType>
|
||||
<serviceId>urn:av-openhome-org:serviceId:Info</serviceId>
|
||||
<controlURL>/upnp/control/Info</controlURL>
|
||||
<eventSubURL>/upnp/event/Info</eventSubURL>
|
||||
<SCPDURL>/scpd/Info.xml</SCPDURL>
|
||||
</service>
|
||||
</serviceList>
|
||||
</device>
|
||||
</root>`, ip, port, usn, ip, port)
|
||||
</root>`, device.IP, device.Port, device.USN, device.IP, device.Port)
|
||||
}
|
||||
@@ -1 +1,93 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type DeviceDescription struct {
|
||||
IP string
|
||||
Port uint
|
||||
USN string
|
||||
Friendly string
|
||||
ModelName string
|
||||
Services *ServiceList
|
||||
}
|
||||
|
||||
const deviceTemplate = `<?xml version="1.0"?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<URLBase>http://{{.IP}}:{{.Port}}/</URLBase>
|
||||
<device>
|
||||
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
||||
<friendlyName>{{.Friendly}}</friendlyName>
|
||||
<manufacturer>GoDLNA</manufacturer>
|
||||
<manufacturerURL>http://example.com</manufacturerURL>
|
||||
<modelDescription>Fake DLNA Renderer</modelDescription>
|
||||
<modelName>{{.ModelName}}</modelName>
|
||||
<modelNumber>1.0</modelNumber>
|
||||
<modelURL>http://example.com/model</modelURL>
|
||||
<UDN>{{.USN}}</UDN>
|
||||
<presentationURL>http://{{.IP}}:{{.Port}}/</presentationURL>
|
||||
{{.ServicesXML}}
|
||||
</device>
|
||||
</root>`
|
||||
|
||||
func NewDevice(ip string, port uint, usn string, friendly string, modelname string) *DeviceDescription {
|
||||
services := NewServiceList()
|
||||
return &DeviceDescription{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
USN: usn,
|
||||
Friendly: friendly,
|
||||
ModelName: modelname,
|
||||
Services: services,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeviceDescription) RegisterService(service string) {
|
||||
d.Services.Append(service)
|
||||
}
|
||||
|
||||
func (d *DeviceDescription) GenerateXML() (string, error) {
|
||||
servicesXML, err := d.Services.GenerateServiceListXML()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to generate service list XML: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
tmpl, err := template.New("device").Parse(deviceTemplate)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to parse device description template: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := struct {
|
||||
IP string
|
||||
Port uint
|
||||
USN string
|
||||
Friendly string
|
||||
ModelName string
|
||||
ServicesXML string // trusted raw XML
|
||||
}{
|
||||
IP: d.IP,
|
||||
Port: d.Port,
|
||||
USN: d.USN,
|
||||
Friendly: d.Friendly,
|
||||
ModelName: d.ModelName,
|
||||
ServicesXML: servicesXML,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
log.Errorf("Failed to execute template: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
//go:embed xml/*.xml
|
||||
var embeddedXML embed.FS
|
||||
|
||||
//go:embed html/*.html
|
||||
var embeddedHTML embed.FS
|
||||
|
||||
// ServeStaticXML mounts handlers for SCPD XML files.
|
||||
func ServeStaticXML(mux *http.ServeMux) {
|
||||
subFS, err := fs.Sub(embeddedXML, "xml")
|
||||
@@ -22,13 +25,28 @@ func ServeStaticXML(mux *http.ServeMux) {
|
||||
mux.Handle("/scpd/", http.StripPrefix("/scpd/", http.FileServer(http.FS(subFS))))
|
||||
}
|
||||
|
||||
func StartHTTPServer(usn, ip string, port uint) {
|
||||
func ServeStaticHTML(mux *http.ServeMux) {
|
||||
subFS, err := fs.Sub(embeddedHTML, "html")
|
||||
if err != nil {
|
||||
panic("failed to create sub FS: " + err.Error())
|
||||
}
|
||||
mux.Handle("/", http.FileServer(http.FS(subFS)))
|
||||
}
|
||||
|
||||
func StartHTTPServer(device *DeviceDescription) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Device description
|
||||
mux.HandleFunc("/description.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/xml")
|
||||
w.Write([]byte(generateDeviceDescription(usn, ip, port)))
|
||||
xml, err := device.GenerateXML()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(xml))
|
||||
})
|
||||
|
||||
// SOAP control endpoints
|
||||
@@ -38,8 +56,9 @@ func StartHTTPServer(usn, ip string, port uint) {
|
||||
|
||||
// Serve static SCPD XML files
|
||||
ServeStaticXML(mux)
|
||||
ServeStaticHTML(mux)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
addr := fmt.Sprintf("%s:%d", device.IP, device.Port)
|
||||
log.Printf("Serving UPnP fake renderer at http://%s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, mux))
|
||||
}
|
||||
|
||||
@@ -1 +1,115 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type ServiceGroup string
|
||||
|
||||
const (
|
||||
UPnP ServiceGroup = "upnp"
|
||||
OpenHome ServiceGroup = "openhome"
|
||||
Other ServiceGroup = "other"
|
||||
)
|
||||
|
||||
type ServiceDescription struct {
|
||||
Group ServiceGroup
|
||||
Type string
|
||||
Name string
|
||||
ID string
|
||||
URLPrefix string
|
||||
XML string // auto-rempli temporairement pour le template parent
|
||||
}
|
||||
|
||||
const serviceTemplate = `
|
||||
<service>
|
||||
<serviceType>{{.Type}}</serviceType>
|
||||
<serviceId>{{.ID}}</serviceId>
|
||||
<controlURL>{{.URLPrefix}}/control</controlURL>
|
||||
<eventSubURL>{{.URLPrefix}}/event</eventSubURL>
|
||||
<SCPDURL>{{.URLPrefix}}.xml</SCPDURL>
|
||||
</service>
|
||||
`
|
||||
|
||||
func NewServiceDescription(serviceType string) *ServiceDescription {
|
||||
parts := strings.Split(serviceType, ":")
|
||||
name := parts[len(parts)-2]
|
||||
|
||||
var group ServiceGroup
|
||||
switch {
|
||||
case strings.Contains(serviceType, "schemas-upnp-org"):
|
||||
group = UPnP
|
||||
case strings.Contains(serviceType, "av-openhome-org"):
|
||||
group = OpenHome
|
||||
default:
|
||||
group = Other
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("urn:%s:serviceId:%s", string(group), name)
|
||||
urlPrefix := fmt.Sprintf("/%s/%s", group, name)
|
||||
|
||||
return &ServiceDescription{
|
||||
Group: group,
|
||||
Type: serviceType,
|
||||
Name: name,
|
||||
ID: id,
|
||||
URLPrefix: urlPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (desc *ServiceDescription) GenerateServiceXML() (string, error) {
|
||||
tmpl, err := template.New("service").Parse(strings.TrimSpace(serviceTemplate))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, desc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
type ServiceList struct {
|
||||
Services []*ServiceDescription
|
||||
}
|
||||
|
||||
func NewServiceList() *ServiceList {
|
||||
return &ServiceList{
|
||||
Services: make([]*ServiceDescription, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *ServiceList) Append(serviceType string) {
|
||||
svc := NewServiceDescription(serviceType)
|
||||
sl.Services = append(sl.Services, svc)
|
||||
}
|
||||
|
||||
func (sl *ServiceList) GenerateServiceListXML() (string, error) {
|
||||
const listTemplate = `
|
||||
<serviceList>
|
||||
{{range .Services}}{{.XML}}
|
||||
{{end}}</serviceList>`
|
||||
|
||||
// Génère tous les XML individuellement
|
||||
for _, svc := range sl.Services {
|
||||
xml, err := svc.GenerateServiceXML()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
svc.XML = xml
|
||||
}
|
||||
|
||||
// Template principal
|
||||
tmpl, err := template.New("list").Parse(strings.TrimSpace(listTemplate))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, sl); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
111
internal/upnp/xml/Info.xml
Normal file
111
internal/upnp/xml/Info.xml
Normal file
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
|
||||
<actionList>
|
||||
<action>
|
||||
<name>Track</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Uri</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackUri</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Metadata</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackMetadata</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Details</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Duration</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackDuration</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>BitRate</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackBitRate</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>BitDepth</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackBitDepth</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>SampleRate</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackSampleRate</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Lossless</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackLossless</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>CodecName</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackCodecName</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Metatext</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Metatext</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackUri</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackMetadata</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackDuration</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackBitRate</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackBitDepth</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackSampleRate</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackLossless</name>
|
||||
<dataType>boolean</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TrackCodecName</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>Metatext</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
89
internal/upnp/xml/Playlist.xml
Normal file
89
internal/upnp/xml/Playlist.xml
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
|
||||
<actionList>
|
||||
<action>
|
||||
<name>Play</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>TransportState</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Pause</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Stop</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Next</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Previous</name>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Insert</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>AfterId</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>TrackId</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Uri</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Uri</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Metadata</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Metadata</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>NewId</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>TrackId</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>TransportState</name>
|
||||
<dataType>string</dataType>
|
||||
<allowedValueList>
|
||||
<allowedValue>Playing</allowedValue>
|
||||
<allowedValue>Paused</allowedValue>
|
||||
<allowedValue>Stopped</allowedValue>
|
||||
</allowedValueList>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Uri</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Metadata</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>TrackId</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
86
internal/upnp/xml/Product.xml
Normal file
86
internal/upnp/xml/Product.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
|
||||
<actionList>
|
||||
<action>
|
||||
<name>Manufacturer</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Manufacturer</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Model</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Model</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Product</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>Product</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>Standby</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Standby</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
|
||||
<action>
|
||||
<name>SetStandby</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Value</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>Standby</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Manufacturer</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Model</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="no">
|
||||
<name>Product</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>Standby</name>
|
||||
<dataType>boolean</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
||||
Reference in New Issue
Block a user