114 lines
3 KiB
Go
114 lines
3 KiB
Go
// moonrelay — self-hosted streaming manager backend.
|
|
//
|
|
// In Docker mode the React SPA is embedded at build time via go:embed.
|
|
// Set MOONRELAY_NO_EMBED=1 to skip the SPA (API-only / dev mode).
|
|
//
|
|
// Environment variables:
|
|
// MOONRELAY_PORT - HTTP listen port (default: 8080)
|
|
// MOONRELAY_HOST - HTTP listen address (default: 0.0.0.0)
|
|
// TS_AUTHKEY - Tailscale auth key (optional; prompts login if unset)
|
|
// TS_HOSTNAME - Tailscale device hostname (default: moonrelay)
|
|
// MOONLIGHT_PATH - Override path to moonlight binary
|
|
// MOONRELAY_NO_TS - Set "1" to disable Tailscale (LAN-only)
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"moonrelay/internal/api"
|
|
"moonrelay/internal/discovery"
|
|
"moonrelay/internal/moonlight"
|
|
tsnode "moonrelay/internal/tailscale"
|
|
)
|
|
|
|
// ui holds the React production build, copied here by the Dockerfile
|
|
// before `go build`. The path must match where the build output lands.
|
|
//
|
|
//go:embed ui
|
|
var uiFiles embed.FS
|
|
|
|
func main() {
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer cancel()
|
|
|
|
port := env("MOONRELAY_PORT", "8080")
|
|
host := env("MOONRELAY_HOST", "0.0.0.0")
|
|
noTS := os.Getenv("MOONRELAY_NO_TS") == "1"
|
|
|
|
// --- Moonlight launcher ---
|
|
launcher, err := moonlight.New(os.Getenv("MOONLIGHT_PATH"))
|
|
if err != nil {
|
|
log.Printf("Warning: %v", err)
|
|
launcher, _ = moonlight.New("")
|
|
}
|
|
|
|
// --- mDNS discovery ---
|
|
disc := discovery.New()
|
|
go disc.Run(ctx)
|
|
|
|
// --- Tailscale node (optional) ---
|
|
var ts *tsnode.Node
|
|
if !noTS {
|
|
log.Println("Starting embedded Tailscale node…")
|
|
ts, err = tsnode.New(ctx, tsnode.Config{
|
|
Hostname: env("TS_HOSTNAME", "moonrelay"),
|
|
AuthKey: os.Getenv("TS_AUTHKEY"),
|
|
})
|
|
if err != nil {
|
|
log.Printf("Warning: Tailscale failed to start (%v) — running in LAN-only mode", err)
|
|
ts = nil
|
|
} else {
|
|
if addr := ts.LocalAddr(); addr != "" {
|
|
log.Printf("Tailscale connected: %s", addr)
|
|
} else {
|
|
log.Println("Tailscale: waiting for auth (check logs for login URL)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Embedded SPA ---
|
|
// Strip the "ui/" prefix so the FS root contains index.html directly.
|
|
uiRoot, err := fs.Sub(uiFiles, "ui")
|
|
if err != nil {
|
|
log.Fatalf("Failed to access embedded UI: %v", err)
|
|
}
|
|
|
|
// --- HTTP server ---
|
|
srv := api.New(disc, launcher, ts, uiRoot)
|
|
addr := fmt.Sprintf("%s:%s", host, port)
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
log.Fatalf("Cannot listen on %s: %v", addr, err)
|
|
}
|
|
log.Printf("Moonlight Relay listening on http://%s", addr)
|
|
|
|
httpSrv := &http.Server{Handler: srv.Handler()}
|
|
go func() {
|
|
if err := httpSrv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
|
log.Printf("HTTP server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
log.Println("Shutting down…")
|
|
_ = httpSrv.Close()
|
|
if ts != nil {
|
|
_ = ts.Close()
|
|
}
|
|
}
|
|
|
|
func env(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|