Files
Strix/cmd/strix/main.go
T

220 lines
6.3 KiB
Go

package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/eduard256/Strix/internal/api"
"github.com/eduard256/Strix/internal/config"
"github.com/eduard256/Strix/internal/utils/logger"
"github.com/eduard256/Strix/webui"
"github.com/go-chi/chi/v5"
)
const (
// Version is the application version
Version = "1.0.0"
// Banner is the application banner
Banner = `
███████╗████████╗██████╗ ██╗██╗ ██╗
██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝
███████╗ ██║ ██████╔╝██║ ╚███╔╝
╚════██║ ██║ ██╔══██╗██║ ██╔██╗
███████║ ██║ ██║ ██║██║██╔╝ ██╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝
Smart IP Camera Stream Discovery System
Version: %s
`
)
func main() {
// Print banner
fmt.Printf(Banner, Version)
fmt.Println()
// Load configuration
cfg := config.Load()
// Setup logger
slogger := cfg.SetupLogger()
slog.SetDefault(slogger)
// Create adapter for our interface
log := logger.NewAdapter(slogger)
log.Info("starting Strix",
slog.String("version", Version),
slog.String("go_version", os.Getenv("GO_VERSION")),
slog.String("listen", cfg.Server.Listen),
)
// Check if ffprobe is available
if err := checkFFProbe(); err != nil {
log.Warn("ffprobe not found, stream validation will be limited", slog.String("error", err.Error()))
}
// Create API server
apiServer, err := api.NewServer(cfg, log)
if err != nil {
log.Error("failed to create API server", err)
os.Exit(1)
}
// Create Web UI server
webuiServer := webui.NewServer(log)
// Create unified router combining API and WebUI
unifiedRouter := chi.NewRouter()
// Mount API routes at /api/v1/*
unifiedRouter.Mount("/api/v1", apiServer.GetRouter())
// Mount WebUI routes at /* (serves everything else including root)
unifiedRouter.Mount("/", webuiServer.GetRouter())
// Create unified HTTP server
httpServer := &http.Server{
Addr: cfg.Server.Listen,
Handler: unifiedRouter,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: 120 * time.Second,
}
// Start server in goroutine
go func() {
log.Info("server starting",
slog.String("address", httpServer.Addr),
slog.String("api_version", "v1"),
)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error("server failed", err)
os.Exit(1)
}
}()
// Print endpoints
printEndpoints(cfg.Server.Listen)
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Info("shutting down server...")
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown server
if err := httpServer.Shutdown(ctx); err != nil {
log.Error("server shutdown failed", err)
os.Exit(1)
}
log.Info("server stopped gracefully")
}
// checkFFProbe checks if ffprobe is available
func checkFFProbe() error {
// Try to execute ffprobe -version
cmd := os.Getenv("PATH")
if cmd == "" {
return fmt.Errorf("PATH environment variable not set")
}
// For now, just check if ffprobe exists in common locations
locations := []string{
"/usr/bin/ffprobe",
"/usr/local/bin/ffprobe",
"/opt/homebrew/bin/ffprobe",
}
for _, loc := range locations {
if _, err := os.Stat(loc); err == nil {
return nil
}
}
return fmt.Errorf("ffprobe not found in common locations")
}
// printEndpoints prints available endpoints
func printEndpoints(listen string) {
// Parse listen address to get host and port
host := "localhost"
port := "4567"
// Extract port from listen address
if len(listen) > 0 {
if listen[0] == ':' {
port = listen[1:]
} else {
// Parse host:port format
for i := len(listen) - 1; i >= 0; i-- {
if listen[i] == ':' {
port = listen[i+1:]
if i > 0 {
host = listen[:i]
if host == "0.0.0.0" || host == "" {
host = "localhost"
}
}
break
}
}
}
}
baseURL := fmt.Sprintf("http://%s:%s", host, port)
fmt.Println("\n🌐 Web Interface:")
fmt.Println("────────────────────────────────────────────────")
fmt.Printf(" Open in browser: %s\n", baseURL)
fmt.Println("────────────────────────────────────────────────")
fmt.Println("\n🚀 API Endpoints:")
fmt.Println("────────────────────────────────────────────────")
fmt.Printf(" Health Check: GET %s/api/v1/health\n", baseURL)
fmt.Printf(" Camera Search: POST %s/api/v1/cameras/search\n", baseURL)
fmt.Printf(" Stream Discovery: POST %s/api/v1/streams/discover (SSE)\n", baseURL)
fmt.Println("────────────────────────────────────────────────")
fmt.Println("\n📝 Example Requests:")
fmt.Println("\n1. Search for cameras:")
fmt.Printf(` curl -X POST %s/api/v1/cameras/search \
-H "Content-Type: application/json" \
-d '{"query": "zosi zg23213m", "limit": 10}'
`, baseURL)
fmt.Println("\n2. Discover streams (SSE):")
fmt.Printf(` curl -X POST %s/api/v1/streams/discover \
-H "Content-Type: application/json" \
-d '{
"target": "192.168.1.100",
"model": "zosi zg23213m",
"username": "admin",
"password": "password",
"timeout": 240,
"max_streams": 10
}'
`, baseURL)
fmt.Println("\n3. Check health:")
fmt.Printf(" curl %s/api/v1/health\n", baseURL)
fmt.Println("\n────────────────────────────────────────────────")
fmt.Println("📚 Documentation: https://github.com/eduard256/Strix")
fmt.Println("────────────────────────────────────────────────")
}