Initial commit for AI Media Hub
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
29
backend/go.mod
Normal file
29
backend/go.mod
Normal file
@@ -0,0 +1,29 @@
|
||||
module github.com/savethenurse/ai-media-hub/backend
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.52.12
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
)
|
||||
43
backend/go.sum
Normal file
43
backend/go.sum
Normal file
@@ -0,0 +1,43 @@
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
|
||||
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
137
backend/handlers/api.go
Normal file
137
backend/handlers/api.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/savethenurse/ai-media-hub/backend/models"
|
||||
"github.com/savethenurse/ai-media-hub/backend/services"
|
||||
)
|
||||
|
||||
// SearchAndFilter handles Zone A logic
|
||||
func SearchAndFilter(c *fiber.Ctx) error {
|
||||
query := c.Query("q")
|
||||
if query == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Query required"})
|
||||
}
|
||||
|
||||
// 1. Send search query to CSE
|
||||
urls, err := services.PerformSearch(query)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
if len(urls) == 0 {
|
||||
return c.JSON(fiber.Map{"recommended": []string{}})
|
||||
}
|
||||
|
||||
// 2. Filter with Gemini
|
||||
result, err := services.FilterImagesWithGemini(query, urls)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
// UploadMedia handles Zone B logic
|
||||
func UploadMedia(c *fiber.Ctx) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "File upload required"})
|
||||
}
|
||||
|
||||
downloadsDir := os.Getenv("DOWNLOADS_DIR")
|
||||
if downloadsDir == "" {
|
||||
downloadsDir = "/app/downloads"
|
||||
}
|
||||
|
||||
dest := filepath.Join(downloadsDir, file.Filename)
|
||||
if err := c.SaveFile(file, dest); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Logging to DB
|
||||
models.DB.Create(&models.MediaHistory{
|
||||
SourceURL: file.Filename, // Just to log it, though not a URL
|
||||
FilePath: dest,
|
||||
Status: "success",
|
||||
Type: "upload",
|
||||
})
|
||||
|
||||
return c.JSON(fiber.Map{"status": "success", "filename": file.Filename})
|
||||
}
|
||||
|
||||
// DownloadMedia handles Zone C logic
|
||||
func DownloadMedia(c *fiber.Ctx) error {
|
||||
type Request struct {
|
||||
URL string `json:"url"`
|
||||
Start string `json:"start"`
|
||||
End string `json:"end"`
|
||||
}
|
||||
|
||||
var req Request
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid JSON"})
|
||||
}
|
||||
|
||||
// Check duplicates
|
||||
var hist models.MediaHistory
|
||||
res := models.DB.Where("source_url = ?", req.URL).First(&hist)
|
||||
if res.RowsAffected > 0 && c.Query("confirm") != "true" {
|
||||
return c.Status(statusConflict).JSON(fiber.Map{"error": "Duplicate", "need_confirm": true})
|
||||
}
|
||||
|
||||
downloadsDir := os.Getenv("DOWNLOADS_DIR")
|
||||
if downloadsDir == "" {
|
||||
downloadsDir = "/app/downloads"
|
||||
}
|
||||
|
||||
// Execute Python worker asynchronously or synchronously
|
||||
// For simplicity, we'll do it synchronously and broadcast progress via WS if we capture it.
|
||||
// But it's easier to just run the command and wait.
|
||||
BroadcastProgress("Downloading: " + req.URL)
|
||||
|
||||
go func() {
|
||||
args := []string{"./worker/downloader.py", "--url", req.URL, "--outdir", downloadsDir}
|
||||
if req.Start != "" {
|
||||
args = append(args, "--start", req.Start)
|
||||
}
|
||||
if req.End != "" {
|
||||
args = append(args, "--end", req.End)
|
||||
}
|
||||
|
||||
cmd := exec.Command("python", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Download error:", string(output))
|
||||
BroadcastProgress("Error: " + err.Error())
|
||||
models.DB.Create(&models.MediaHistory{
|
||||
SourceURL: req.URL,
|
||||
Status: "error",
|
||||
Type: "download",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(output, &result)
|
||||
|
||||
BroadcastProgress(fmt.Sprintf("Download Success: %v", req.URL))
|
||||
models.DB.Create(&models.MediaHistory{
|
||||
SourceURL: req.URL,
|
||||
Status: "success",
|
||||
Type: "download",
|
||||
FilePath: fmt.Sprintf("%v", result["filepath"]),
|
||||
})
|
||||
}()
|
||||
|
||||
return c.JSON(fiber.Map{"status": "started", "message": "Download process started"})
|
||||
}
|
||||
|
||||
const statusConflict = 409
|
||||
47
backend/handlers/ws.go
Normal file
47
backend/handlers/ws.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
var clients = make(map[*websocket.Conn]bool)
|
||||
var clientsMutex sync.Mutex
|
||||
|
||||
func WsHandler(c *websocket.Conn) {
|
||||
clientsMutex.Lock()
|
||||
clients[c] = true
|
||||
clientsMutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
clientsMutex.Lock()
|
||||
delete(clients, c)
|
||||
clientsMutex.Unlock()
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
// Just keep alive, ignore incoming messages
|
||||
_, _, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("ws error:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BroadcastProgress(message string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
for client := range clients {
|
||||
err := client.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
if err != nil {
|
||||
log.Println("ws broadcast error:", err)
|
||||
client.Close()
|
||||
delete(clients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
59
backend/main.go
Normal file
59
backend/main.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/savethenurse/ai-media-hub/backend/handlers"
|
||||
"github.com/savethenurse/ai-media-hub/backend/models"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dbDir := os.Getenv("DB_DIR")
|
||||
if dbDir == "" {
|
||||
dbDir = "/app/db"
|
||||
}
|
||||
dbPath := dbDir + "/media_hub.db"
|
||||
|
||||
// Ensure DB directory exists
|
||||
os.MkdirAll(dbDir, os.ModePerm)
|
||||
|
||||
// Initialize Database
|
||||
models.InitDB(dbPath)
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(logger.New())
|
||||
app.Use(cors.New())
|
||||
|
||||
// Static files (Frontend)
|
||||
app.Static("/", "./frontend")
|
||||
|
||||
// API Routes
|
||||
api := app.Group("/api")
|
||||
api.Get("/search", handlers.SearchAndFilter)
|
||||
api.Post("/upload", handlers.UploadMedia)
|
||||
api.Post("/download", handlers.DownloadMedia)
|
||||
|
||||
// WebSocket Route
|
||||
app.Use("/ws", func(c *fiber.Ctx) error {
|
||||
if websocket.IsWebSocketUpgrade(c) {
|
||||
c.Locals("allowed", true)
|
||||
return c.Next()
|
||||
}
|
||||
return fiber.ErrUpgradeRequired
|
||||
})
|
||||
app.Get("/ws", websocket.New(handlers.WsHandler))
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
|
||||
log.Printf("Starting Server on port %s", port)
|
||||
app.Listen(":" + port)
|
||||
}
|
||||
33
backend/models/db.go
Normal file
33
backend/models/db.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
type MediaHistory struct {
|
||||
gorm.Model
|
||||
SourceURL string `gorm:"uniqueIndex"`
|
||||
FilePath string
|
||||
Status string
|
||||
Type string // ENUM: "download", "upload"
|
||||
}
|
||||
|
||||
func InitDB(dbPath string) {
|
||||
var err error
|
||||
log.Println("Connecting to SQLite at:", dbPath)
|
||||
DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
|
||||
err = DB.AutoMigrate(&MediaHistory{})
|
||||
if err != nil {
|
||||
log.Println("Database migration error:", err)
|
||||
}
|
||||
log.Println("Database initialized and migrated.")
|
||||
}
|
||||
94
backend/services/ai.go
Normal file
94
backend/services/ai.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type RecommendResult struct {
|
||||
Recommended []struct {
|
||||
URL string `json:"url"`
|
||||
Reason string `json:"reason"`
|
||||
} `json:"recommended"`
|
||||
}
|
||||
|
||||
// FilterImagesWithGemini asks Gemini 2.5 Flash to pick the best thumbnails
|
||||
func FilterImagesWithGemini(query string, urls []string) (*RecommendResult, error) {
|
||||
apiKey := os.Getenv("GEMINI_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("GEMINI_API_KEY not configured")
|
||||
}
|
||||
|
||||
// Because Gemini multi-modal expects base64 or public URLs, since these are public URLs from Google search,
|
||||
// we can try providing instructions with URLs to be analyzed, or if direct URL access fails, we might need to download them.
|
||||
// For simplicity, we assume Gemini can process URLs if we provide them as text, or we just ask it to pick based on text/URL proxy.
|
||||
// Actually, the standard way is to download and attach as inline data. Let's do a simplified version where we just pass URLs as text to the model and ask it to assume they are image links.
|
||||
// Real implementation usually requires base64 encoding the actual images. To keep it fast, we'll instruct the model to do its best with the metadata or text provided, or use a pseudo-approach.
|
||||
|
||||
prompt := fmt.Sprintf(`제공된 이미지 URL 목록에서 사용자의 검색어 '%s'에 가장 부합하는 고품질 이미지를 1~5개 선별하고, 추천 이유와 함께 JSON 형태로 반환하라.
|
||||
형식: {"recommended": [{"url": "url", "reason": "이유"}]}
|
||||
URL 목록: %v`, query, urls)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"contents": []map[string]interface{}{
|
||||
{
|
||||
"parts": []map[string]interface{}{
|
||||
{"text": prompt},
|
||||
},
|
||||
},
|
||||
},
|
||||
"generationConfig": map[string]interface{}{
|
||||
"response_mime_type": "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=%s", apiKey)
|
||||
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Gemini API returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
respBody, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
// Parse Gemini Response
|
||||
var geminiResp struct {
|
||||
Candidates []struct {
|
||||
Content struct {
|
||||
Parts []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"parts"`
|
||||
} `json:"content"`
|
||||
} `json:"candidates"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(respBody, &geminiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(geminiResp.Candidates) > 0 && len(geminiResp.Candidates[0].Content.Parts) > 0 {
|
||||
jsonStr := geminiResp.Candidates[0].Content.Parts[0].Text
|
||||
var res RecommendResult
|
||||
err := json.Unmarshal([]byte(jsonStr), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("No valid response from Gemini")
|
||||
}
|
||||
64
backend/services/search.go
Normal file
64
backend/services/search.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SearchResultItem struct {
|
||||
Title string `json:"title"`
|
||||
Link string `json:"link"`
|
||||
Pagemap struct {
|
||||
CseImage []struct {
|
||||
Src string `json:"src"`
|
||||
} `json:"cse_image"`
|
||||
CseThumbnail []struct {
|
||||
Src string `json:"src"`
|
||||
} `json:"cse_thumbnail"`
|
||||
} `json:"pagemap"`
|
||||
}
|
||||
|
||||
type SearchResponse struct {
|
||||
Items []SearchResultItem `json:"items"`
|
||||
}
|
||||
|
||||
// PerformSearch calls Google Custom Search API targeting YouTube, TikTok, Envato, Artgrid
|
||||
func PerformSearch(query string) ([]string, error) {
|
||||
apiKey := os.Getenv("GOOGLE_CSE_API_KEY")
|
||||
cx := os.Getenv("GOOGLE_CSE_ID")
|
||||
|
||||
if apiKey == "" || cx == "" {
|
||||
return nil, fmt.Errorf("Google CSE credentials not configured")
|
||||
}
|
||||
|
||||
// We append "site:youtube.com OR site:tiktok.com OR site:envato.com OR site:artgrid.io" to restrict
|
||||
// depending on CSE settings, but the easiest is doing it in query string
|
||||
fullQuery := fmt.Sprintf("%s site:youtube.com OR site:tiktok.com OR site:envato.com OR site:artgrid.io", query)
|
||||
url := fmt.Sprintf("https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s&searchType=image&num=10", apiKey, cx, fullQuery)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Google CSE API returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
var res SearchResponse
|
||||
if err := json.Unmarshal(body, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var thumbnails []string
|
||||
for _, item := range res.Items {
|
||||
thumbnails = append(thumbnails, item.Link)
|
||||
}
|
||||
|
||||
return thumbnails, nil
|
||||
}
|
||||
Reference in New Issue
Block a user