how to download DDG image with golang

While DuckDuckGo offers Instant Answer APIs, it does not provide URLs to download images, certainly because of copyright isusses.

One way is to fetch a vqd token from an initial basic request, then use it to find the image URLs, and finally download the image.

package main
 
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "net/http"
    "regexp"
    "strings"
)
 
func main() {
    arg := os.Args[1]
 
    img := make(chan *ImgFetchResult)
	f := &DDGImgFetcher{}
	go f.Fetch(img, arg)
	result := <-img
	if result.Error != nil {
		log.Fatalln(result.Error)
	}
	createFile(result.Img, "/tmp/"+arg+"."+result.ContentType)
}
 
func createFile(content []byte, filePath string) {
	file, err := os.Create(filePath)
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()
 
	_, err = io.Copy(file, bytes.NewReader(content))
	if err != nil {
		log.Fatalln(err)
	}
}
 
// DDGImgFetcher fetches the image from duckduckgo
type DDGImgFetcher struct {
}
 
// ImgFetchResult is the representation of the result to fetch the image
// It wraps an error in case something went wrong
type ImgFetchResult struct {
	Img         []byte
	ContentType string
	Error       error
}
 
const ddgURL = "https://duckduckgo.com"
 
// Fetch the image from duckduckgo
// Code inspired from https://github.com/deepanprabhu/duckduckgo-images-api/blob/master/duckduckgo_images_api/api.py
func (f *DDGImgFetcher) Fetch(result chan *ImgFetchResult, title string) {
	token, err := fetchToken(title)
	if err != nil {
		result <- &ImgFetchResult{nil, "", err}
		return
	}
	imgURL, err := fetchImgURL(title, token)
	if err != nil {
		result <- &ImgFetchResult{nil, "", err}
		return
	}
	img, contentType, err := downloadImg(imgURL)
	if err != nil {
		result <- &ImgFetchResult{nil, "", err}
		return
	}
	result <- &ImgFetchResult{img, contentType, nil}
}
 
// fetchToken needed to perform the image search
func fetchToken(title string) (string, error) {
	req, err := http.NewRequest("GET", ddgURL, nil)
	if err != nil {
		return "", fmt.Errorf("Could not build the request for URL %s. Error was %s", ddgURL, err)
	}
	q := req.URL.Query()
	q.Add("q", title)
	req.URL.RawQuery = q.Encode()
	resp, err := http.Get(req.URL.String())
	if err != nil {
		return "", fmt.Errorf("Could not fetch the result of %s", req.URL.String())
	}
	if resp.StatusCode != 200 {
		return "", fmt.Errorf("Duckduckgo returns %v status code", resp.StatusCode)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("Could not read the response body from duckduckgo. Error was %s", err)
	}
 
	content := string(body)
 
	r := regexp.MustCompile("vqd=([\\d-]+)")
	token := strings.ReplaceAll(r.FindString(content), "vqd=", "")
 
	return token, nil
}
 
func fetchImgURL(title, token string) (string, error) {
	url := ddgURL + "/i.js"
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "", fmt.Errorf("Could not build the request for URL %s. Error was %s", url, err)
	}
	q := req.URL.Query()
	q.Add("l", "wt-wt")
	q.Add("o", "json")
	q.Add("q", title)
	q.Add("vqd", token)
	q.Add("f", ",,,")
	q.Add("p", "2")
	req.URL.RawQuery = q.Encode()
	resp, err := http.Get(req.URL.String())
	if err != nil {
		return "", fmt.Errorf("Could not fetch the result of %s", req.URL.String())
	}
	if resp.StatusCode != 200 {
		return "", fmt.Errorf("Duckduckgo returns %v status code", resp.StatusCode)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("Could not read the response body from duckduckgo. Error was %s", err)
	}
	var data map[string]interface{}
	err = json.Unmarshal(body, &data)
	if err != nil {
		return "", fmt.Errorf("Could not parse the response body from wikipedia. Error was %s", err.Error())
	}
 
	results := data["results"].([]interface{})
	firstResult := results[0].(map[string]interface{})
	imgURL := firstResult["image"].(string)
	return imgURL, nil
}
 
func downloadImg(imgURL string) ([]byte, string, error) {
	resp, err := http.Get(imgURL)
	if err != nil {
		return nil, "", fmt.Errorf("Could not download the image from %s", imgURL)
	}
	if resp.StatusCode != 200 {
		return nil, "", fmt.Errorf("Could not download the image from %s. Status comde was %v", imgURL, resp.StatusCode)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("Could not read the response body from %s. Error was %s", imgURL, err.Error())
	}
	contentType := strings.ToLower(strings.ReplaceAll(resp.Header.Get("Content-Type"), "image/", ""))
	return body, contentType, nil
}
# Copy above content to main.go file
vim main.go
# Run with the following
go run main.go

References