Files
openserp/ecosia/url.go
Martin Czygan 3964653e45 Add search engine ecosia
Ecosia's search results are [in parts] provided by its own search index,
Staan, developed by the co-founded European Search Perspective (EUSP) --
https://en.wikipedia.org/wiki/Ecosia
2026-05-03 13:56:14 +02:00

153 lines
3.8 KiB
Go

package ecosia
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/karust/openserp/core"
)
const (
baseURL = "https://www.ecosia.org/search"
imagesURL = "https://www.ecosia.org/images"
)
// ecosiaFreshness maps a YYYYMMDD..YYYYMMDD DateInterval to Ecosia's freshness
// bucket: <= 1d -> day, <= 7d -> week, <= 31d -> month. Longer spans are
// dropped (returns "", nil) since Ecosia exposes no finer control. Malformed
// input is rejected so non-spec values do not silently lose the filter.
func ecosiaFreshness(dateInterval string) (string, error) {
s := strings.TrimSpace(dateInterval)
if s == "" {
return "", nil
}
parts := strings.Split(s, "..")
if len(parts) != 2 {
return "", errors.New("incorrect date interval provided, expected YYYYMMDD..YYYYMMDD")
}
start, err := time.Parse("20060102", parts[0])
if err != nil {
return "", errors.New("invalid start date format, expected YYYYMMDD")
}
end, err := time.Parse("20060102", parts[1])
if err != nil {
return "", errors.New("invalid end date format, expected YYYYMMDD")
}
span := end.Sub(start)
if span < 0 {
return "", errors.New("date interval end is before start")
}
switch {
case span <= 24*time.Hour:
return "day", nil
case span <= 7*24*time.Hour:
return "week", nil
case span <= 31*24*time.Hour:
return "month", nil
default:
return "", nil
}
}
// BuildURL builds an Ecosia web search URL for the supplied query and
// 0-based page index. q.LangCode is not encoded — Ecosia takes region from
// the ECFG cookie / Accept-Language, set via the browser profile.
func BuildURL(q core.Query, page int) (string, error) {
base, err := url.Parse(baseURL)
if err != nil {
return "", err
}
params := url.Values{}
params.Set("method", "index")
text := q.Text
if q.Site != "" {
text += " site:" + q.Site
}
if q.Filetype != "" {
text += " filetype:" + q.Filetype
}
if strings.TrimSpace(text) == "" {
return "", errors.New("empty query built")
}
params.Set("q", text)
f, err := ecosiaFreshness(q.DateInterval)
if err != nil {
return "", err
}
if f != "" {
params.Set("freshness", f)
}
if page > 0 {
params.Set("p", fmt.Sprintf("%d", page))
}
base.RawQuery = params.Encode()
return base.String(), nil
}
// validImageTypes are the categories Ecosia exposes on /images via the
// imageType URL param; q.Filetype is matched against this set.
var validImageTypes = map[string]struct{}{
"clipart": {},
"photo": {},
"line": {},
"animatedgif": {},
"transparent": {},
}
// BuildImageURL builds an Ecosia image search URL for the supplied query
// and 0-based page index. Image search lives on /images, not /search.
// q.Filetype maps to imageType (clipart, photo, line, animatedgif,
// transparent), not a generic file extension.
//
// Of the six image filters Ecosia exposes (Size, Colour, Type, Time, Layouts,
// License), only Type (q.Filetype -> imageType) and Time (q.DateInterval ->
// freshness) are mapped; size/colour/layouts/license are not mapped currently.
func BuildImageURL(q core.Query, page int) (string, error) {
base, err := url.Parse(imagesURL)
if err != nil {
return "", err
}
text := q.Text
if q.Site != "" {
text += " site:" + q.Site
}
if strings.TrimSpace(text) == "" {
return "", errors.New("empty query built")
}
params := url.Values{}
params.Set("q", text)
f, err := ecosiaFreshness(q.DateInterval)
if err != nil {
return "", err
}
if f != "" {
params.Set("freshness", f)
}
if q.Filetype != "" {
v := strings.ToLower(strings.TrimSpace(q.Filetype))
if _, ok := validImageTypes[v]; !ok {
return "", fmt.Errorf("unsupported imageType %q (want one of: clipart, photo, line, animatedgif, transparent)", q.Filetype)
}
params.Set("imageType", v)
}
if page > 0 {
params.Set("p", fmt.Sprintf("%d", page))
}
base.RawQuery = params.Encode()
return base.String(), nil
}