package main import ( "log" "net/http" "os" "path/filepath" "strconv" "strings" "ai-media-hub/backend/handlers" "ai-media-hub/backend/models" "ai-media-hub/backend/services" "github.com/gin-gonic/gin" ) func main() { root := envOrDefault("APP_ROOT", "/app") dbPath := envOrDefault("SQLITE_PATH", filepath.Join(root, "db", "media.db")) downloadsDir := envOrDefault("DOWNLOADS_DIR", filepath.Join(root, "downloads")) giphyDownloadDir := envOrDefault("GIPHY_DOWNLOAD_DIR", filepath.Join(downloadsDir, "giphy")) frontendDir := envOrDefault("FRONTEND_DIR", filepath.Join(root, "frontend")) workerScript := envOrDefault("WORKER_SCRIPT", filepath.Join(root, "worker", "downloader.py")) geminiAPIKey := os.Getenv("GEMINI_API_KEY") geminiModel := envOrDefault("GEMINI_MODEL", "gemini-2.5-flash") giphyEnabled := envBoolOrDefault("GIPHY_ENABLED", true) giphyAPIKey := os.Getenv("GIPHY_API_KEY") giphyMaxResults := envIntOrDefault("GIPHY_MAX_RESULTS", 100) giphyRating := envOrDefault("GIPHY_RATING", "g") giphyLang := envOrDefault("GIPHY_LANG", "en") db, err := models.InitDB(dbPath) if err != nil { log.Fatal(err) } defer db.Close() if err := handlers.EnsurePaths(downloadsDir, workerScript); err != nil { log.Fatal(err) } if geminiAPIKey == "" { log.Printf("warning: GEMINI_API_KEY is not configured; query expansion will use fallback behavior") } if giphyEnabled && strings.TrimSpace(giphyAPIKey) == "" { log.Fatal("GIPHY_ENABLED is true but GIPHY_API_KEY is not configured") } if err := os.MkdirAll(giphyDownloadDir, 0o755); err != nil { log.Fatal(err) } geminiService := services.NewGeminiService(geminiAPIKey, geminiModel) giphyService := services.NewGiphyService(services.GiphyConfig{ Enabled: giphyEnabled, APIKey: giphyAPIKey, MaxResults: giphyMaxResults, Rating: giphyRating, Lang: giphyLang, DownloadDir: giphyDownloadDir, }, geminiService) app := &handlers.App{ DB: db, DownloadsDir: downloadsDir, PreviewCacheDir: filepath.Join(downloadsDir, ".preview-cache"), WorkerScript: workerScript, SearchService: services.NewSearchService( os.Getenv("SEARXNG_BASE_URL"), os.Getenv("SEARXNG_GOOGLE_VIDEO_ENGINE"), os.Getenv("SEARXNG_WEB_ENGINE"), ), GeminiService: geminiService, GiphyService: giphyService, Hub: handlers.NewHub(), } app.SearchService.Debug = func(message string, data any) { app.Hub.Broadcast("debug", gin.H{"message": message, "data": data}) } app.GeminiService.Debug = func(message string, data any) { app.Hub.Broadcast("debug", gin.H{"message": message, "data": data}) } app.GiphyService.Debug = func(message string, data any) { app.Hub.Broadcast("debug", gin.H{"message": message, "data": data}) } router := gin.Default() handlers.RegisterRoutes(router, app) router.StaticFile("/", filepath.Join(frontendDir, "index.html")) router.StaticFile("/app.js", filepath.Join(frontendDir, "app.js")) router.StaticFile("/style.css", filepath.Join(frontendDir, "style.css")) router.NoRoute(func(c *gin.Context) { c.File(filepath.Join(frontendDir, "index.html")) }) router.NoMethod(func(c *gin.Context) { c.JSON(http.StatusMethodNotAllowed, gin.H{"error": "method not allowed"}) }) addr := envOrDefault("APP_ADDR", ":8080") log.Printf("server listening on %s", addr) if err := router.Run(addr); err != nil { log.Fatal(err) } } func envOrDefault(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func envBoolOrDefault(key string, fallback bool) bool { value := strings.TrimSpace(strings.ToLower(os.Getenv(key))) switch value { case "1", "true", "yes", "on": return true case "0", "false", "no", "off": return false case "": return fallback default: return fallback } } func envIntOrDefault(key string, fallback int) int { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed }