package main import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "net/http" "net/url" "sync" "time" ) // QwantAPIResponse represents the JSON response structure from Qwant API type QwantAPIResponse struct { Data struct { Result struct { Items []struct { Media string `json:"media"` //Thumbnail string `json:"thumbnail"` Title string `json:"title"` Url string `json:"url"` Width int `json:"width"` Height int `json:"height"` } `json:"items"` } `json:"result"` } `json:"data"` } func ConvertToQwantLocale(langCode string) string { langMap := map[string]string{ "en": "en_us", // English "af": "en_us", // Afrikaans (no direct match, default to English) "ar": "ar", // Arabic "hy": "en_us", // Armenian (no direct match, default to English) "be": "en_us", // Belarusian (no direct match, default to English) "bg": "bg_bg", // Bulgarian "ca": "ca_es", // Catalan "zh-CN": "zh_cn", // Chinese Simplified "zh-TW": "zh_hk", // Chinese Traditional "hr": "en_us", // Croatian (no direct match, default to English) "cs": "cs_cz", // Czech "da": "da_dk", // Danish "nl": "nl_nl", // Dutch "eo": "en_us", // Esperanto (no direct match, default to English) "et": "et_ee", // Estonian "tl": "en_us", // Tagalog (no direct match, default to English) "fi": "fi_fi", // Finnish "fr": "fr_fr", // French "de": "de_de", // German "el": "el_gr", // Greek "iw": "he_il", // Hebrew "hi": "en_us", // Hindi (no direct match, default to English) "hu": "hu_hu", // Hungarian "is": "en_us", // Icelandic (no direct match, default to English) "id": "en_us", // Indonesian (no direct match, default to English) "it": "it_it", // Italian "ja": "ja_jp", // Japanese "ko": "ko_kr", // Korean "lv": "en_us", // Latvian (no direct match, default to English) "lt": "en_us", // Lithuanian (no direct match, default to English) "no": "nb_no", // Norwegian "fa": "en_us", // Persian (no direct match, default to English) "pl": "pl_pl", // Polish "pt": "pt_pt", // Portuguese "ro": "ro_ro", // Romanian "ru": "ru_ru", // Russian "sr": "en_us", // Serbian (no direct match, default to English) "sk": "en_us", // Slovak (no direct match, default to English) "sl": "en_us", // Slovenian (no direct match, default to English) "es": "es_es", // Spanish "sw": "en_us", // Swahili (no direct match, default to English) "sv": "sv_se", // Swedish "th": "th_th", // Thai "tr": "tr_tr", // Turkish "uk": "en_us", // Ukrainian (no direct match, default to English) "vi": "en_us", // Vietnamese (no direct match, default to English) "": "en_us", // Default to English if no language is provided } if qwantLocale, exists := langMap[langCode]; exists { return qwantLocale } printWarn("Qwant locale code missing: %v, defaulting to: en_us", langCode) return "en_us" // Default fallback } // PerformQwantImageSearch performs an image search on Qwant and returns the results. func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchResult, time.Duration, error) { startTime := time.Now() // Start the timer const resultsPerPage = 50 var offset int if page <= 1 { offset = 0 } else { offset = (page - 1) * resultsPerPage } // Ensure count + offset is within acceptable limits if offset+resultsPerPage > 250 { return nil, 0, fmt.Errorf("count + offset must be lower than 250 for Qwant") } if safe == "" { safe = "0" } lang = ConvertToQwantLocale(lang) apiURL := fmt.Sprintf("https://api.qwant.com/v3/search/images?t=images&q=%s&count=%d&locale=%s&offset=%d&device=desktop&tgp=2&safesearch=%s", url.QueryEscape(query), resultsPerPage, lang, offset, safe) // Create the HTTP request req, err := http.NewRequest("GET", apiURL, nil) if err != nil { return nil, 0, fmt.Errorf("creating request: %v", err) } // Get the User-Agent string ImageUserAgent, err := GetUserAgent("Image-Search-Qwant") if err != nil { return nil, 0, fmt.Errorf("getting user-agent: %v", err) } req.Header.Set("User-Agent", ImageUserAgent) // Perform the request with MetaProxy if enabled resp, err := DoMetaProxyRequest(req) if err != nil { return nil, 0, fmt.Errorf("making request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } // Read and (if gzip) decompress body var bodyReader io.ReadCloser = resp.Body if resp.Header.Get("Content-Encoding") == "gzip" { gr, err := gzip.NewReader(resp.Body) if err != nil { printDebug("Failed to init gzip reader: %v", err) } else { bodyReader = gr defer gr.Close() } } var apiResp QwantAPIResponse bodyBytes, err := io.ReadAll(bodyReader) if err != nil { printDebug("Failed to read response body: %v", err) } else { printDebug("Qwant response body:\n%s", string(bodyBytes)) } // Decode JSON from bodyBytes if err := json.Unmarshal(bodyBytes, &apiResp); err != nil { return nil, 0, fmt.Errorf("failed to decode JSON: %v\nRaw:\n%s", err, string(bodyBytes)) } // Optional: recreate body for reuse resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) // Process the results var wg sync.WaitGroup results := make([]ImageSearchResult, len(apiResp.Data.Result.Items)) for i, item := range apiResp.Data.Result.Items { wg.Add(1) go func(i int, item struct { Media string `json:"media"` //Thumbnail string `json:"thumbnail"` Title string `json:"title"` Url string `json:"url"` Width int `json:"width"` Height int `json:"height"` }) { defer wg.Done() // Populate the result results[i] = ImageSearchResult{ Thumb: item.Media, // item.Thumbnail is not working Title: item.Title, Full: item.Media, Source: item.Url, Width: item.Width, Height: item.Height, } }(i, item) } // Wait for all goroutines to complete wg.Wait() duration := time.Since(startTime) // Calculate the duration if len(results) == 0 { return nil, duration, fmt.Errorf("no images found") } return results, duration, nil }