Three new image search engines: - bing_images: Bing Images via RSS endpoint - ddg_images: DuckDuckGo Images via VQD API - qwant_images: Qwant Images via v3 search API Frontend: - Image grid layout with responsive columns - image_item template with thumbnail, title, and source metadata - Hover animations and lazy loading - Grid activates automatically when category=images Backend: - category=images routes to image engines via planner - Image engines registered in factory and engine allowlist - extractImgSrc helper for parsing thumbnail URLs from HTML - IsImageSearch flag on PageData for template layout switching
123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
// kafka — a privacy-respecting metasearch engine
|
|
// Copyright (C) 2026-present metamorphosis-dev
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
package engines
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/metamorphosis-dev/kafka/internal/contracts"
|
|
)
|
|
|
|
// BingImagesEngine searches Bing Images via their public RSS endpoint.
|
|
type BingImagesEngine struct {
|
|
client *http.Client
|
|
}
|
|
|
|
func (e *BingImagesEngine) Name() string { return "bing_images" }
|
|
|
|
func (e *BingImagesEngine) Search(ctx context.Context, req contracts.SearchRequest) (contracts.SearchResponse, error) {
|
|
if e == nil || e.client == nil {
|
|
return contracts.SearchResponse{}, errors.New("bing_images engine not initialized")
|
|
}
|
|
q := strings.TrimSpace(req.Query)
|
|
if q == "" {
|
|
return contracts.SearchResponse{Query: req.Query}, nil
|
|
}
|
|
|
|
offset := (req.Pageno - 1) * 10
|
|
endpoint := fmt.Sprintf(
|
|
"https://www.bing.com/images/search?q=%s&count=10&offset=%d&format=rss",
|
|
url.QueryEscape(q),
|
|
offset,
|
|
)
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return contracts.SearchResponse{}, err
|
|
}
|
|
httpReq.Header.Set("User-Agent", "kafka/0.1 (compatible; +https://git.ashisgreat.xyz/penal-colony/kafka)")
|
|
|
|
resp, err := e.client.Do(httpReq)
|
|
if err != nil {
|
|
return contracts.SearchResponse{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
|
|
return contracts.SearchResponse{}, fmt.Errorf("bing_images upstream error: status %d", resp.StatusCode)
|
|
}
|
|
|
|
return parseBingImagesRSS(resp.Body, req.Query)
|
|
}
|
|
|
|
// parseBingImagesRSS parses Bing's RSS image search results.
|
|
// The description field contains HTML with an <img> tag whose src is the
|
|
// thumbnail and whose enclosing <a> tag links to the source page.
|
|
func parseBingImagesRSS(r io.Reader, query string) (contracts.SearchResponse, error) {
|
|
type bingImageItem struct {
|
|
Title string `xml:"title"`
|
|
Link string `xml:"link"`
|
|
Descrip string `xml:"description"`
|
|
}
|
|
|
|
type rssFeed struct {
|
|
XMLName xml.Name `xml:"rss"`
|
|
Channel struct {
|
|
Items []bingImageItem `xml:"item"`
|
|
} `xml:"channel"`
|
|
}
|
|
|
|
var rss rssFeed
|
|
if err := xml.NewDecoder(r).Decode(&rss); err != nil {
|
|
return contracts.SearchResponse{}, fmt.Errorf("bing_images RSS parse error: %w", err)
|
|
}
|
|
|
|
results := make([]contracts.MainResult, 0, len(rss.Channel.Items))
|
|
for _, item := range rss.Channel.Items {
|
|
if item.Link == "" {
|
|
continue
|
|
}
|
|
|
|
// Extract thumbnail URL from the description HTML.
|
|
thumbnail := extractImgSrc(item.Descrip)
|
|
content := stripHTML(item.Descrip)
|
|
|
|
linkPtr := item.Link
|
|
results = append(results, contracts.MainResult{
|
|
Template: "images",
|
|
Title: item.Title,
|
|
Content: content,
|
|
URL: &linkPtr,
|
|
Thumbnail: thumbnail,
|
|
Engine: "bing_images",
|
|
Score: 0,
|
|
Category: "images",
|
|
Engines: []string{"bing_images"},
|
|
})
|
|
}
|
|
|
|
return contracts.SearchResponse{
|
|
Query: query,
|
|
NumberOfResults: len(results),
|
|
Results: results,
|
|
Answers: []map[string]any{},
|
|
Corrections: []string{},
|
|
Infoboxes: []map[string]any{},
|
|
Suggestions: []string{},
|
|
UnresponsiveEngines: [][2]string{},
|
|
}, nil
|
|
}
|