package main import ( "context" "encoding/json" "log" "net/http" "os" "google.golang.org/genai" ) var geminiClient *genai.Client func main() { loadDotEnv(".env") ctx := context.Background() client, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: os.Getenv("GEMINI_API_KEY"), Backend: genai.BackendGeminiAPI, }) if err != nil { log.Fatalf("failed to create genai client: %v", err) } geminiClient = client port := os.Getenv("PORT") if port == "" { port = "8080" } http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }) http.HandleFunc("/api/video-meta", handleVideoMeta) http.HandleFunc("/api/search", handleSearch) http.HandleFunc("/api/chat", handleChat) http.HandleFunc("/api/unlock", handleUnlock) http.Handle("/", http.FileServer(http.Dir("web"))) log.Printf("listening on :%s", port) if err := http.ListenAndServe(":"+port, nil); err != nil { log.Fatal(err) } } func handleVideoMeta(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") if id == "" { http.Error(w, "missing id", http.StatusBadRequest) return } meta, err := fetchVideoMeta(id, os.Getenv("YOUTUBE_API_KEY")) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(meta) } func handleSearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if query == "" { http.Error(w, "missing q", http.StatusBadRequest) return } results, err := searchVideos(query, os.Getenv("YOUTUBE_API_KEY")) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(results) } type chatRequest struct { VideoTitle string `json:"videoTitle"` VideoDescription string `json:"videoDescription"` History []chatTurn `json:"history"` Message string `json:"message"` UnlockCode string `json:"unlockCode"` Persona string `json:"persona"` } type chatStreamEvent struct { Type string `json:"type"` // "status" | "final" | "error" Text string `json:"text,omitempty"` Reply string `json:"reply,omitempty"` Model string `json:"model,omitempty"` Videos []searchResult `json:"videos,omitempty"` AutoPlayVideoID string `json:"autoPlayVideoId,omitempty"` } func handleChat(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req chatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/x-ndjson") flusher, _ := w.(http.Flusher) encoder := json.NewEncoder(w) writeEvent := func(ev chatStreamEvent) { encoder.Encode(ev) if flusher != nil { flusher.Flush() } } model := selectModel(req.UnlockCode, os.Getenv("UNLOCK_CODE")) outcome, err := chatReply(r.Context(), geminiClient, model, os.Getenv("YOUTUBE_API_KEY"), req.VideoTitle, req.VideoDescription, req.Persona, req.History, req.Message, func(status string) { writeEvent(chatStreamEvent{Type: "status", Text: status}) }) if err != nil { writeEvent(chatStreamEvent{Type: "error", Text: err.Error()}) return } writeEvent(chatStreamEvent{ Type: "final", Reply: outcome.Reply, Model: outcome.Model, Videos: outcome.Videos, AutoPlayVideoID: outcome.AutoPlayVideoID, }) } type unlockRequest struct { Code string `json:"code"` } type unlockResponse struct { Ok bool `json:"ok"` Model string `json:"model"` } func handleUnlock(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req unlockRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } model := selectModel(req.Code, os.Getenv("UNLOCK_CODE")) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(unlockResponse{Ok: model == modelGemini, Model: model}) }