Files
openserp/duckduckgo/url.go

217 lines
5.3 KiB
Go

package duckduckgo
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/karust/openserp/core"
)
const baseURL = "https://duckduckgo.com"
// ddgKLByLocale maps lowercase BCP47 codes ("en", "en-gb", "zh-tw") to
// DuckDuckGo's "kl" parameter, which uses an inverted region-language form
// (e.g. "uk-en"). Lookup is locale-first, then language-only as a fallback.
var ddgKLByLocale = map[string]string{
"en": "us-en",
"en-us": "us-en",
"en-gb": "uk-en",
"en-au": "au-en",
"en-ca": "ca-en",
"de": "de-de",
"de-at": "at-de",
"de-ch": "ch-de",
"fr": "fr-fr",
"fr-ca": "ca-fr",
"fr-be": "be-fr",
"fr-ch": "ch-fr",
"es": "es-es",
"es-mx": "mx-es",
"es-ar": "ar-es",
"it": "it-it",
"nl": "nl-nl",
"nl-be": "be-nl",
"pt": "pt-pt",
"pt-br": "br-pt",
"ru": "ru-ru",
"pl": "pl-pl",
"cs": "cz-cs",
"sk": "sk-sk",
"hu": "hu-hu",
"ro": "ro-ro",
"da": "dk-da",
"sv": "se-sv",
"no": "no-no",
"fi": "fi-fi",
"tr": "tr-tr",
"el": "gr-el",
"he": "il-he",
"ar": "xa-ar",
"zh": "cn-zh",
"zh-cn": "cn-zh",
"zh-tw": "tw-zh",
"ja": "jp-ja",
"ko": "kr-ko",
}
// duckDuckGoKL resolves a DuckDuckGo "kl" value for the supplied language code.
// Returns "" when the input has no known mapping so callers can omit the
// parameter rather than send an unrecognized region.
func duckDuckGoKL(langCode, region string) string {
locale := core.ParseLocale(langCode)
if locale.Language == "" {
return ""
}
country := core.CountryFromRegion(region)
if country == "" {
country = locale.Country
}
if country != "" {
key := locale.Language + "-" + strings.ToLower(country)
if kl, ok := ddgKLByLocale[key]; ok {
return kl
}
}
return ddgKLByLocale[locale.Language]
}
// BuildURL builds a DuckDuckGo web search URL for the provided query and page
// index. It returns an error when query text or date parameters are invalid.
func BuildURL(q core.Query, page int) (string, error) {
base, err := url.Parse(baseURL)
if err != nil {
return "", err
}
base.Path += ""
params := url.Values{}
// Set request text
if q.Text != "" || q.Site != "" || q.Filetype != "" {
text := q.Text
if q.Site != "" {
text += " site:" + q.Site
}
if q.Filetype != "" {
text += " filetype:" + q.Filetype
}
params.Add("q", text)
}
if len(params.Get("q")) == 0 {
return "", errors.New("empty query built")
}
// Set search date range
if q.DateInterval != "" {
intervals := strings.Split(q.DateInterval, "..")
if len(intervals) != 2 {
return "", errors.New("incorrect date interval provided")
}
// Convert from YYYYMMDD to YYYY-MM-DD format (DuckDuckGo requirement)
startDate, err := time.Parse("20060102", intervals[0])
if err != nil {
return "", errors.New("invalid start date format, expected YYYYMMDD")
}
endDate, err := time.Parse("20060102", intervals[1])
if err != nil {
return "", errors.New("invalid end date format, expected YYYYMMDD")
}
// DuckDuckGo uses YYYY-MM-DD..YYYY-MM-DD format
dateRange := fmt.Sprintf("%s..%s",
startDate.Format("2006-01-02"),
endDate.Format("2006-01-02"))
params.Add("df", dateRange)
}
if kl := duckDuckGoKL(q.LangCode, q.Region); kl != "" {
params.Add("kl", kl)
}
// DuckDuckGo specific parameters
params.Add("t", "h") // HTML format
params.Add("ia", "web") // Web search
// Add pagination parameter if not on first page
if page > 0 {
// DuckDuckGo uses 's' parameter for pagination (start offset)
// Each page has approximately 25-30 results, but we'll use conservative estimate
offset := page * 25
params.Add("s", fmt.Sprintf("%d", offset))
}
base.RawQuery = params.Encode()
return base.String(), nil
}
// BuildImageURL builds a DuckDuckGo image search URL from Query fields.
// It returns an error when query text or date parameters are invalid.
func BuildImageURL(q core.Query) (string, error) {
base, err := url.Parse(baseURL)
if err != nil {
return "", err
}
base.Path += ""
params := url.Values{}
params.Add("t", "h") // HTML format
params.Add("iax", "images") // Image search
params.Add("ia", "images")
// Set request text
if q.Text != "" || q.Site != "" || q.Filetype != "" {
text := q.Text
if q.Site != "" {
text += " site:" + q.Site
}
if q.Filetype != "" {
text += " filetype:" + q.Filetype
}
params.Add("q", text)
}
if len(params.Get("q")) == 0 {
return "", errors.New("empty query built")
}
// Set search date range
if q.DateInterval != "" {
intervals := strings.Split(q.DateInterval, "..")
if len(intervals) != 2 {
return "", errors.New("incorrect date interval provided")
}
// Convert from YYYYMMDD to YYYY-MM-DD format (DuckDuckGo requirement)
startDate, err := time.Parse("20060102", intervals[0])
if err != nil {
return "", errors.New("invalid start date format, expected YYYYMMDD")
}
endDate, err := time.Parse("20060102", intervals[1])
if err != nil {
return "", errors.New("invalid end date format, expected YYYYMMDD")
}
// DuckDuckGo uses YYYY-MM-DD..YYYY-MM-DD format
dateRange := fmt.Sprintf("%s..%s",
startDate.Format("2006-01-02"),
endDate.Format("2006-01-02"))
params.Add("df", dateRange)
}
if kl := duckDuckGoKL(q.LangCode, q.Region); kl != "" {
params.Add("kl", kl)
}
base.RawQuery = params.Encode()
return base.String(), nil
}