🎉 live server seems to be working now

This commit is contained in:
2026-05-14 14:29:57 +02:00
commit d72e439fd9
181 changed files with 47406 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
// ABOUTME: Entry point for Sendspin Protocol server
// ABOUTME: Thin CLI wrapper around pkg/sendspin.Server with TUI support
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/Sendspin/sendspin-go/internal/server"
"github.com/Sendspin/sendspin-go/pkg/sendspin"
)
var (
port = flag.Int("port", 8927, "WebSocket server port")
name = flag.String("name", "", "Server friendly name (default: hostname-sendspin-server)")
logFile = flag.String("log-file", "sendspin-server.log", "Log file path")
debug = flag.Bool("debug", false, "Enable debug logging")
noMDNS = flag.Bool("no-mdns", false, "Disable mDNS advertisement")
noTUI = flag.Bool("no-tui", false, "Disable TUI, use streaming logs instead")
audioFile = flag.String("audio", "", "Audio source to stream (MP3, FLAC, HTTP URL, HLS). Default: test tone")
discoverClients = flag.Bool("discover-clients", false, "Enable server-initiated discovery: browse _sendspin._tcp and dial out to clients")
daemon = flag.Bool("daemon", false, "Daemon mode: log to stdout only (journalctl-friendly), no TUI, no log file")
configPath = flag.String("config", "", "Path to server.yaml config file. Default search: $SENDSPIN_SERVER_CONFIG, ~/.config/sendspin/server.yaml, /etc/sendspin/server.yaml.")
)
func main() {
flag.Parse()
// Overlay YAML file and SENDSPIN_SERVER_* env vars onto flag vars for
// anything the user didn't set on the CLI. --config is excluded because
// putting it in the config file would be circular.
setByUser := map[string]bool{"config": true}
flag.Visit(func(f *flag.Flag) { setByUser[f.Name] = true })
cfg, _, err := sendspin.LoadServerConfig(*configPath)
if err != nil {
log.Fatalf("config: %v", err)
}
if err := sendspin.ApplyEnvAndFile(flag.CommandLine, setByUser, sendspin.ServerEnvPrefix, cfg.AsStringMap()); err != nil {
log.Fatalf("config overlay: %v", err)
}
useTUI := !(*noTUI || *daemon)
if *daemon {
// Daemon mode: log to stdout only. systemd/journalctl captures stdout
// and adds its own timestamps, so we keep ours for grep-ability.
log.SetOutput(os.Stdout)
} else {
f, err := os.OpenFile(*logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
log.Fatalf("error opening log file: %v", err)
}
defer f.Close()
if useTUI {
// Log to file only when TUI is running; otherwise the log would stomp the TUI
log.SetOutput(f)
} else {
log.SetOutput(io.MultiWriter(os.Stdout, f))
}
}
serverName := *name
if serverName == "" {
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
}
serverName = fmt.Sprintf("%s-sendspin-server", hostname)
}
if !useTUI {
log.Printf("Starting Sendspin Server: %s on port %d", serverName, *port)
if *daemon {
log.Printf("Daemon mode: logging to stdout only")
}
}
var source sendspin.AudioSource
if *audioFile == "" {
source = sendspin.NewTestTone(sendspin.DefaultSampleRate, sendspin.DefaultChannels)
} else {
internalSource, err := server.NewAudioSource(*audioFile)
if err != nil {
log.Fatalf("Failed to create audio source: %v", err)
}
source = internalSource
}
srv, err := sendspin.NewServer(sendspin.ServerConfig{
Port: *port,
Name: serverName,
Source: source,
EnableMDNS: !*noMDNS,
Debug: *debug,
DiscoverClients: *discoverClients,
})
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
var tui *server.ServerTUI
var tuiDone chan struct{}
if useTUI {
tui = server.NewServerTUI(serverName, *port)
tuiDone = make(chan struct{})
go func() {
defer close(tuiDone)
if err := tui.Start(serverName, *port); err != nil {
log.Printf("TUI error: %v", err)
}
}()
time.Sleep(100 * time.Millisecond)
go func() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
updateTUI(tui, srv, source, serverName, *port)
case <-tuiDone:
return
}
}
}()
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
var tuiQuitChan <-chan struct{}
if tui != nil {
tuiQuitChan = tui.QuitChan()
}
serverDone := make(chan error, 1)
go func() {
serverDone <- srv.Start()
}()
serverStopped := false
select {
case sig := <-sigChan:
log.Printf("Received %v, shutting down...", sig)
case <-tuiQuitChan:
log.Printf("TUI quit requested, shutting down...")
case err := <-serverDone:
serverStopped = true
if err != nil {
log.Printf("Server error: %v", err)
}
}
srv.Stop()
if tui != nil {
tui.Stop()
<-tuiDone
}
if !serverStopped {
if err := <-serverDone; err != nil {
log.Printf("Server shutdown error: %v", err)
}
}
log.Printf("Server stopped")
}
func updateTUI(tui *server.ServerTUI, srv *sendspin.Server, source sendspin.AudioSource, serverName string, port int) {
clients := srv.Clients()
tuiClients := make([]server.ClientInfo, len(clients))
for i, c := range clients {
tuiClients[i] = server.ClientInfo{
Name: c.Name,
ID: c.ID,
Codec: c.Codec,
State: c.State,
}
}
title, artist, _ := source.Metadata()
audioTitle := title
if artist != "" && artist != "Unknown Artist" {
audioTitle = artist + " - " + title
}
tui.Update(server.ServerStatus{
Name: serverName,
Port: port,
Clients: tuiClients,
AudioTitle: audioTitle,
})
}