2025-09-09 19:44:11 +02:00
|
|
|
|
package pmoupnp
|
2025-09-06 17:45:28 +02:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"net/http"
|
2025-09-07 07:59:26 +02:00
|
|
|
|
"runtime"
|
2025-09-06 17:45:28 +02:00
|
|
|
|
"sync"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/beevik/etree"
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/netutils"
|
2025-09-07 22:08:41 +02:00
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmoapp"
|
2025-09-09 09:07:48 +02:00
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmoconfig"
|
|
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmocover"
|
2025-09-07 07:59:26 +02:00
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmolog"
|
|
|
|
|
|
"gargoton.petite-maison-orange.fr/eric/pmomusic/ssdp"
|
2025-09-06 17:45:28 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type Server struct {
|
|
|
|
|
|
name string
|
|
|
|
|
|
HTTPPort int
|
|
|
|
|
|
baseURL string
|
|
|
|
|
|
|
|
|
|
|
|
Logger *log.Logger
|
|
|
|
|
|
httpSrv *http.Server
|
2025-09-07 07:59:26 +02:00
|
|
|
|
sspd *ssdp.SSDPServer
|
2025-09-06 17:45:28 +02:00
|
|
|
|
|
|
|
|
|
|
devices DeviceInstanceSet
|
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
|
startOnce sync.Once
|
|
|
|
|
|
stopOnce sync.Once
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewServer(name string, opts ...ServerOption) *Server {
|
2025-09-09 09:07:48 +02:00
|
|
|
|
config := pmoconfig.GetConfig()
|
2025-09-06 17:45:28 +02:00
|
|
|
|
|
|
|
|
|
|
baseURL := config.GetBaseURL()
|
|
|
|
|
|
httpPort := config.GetHTTPPort()
|
|
|
|
|
|
|
|
|
|
|
|
if baseURL == "" {
|
|
|
|
|
|
ip, err := netutils.GuessLocalIP()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
panic(fmt.Errorf("unable to determine local IP: %w", err))
|
|
|
|
|
|
}
|
|
|
|
|
|
baseURL = fmt.Sprintf("http://%s:%d", ip, httpPort)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s := &Server{
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
HTTPPort: httpPort,
|
|
|
|
|
|
baseURL: baseURL,
|
|
|
|
|
|
Logger: log.New(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
|
opt(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) Name() string { return s.name }
|
|
|
|
|
|
func (s *Server) TypeID() string { return "Server" }
|
|
|
|
|
|
|
|
|
|
|
|
type ServerOption func(*Server)
|
|
|
|
|
|
|
|
|
|
|
|
func WithLogger(l *log.Logger) ServerOption {
|
|
|
|
|
|
return func(s *Server) {
|
|
|
|
|
|
s.Logger = l
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) Start() error {
|
|
|
|
|
|
s.startOnce.Do(func() {
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
|
|
|
|
|
|
s.mu.RLock()
|
|
|
|
|
|
|
2025-09-09 09:07:48 +02:00
|
|
|
|
cover_cache, err := pmocover.GetCoverCache()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Panicf("❌ Cannot initialize the Cover Cache")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Info("✅ Cover cache activated")
|
|
|
|
|
|
|
|
|
|
|
|
cover_cache.ServeMux(mux)
|
2025-09-07 22:08:41 +02:00
|
|
|
|
pmoapp.Handler(mux)
|
2025-09-06 17:45:28 +02:00
|
|
|
|
|
|
|
|
|
|
s.httpSrv = &http.Server{
|
|
|
|
|
|
Addr: fmt.Sprintf(":%d", s.HTTPPort),
|
|
|
|
|
|
Handler: mux,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for device := range s.devices.All() {
|
|
|
|
|
|
err := device.RegisterURLs()
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Panicf("❌ Cannot register URLs: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
if err := s.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
|
|
|
|
s.Logger.Printf("❌ server error: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
log.Infof("✅ UPnP server started on %s", s.baseURL)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) Stop(ctx context.Context) error {
|
|
|
|
|
|
var err error
|
|
|
|
|
|
s.stopOnce.Do(func() {
|
|
|
|
|
|
if s.httpSrv != nil {
|
|
|
|
|
|
s.Logger.Println("✅ Shutting down UPNP server...")
|
|
|
|
|
|
err = s.httpSrv.Shutdown(ctx)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) Run(ctx context.Context) error {
|
|
|
|
|
|
if err := s.Start(); err != nil {
|
2025-09-07 07:59:26 +02:00
|
|
|
|
return fmt.Errorf("❌ failed to start server: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 08:18:31 +02:00
|
|
|
|
pmolog.LoggerWeb(ctx, s.httpSrv.Handler.(*http.ServeMux))
|
|
|
|
|
|
|
2025-09-07 07:59:26 +02:00
|
|
|
|
s.sspd = ssdp.NewSSDPServer()
|
|
|
|
|
|
if err := s.sspd.Start(ctx); err != nil {
|
|
|
|
|
|
return fmt.Errorf("❌ failed to start SSDP server: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for d := range s.devices.All() {
|
|
|
|
|
|
d.RegisterSSPD()
|
2025-09-07 16:17:24 +02:00
|
|
|
|
|
|
|
|
|
|
for svc := range d.services.All() {
|
|
|
|
|
|
svc.StartNotifier(ctx, 1*time.Second)
|
|
|
|
|
|
}
|
2025-09-06 17:45:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// attente d’annulation du contexte
|
|
|
|
|
|
<-ctx.Done()
|
|
|
|
|
|
|
|
|
|
|
|
// arrêt avec le même ctx ou un nouveau ctx avec timeout
|
|
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
return s.Stop(shutdownCtx)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) BaseURL() string { return s.baseURL }
|
|
|
|
|
|
|
|
|
|
|
|
// ServeXML prend un générateur de XML (*etree.Element)
|
|
|
|
|
|
// et renvoie la string XML avec header.
|
|
|
|
|
|
func (s *Server) XML(gen func() *etree.Element) (string, error) {
|
|
|
|
|
|
root := gen()
|
|
|
|
|
|
doc := etree.NewDocument()
|
|
|
|
|
|
doc.SetRoot(root)
|
|
|
|
|
|
|
|
|
|
|
|
doc.Indent(2)
|
|
|
|
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
|
|
if _, err := doc.WriteTo(buf); err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ajoute le header XML
|
|
|
|
|
|
return `<?xml version="1.0" encoding="utf-8"?>` + "\n" + buf.String(), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Server) ServeXML(gen func() *etree.Element) func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
xmlStr, err := s.XML(gen)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, "failed to generate XML", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-07 07:59:26 +02:00
|
|
|
|
|
|
|
|
|
|
osName := runtime.GOOS
|
|
|
|
|
|
arch := runtime.GOARCH
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Server", fmt.Sprintf(
|
|
|
|
|
|
"%s/%s UPnP/1.1 PMOMusic/1.0",
|
|
|
|
|
|
osName, arch,
|
|
|
|
|
|
))
|
|
|
|
|
|
w.Header().Set("Connection", "close")
|
|
|
|
|
|
w.Header().Set("Cache-Control", "max-age=1800")
|
|
|
|
|
|
w.Header().Set("EXT", "")
|
|
|
|
|
|
w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"")
|
2025-09-06 17:45:28 +02:00
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
|
w.Write([]byte(xmlStr))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|