package playerserver import ( "context" "embed" "encoding/json" "fmt" "io/fs" "math/rand" "net" "net/http" "os" "strconv" "strings" "time" ) //go:embed assets var assetsFS embed.FS // PlaylistItem is a single displayable content item fetched from the backend. type PlaylistItem struct { Src string `json:"src"` Type string `json:"type"` // web | image | video | pdf Title string `json:"title,omitempty"` DurationSeconds int `json:"duration_seconds"` } // NowPlaying describes the current playback state returned to the browser. type NowPlaying struct { // Playlist holds the ordered items to display (primary). Playlist []PlaylistItem `json:"playlist,omitempty"` // URL is a legacy single-URL fallback (kept for backwards compatibility). URL string `json:"url,omitempty"` Status string `json:"status"` Connectivity string `json:"connectivity"` } // InfoItem is a single entry shown in the lower-third sysinfo overlay. type InfoItem struct { Label string `json:"label"` Value string `json:"value"` } // SysInfo holds the items shown in the lower-third overlay. type SysInfo struct { Items []InfoItem `json:"items"` } // Server serves the local player UI to Chromium. type Server struct { listenAddr string nowFn func() NowPlaying startupToken string // zufälliger Token der sich bei jedem Start ändert } // New creates a Server. listenAddr is e.g. "127.0.0.1:8090". // nowFn is called on each request and returns the current playback state. func New(listenAddr string, nowFn func() NowPlaying) *Server { token := fmt.Sprintf("%016x", rand.Uint64()) return &Server{listenAddr: listenAddr, nowFn: nowFn, startupToken: token} } // Run starts the HTTP server and blocks until ctx is cancelled. func (s *Server) Run(ctx context.Context) error { sub, err := fs.Sub(assetsFS, "assets") if err != nil { return err } mux := http.NewServeMux() mux.HandleFunc("GET /player", s.handlePlayer) mux.HandleFunc("GET /api/now-playing", s.handleNowPlaying) mux.HandleFunc("GET /api/sysinfo", handleSysInfo) mux.HandleFunc("GET /api/startup-token", s.handleStartupToken) mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub)))) srv := &http.Server{Handler: mux} ln, err := net.Listen("tcp", s.listenAddr) if err != nil { return err } go func() { <-ctx.Done() srv.Close() }() if err := srv.Serve(ln); err != http.ErrServerClosed { return err } return nil } func (s *Server) handlePlayer(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(playerHTML)) //nolint:errcheck } func (s *Server) handleNowPlaying(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(s.nowFn()) //nolint:errcheck } // handleStartupToken gibt einen zufälligen Token zurück der sich bei jedem // Agent-Start ändert. Der Browser erkennt daran, dass der Agent neu gestartet // wurde und lädt die Seite automatisch neu. func (s *Server) handleStartupToken(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"token": s.startupToken}) //nolint:errcheck } func handleSysInfo(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(collectSysInfo()) //nolint:errcheck } // collectSysInfo builds the list of info items shown in the overlay. // To add more items, append to the slice here. func collectSysInfo() SysInfo { var items []InfoItem if h, err := os.Hostname(); err == nil { items = append(items, InfoItem{Label: "Hostname", Value: h}) } if up := readUptime(); up != "" { items = append(items, InfoItem{Label: "Uptime", Value: up}) } return SysInfo{Items: items} } func readUptime() string { data, err := os.ReadFile("/proc/uptime") if err != nil { return "" } parts := strings.Fields(string(data)) if len(parts) == 0 { return "" } secs, err := strconv.ParseFloat(parts[0], 64) if err != nil { return "" } d := time.Duration(secs) * time.Second days := int(d.Hours()) / 24 hours := int(d.Hours()) % 24 mins := int(d.Minutes()) % 60 if days > 0 { return fmt.Sprintf("%dd %dh %dm", days, hours, mins) } if hours > 0 { return fmt.Sprintf("%dh %dm", hours, mins) } return fmt.Sprintf("%dm", mins) } const playerHTML = `