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("────────────────────────────────────────────────") }