package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "strings" "github.com/gofiber/fiber/v2" "github.com/gofiber/websocket/v2" ) // Config - 환경 변수 등 const ( Port = ":8000" ) type SearchRequest struct { Query string `json:"query"` } type SearchResult struct { Title string `json:"title"` Link string `json:"link"` Thumbnail string `json:"thumbnail"` Snippet string `json:"snippet"` Source string `json:"source"` AiRecommended bool `json:"ai_recommended"` AiReasoning string `json:"ai_reasoning"` } type DownloadRequest struct { URL string `json:"url"` Start string `json:"start"` End string `json:"end"` } func main() { app := fiber.New() // Static Files (Frontend) app.Static("/", "./index.html") // Middleware for WebSocket upgrade app.Use("/ws", func(c *fiber.Ctx) error { if websocket.IsWebSocketUpgrade(c) { return c.Next() } return fiber.ErrUpgradeRequired }) // WebSocket handler (for progress updates) app.Get("/ws", websocket.New(func(c *websocket.Conn) { for { // keep connection alive or handle client messages if _, _, err := c.ReadMessage(); err != nil { break } } })) app.Post("/api/search", handleSearch) app.Post("/api/download", handleDownload) log.Printf("Server starting on %s", Port) if err := app.Listen(Port); err != nil { log.Fatal(err) } } // 1차: Google CSE를 통한 메타데이터/썸네일 수집 // 2차: Gemini 2.5 Flash를 통한 비전 분석 func handleSearch(c *fiber.Ctx) error { var req SearchRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } apiKey := os.Getenv("GOOGLE_API_KEY") cx := os.Getenv("GOOGLE_CSE_ID") geminiKey := os.Getenv("GEMINI_API_KEY") // 1. Google Custom Search API 호출 (Mock logic if keys are missing) var rawResults []SearchResult if apiKey != "" && cx != "" { rawResults = fetchGoogleCSE(req.Query, apiKey, cx) } else { // Mock Data for demonstration rawResults = []SearchResult{ {Title: "Sample Video 1", Link: "https://youtube.com", Thumbnail: "https://via.placeholder.com/300x200", Source: "YouTube", Snippet: "Cyberpunk city night"}, {Title: "Sample TikTok", Link: "https://tiktok.com", Thumbnail: "https://via.placeholder.com/300x200?text=TikTok", Source: "TikTok", Snippet: "Neon lights"}, } } // 2. Gemini 2.5 Flash API로 멀티모달 프롬프팅 if geminiKey != "" && len(rawResults) > 0 { rawResults = analyzeWithGemini(req.Query, rawResults, geminiKey) } else { // Mock logic: mark first as recommended if len(rawResults) > 0 { rawResults[0].AiRecommended = true rawResults[0].AiReasoning = "입력한 [ " + req.Query + " ] 키워드와 분위기가 가장 흡사해 보입니다." } } return c.JSON(fiber.Map{"results": rawResults}) } // Google Custom Search API 연동 func fetchGoogleCSE(query, apiKey, cx string) []SearchResult { // 실제 CSE API 요청 url := fmt.Sprintf("https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s&searchType=image&num=10", apiKey, cx, query) resp, err := http.Get(url) if err != nil { log.Println("CSE Fetch error:", err) return nil } defer resp.Body.Close() var cseResp struct { Items []struct { Title string `json:"title"` Link string `json:"link"` // image url Snippet string `json:"snippet"` Image struct { ContextLink string `json:"contextLink"` // original page } `json:"image"` } `json:"items"` } if err := json.NewDecoder(resp.Body).Decode(&cseResp); err != nil { log.Println("CSE Decode error:", err) return nil } var results []SearchResult for _, item := range cseResp.Items { source := "Unknown" if strings.Contains(item.Image.ContextLink, "youtube.com") { source = "YouTube" } else if strings.Contains(item.Image.ContextLink, "tiktok.com") { source = "TikTok" } else if strings.Contains(item.Image.ContextLink, "envato.com") { source = "Envato Elements" } else if strings.Contains(item.Image.ContextLink, "artgrid.io") { source = "Artgrid" } results = append(results, SearchResult{ Title: item.Title, Link: item.Image.ContextLink, Thumbnail: item.Link, Snippet: item.Snippet, Source: source, }) } return results } // Gemini 2.5 Flash를 이용한 이미지 기반 추천 로직 (멀티모달) func analyzeWithGemini(query string, items []SearchResult, apiKey string) []SearchResult { // 실제 환경에서는 각 썸네일 이미지를 Base64 인코딩하거나 파일 URI를 첨부하여 전송해야 하나 // 구조적인 코드 예시로서 REST API 호출 포맷을 구현합니다. apiURL := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=%s", apiKey) // 단순화를 위해 텍스트 프롬프트 형태로 썸네일 URL과 함께 전달 prompt := fmt.Sprintf("사용자의 검색어: '%s'. 다음은 수집된 미디어 항목들의 썸네일 URL과 정보입니다. 이 중 검색어와 시각적으로 가장 잘 부합하는 것을 1~2개 고르고, 그 이유를 설명해줘. 입력 정보: ", query) for i, it := range items { prompt += fmt.Sprintf("[%d] 제목: %s, URL: %s ", i, it.Title, it.Thumbnail) } bodyMap := map[string]interface{}{ "contents": []map[string]interface{}{ { "parts": []map[string]interface{}{ {"text": prompt}, }, }, }, } b, _ := json.Marshal(bodyMap) resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(b)) if err != nil { log.Println("Gemini API Error:", err) return items } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) respStr := string(respBody) // 응답을 분석하여 AI Recommended 태그 부착 (단순 문자열 매칭 방식 예시) for i := range items { if strings.Contains(respStr, items[i].Title) || strings.Contains(respStr, fmt.Sprintf("[%d]", i)) { items[i].AiRecommended = true items[i].AiReasoning = "Gemini 2.5 Flash 분석: 사용자의 요구사항과 시각적으로 매칭도가 높다고 판단됨." } } return items } // Download request - yt-dlp 호출 (Python backend 역할) func handleDownload(c *fiber.Ctx) error { var req DownloadRequest if err := c.BodyParser(&req); err != nil { return c.Status(400).SendString("Invalid Input") } // 컨테이너/호스트 내 NAS 마운트 경로 (예: /data/nas) outDir := "/data/nas" os.MkdirAll(outDir, os.ModePerm) // yt-dlp 명령어 구성 args := []string{"-o", outDir + "/%(title)s.%(ext)s", req.URL} // 구간 크롭 옵션 if req.Start != "" && req.End != "" { args = append(args, "--download-sections", fmt.Sprintf("*%s-%s", req.Start, req.End)) } // 비동기 실행 go func(args []string) { cmd := exec.Command("yt-dlp", args...) // stdout 진행률 파싱하여 WebSocket으로 전송 가능 (여기서는 실행만 구현) err := cmd.Run() if err != nil { log.Println("Download failed:", err) } else { log.Println("Download complete") } }(args) return c.JSON(fiber.Map{"status": "started"}) }