package services import ( "encoding/json" "fmt" "net/http" "net/url" "strings" "time" ) type SearchResult struct { Title string `json:"title"` Link string `json:"link"` DisplayLink string `json:"displayLink"` Snippet string `json:"snippet"` ThumbnailURL string `json:"thumbnailUrl"` Source string `json:"source"` } type SearchService struct { APIKey string CX string Client *http.Client } func NewSearchService(apiKey, cx string) *SearchService { return &SearchService{ APIKey: apiKey, CX: cx, Client: &http.Client{Timeout: 20 * time.Second}, } } func (s *SearchService) SearchMedia(query string) ([]SearchResult, error) { if s.APIKey == "" || s.CX == "" { return nil, fmt.Errorf("google cse credentials are not configured") } domains := []string{"youtube.com", "tiktok.com", "envato.com", "artgrid.io"} siteQuery := strings.Join(domains, " OR site:") fullQuery := fmt.Sprintf("%s (site:%s)", query, siteQuery) values := url.Values{} values.Set("key", s.APIKey) values.Set("cx", s.CX) values.Set("q", fullQuery) values.Set("searchType", "image") values.Set("num", "10") values.Set("safe", "off") results := make([]SearchResult, 0, 30) seen := map[string]bool{} for _, start := range []string{"1", "11", "21"} { values.Set("start", start) endpoint := "https://www.googleapis.com/customsearch/v1?" + values.Encode() resp, err := s.Client.Get(endpoint) if err != nil { return nil, err } if resp.StatusCode >= 300 { resp.Body.Close() return nil, fmt.Errorf("google cse returned status %d", resp.StatusCode) } var payload struct { Items []struct { Title string `json:"title"` Link string `json:"link"` DisplayLink string `json:"displayLink"` Snippet string `json:"snippet"` Image struct { ThumbnailLink string `json:"thumbnailLink"` } `json:"image"` } `json:"items"` } if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { resp.Body.Close() return nil, err } resp.Body.Close() for _, item := range payload.Items { if item.Link == "" || seen[item.Link] { continue } seen[item.Link] = true results = append(results, SearchResult{ Title: item.Title, Link: item.Link, DisplayLink: item.DisplayLink, Snippet: item.Snippet, ThumbnailURL: item.Image.ThumbnailLink, Source: inferSource(item.DisplayLink), }) } } return results, nil } func inferSource(displayLink string) string { switch { case strings.Contains(displayLink, "youtube"): return "YouTube" case strings.Contains(displayLink, "tiktok"): return "TikTok" case strings.Contains(displayLink, "envato"): return "Envato" case strings.Contains(displayLink, "artgrid"): return "Artgrid" default: return displayLink } }