Add GIPHY image search feature
This commit is contained in:
@@ -33,6 +33,7 @@ type App struct {
|
||||
WorkerScript string
|
||||
SearchService *services.SearchService
|
||||
GeminiService *services.GeminiService
|
||||
GiphyService *services.GiphyService
|
||||
Hub *Hub
|
||||
}
|
||||
|
||||
@@ -149,6 +150,8 @@ func RegisterRoutes(router *gin.Engine, app *App) {
|
||||
router.POST("/api/download", app.startDownload)
|
||||
router.POST("/api/translate/summary", app.translateSummary)
|
||||
router.POST("/api/search", app.searchMedia)
|
||||
router.POST("/api/giphy/search", app.searchGiphy)
|
||||
router.POST("/api/giphy/download", app.downloadGiphy)
|
||||
}
|
||||
|
||||
func (a *App) debug(message string, data any) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"ai-media-hub/backend/models"
|
||||
"ai-media-hub/backend/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (a *App) searchGiphy(c *gin.Context) {
|
||||
if a.GiphyService == nil || !a.GiphyService.Config.Enabled {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "giphy search is disabled"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Query string `json:"query"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.Query = strings.TrimSpace(req.Query)
|
||||
if req.Query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query is required"})
|
||||
return
|
||||
}
|
||||
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "giphy-search", "status": "expanding query for GIPHY", "progress": 20})
|
||||
resp, err := a.GiphyService.SearchImages(req.Query, req.MaxResults)
|
||||
if err != nil {
|
||||
a.debug("giphy:search_error", gin.H{"query": req.Query, "error": err.Error()})
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "giphy-search", "status": "giphy search failed", "progress": 100, "message": err.Error()})
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
a.debug("giphy:search_complete", gin.H{
|
||||
"query": req.Query,
|
||||
"expandedQueries": resp.ExpandedQueries,
|
||||
"total": resp.Total,
|
||||
"warning": resp.Warning,
|
||||
})
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "giphy-search", "status": "giphy search complete", "progress": 100, "count": resp.Total})
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (a *App) downloadGiphy(c *gin.Context) {
|
||||
if a.GiphyService == nil || !a.GiphyService.Config.Enabled {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"ok": false, "error": "GIPHY_DISABLED", "message": "GIPHY download is disabled"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ProviderID string `json:"providerId"`
|
||||
Title string `json:"title"`
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
OriginalQuery string `json:"originalQuery"`
|
||||
SelectedExpansionQuery string `json:"selectedExpansionQuery"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "INVALID_REQUEST", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := a.GiphyService.DownloadMedia(services.GiphyDownloadRequest{
|
||||
ProviderID: req.ProviderID,
|
||||
Title: req.Title,
|
||||
DownloadURL: req.DownloadURL,
|
||||
OriginalQuery: req.OriginalQuery,
|
||||
SelectedExpansionQuery: req.SelectedExpansionQuery,
|
||||
})
|
||||
if err != nil {
|
||||
a.debug("giphy:download_error", gin.H{
|
||||
"providerId": req.ProviderID,
|
||||
"title": req.Title,
|
||||
"error": err.Error(),
|
||||
})
|
||||
status := http.StatusBadGateway
|
||||
if resp.Error == "INVALID_REQUEST" || resp.Error == "INVALID_DOWNLOAD_URL" {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, resp)
|
||||
return
|
||||
}
|
||||
|
||||
if a.DB != nil {
|
||||
if recordID, dbErr := models.InsertDownload(a.DB, req.DownloadURL, "GIPHY", resp.SavedPath, "completed"); dbErr == nil {
|
||||
_ = models.MarkDownloadCompleted(a.DB, recordID, "completed")
|
||||
}
|
||||
}
|
||||
a.Hub.Broadcast("progress", gin.H{"type": "giphy-download", "status": "giphy download complete", "progress": 100, "fileName": resp.FileName})
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ai-media-hub/backend/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestSearchGiphyEndpointReturnsExpandedQueriesAndItems(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/generate", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"candidates":[{"content":{"parts":[{"text":"{\"queries\":[\"happy dog\",\"happy dog gif\",\"dog reaction\",\"dog meme gif\",\"animated dog sticker\"]}"}]}}]}`))
|
||||
})
|
||||
mux.HandleFunc("/v1/gifs/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":"dog-1","title":"Happy Dog","slug":"happy-dog","rating":"g","url":"https://giphy.com/gifs/dog-1","images":{"original":{"url":"https://media.giphy.com/media/dog-1/giphy.gif","width":"480","height":"270"},"fixed_width":{"url":"https://media.giphy.com/media/dog-1/200w.gif","width":"200","height":"113"},"fixed_width_still":{"url":"https://media.giphy.com/media/dog-1/200w_s.gif"}}}]}`))
|
||||
})
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
gemini := services.NewGeminiService("dummy-key", "gemini-2.5-flash")
|
||||
gemini.Client = &http.Client{Timeout: 2 * time.Second}
|
||||
gemini.GenerateEndpoint = server.URL + "/generate"
|
||||
|
||||
giphy := services.NewGiphyService(services.GiphyConfig{
|
||||
Enabled: true,
|
||||
APIKey: "test-key",
|
||||
MaxResults: 100,
|
||||
BaseURL: server.URL,
|
||||
}, gemini)
|
||||
giphy.Client = &http.Client{Timeout: 2 * time.Second}
|
||||
|
||||
app := &App{GiphyService: giphy, Hub: NewHub()}
|
||||
router := gin.New()
|
||||
RegisterRoutes(router, app)
|
||||
|
||||
body := bytes.NewBufferString(`{"query":"행복한 강아지","maxResults":100}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/giphy/search", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
ExpandedQueries []string `json:"expandedQueries"`
|
||||
Items []services.GiphyResult `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if len(payload.ExpandedQueries) != 5 || len(payload.Items) == 0 {
|
||||
t.Fatalf("unexpected payload: %#v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadGiphyEndpointRejectsNonGiphyHost(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
app := &App{
|
||||
GiphyService: services.NewGiphyService(services.GiphyConfig{
|
||||
Enabled: true,
|
||||
APIKey: "test-key",
|
||||
DownloadDir: t.TempDir(),
|
||||
}, nil),
|
||||
Hub: NewHub(),
|
||||
}
|
||||
router := gin.New()
|
||||
RegisterRoutes(router, app)
|
||||
|
||||
body := bytes.NewBufferString(`{"providerId":"x","title":"bad","downloadUrl":"https://example.com/file.gif"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/giphy/download", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "INVALID_DOWNLOAD_URL") {
|
||||
t.Fatalf("expected invalid host error, got %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user