Files
pmomusic/pmoupnp/server.go

205 lines
4.4 KiB
Go
Raw Normal View History

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"
"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)
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 dannulation 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))
}
}