mirror of
https://github.com/wahyd4/hey-search.git
synced 2026-08-08 21:05:14 +10:00
115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
"""DuckDuckGo Search engine implementation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from urllib.parse import urlencode
|
|
|
|
from lxml import html as lxml_html
|
|
|
|
from app.models import WebResult, ImageResult
|
|
from app.engines.base import SearchEngine, get_http_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DDG_HTML_URL = "https://html.duckduckgo.com/html/"
|
|
DDG_LITE_URL = "https://lite.duckduckgo.com/lite/"
|
|
|
|
|
|
class DuckDuckGoEngine(SearchEngine):
|
|
name = "duckduckgo"
|
|
display_name = "DuckDuckGo"
|
|
supports_web = True
|
|
supports_images = True
|
|
|
|
async def search_web(self, query: str, page: int = 1) -> list[WebResult]:
|
|
data = {"q": query, "b": "", "kl": "wt-wt"}
|
|
client = get_http_client()
|
|
resp = await client.post(
|
|
DDG_HTML_URL,
|
|
data=data,
|
|
headers={
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Referer": DDG_HTML_URL,
|
|
},
|
|
)
|
|
resp.raise_for_status()
|
|
|
|
results: list[WebResult] = []
|
|
dom = lxml_html.fromstring(resp.text)
|
|
|
|
for div in dom.xpath('//div[@id="links"]/div[contains(@class, "web-result")]'):
|
|
title_els = div.xpath(".//h2/a")
|
|
if not title_els:
|
|
continue
|
|
title = title_els[0].text_content().strip()
|
|
url_els = div.xpath(".//h2/a/@href")
|
|
if not url_els:
|
|
continue
|
|
url = url_els[0]
|
|
content_els = div.xpath('.//a[contains(@class, "result__snippet")]')
|
|
content = content_els[0].text_content().strip() if content_els else ""
|
|
results.append(WebResult(title=title, url=url, content=content, engine=self.name))
|
|
|
|
return results
|
|
|
|
async def search_images(self, query: str, page: int = 1) -> list[ImageResult]:
|
|
"""Search images via DDG's i.js API (requires vqd token)."""
|
|
client = get_http_client()
|
|
# First get a vqd token from the HTML page
|
|
token_resp = await client.get(
|
|
f"https://duckduckgo.com/?{urlencode({'q': query, 'iax': 'images', 'ia': 'images'})}",
|
|
)
|
|
token_resp.raise_for_status()
|
|
|
|
vqd = ""
|
|
for line in token_resp.text.split("\n"):
|
|
if "vqd=" in line or "vqd'" in line or 'vqd"' in line:
|
|
# Try to extract vqd value
|
|
import re
|
|
m = re.search(r"vqd=['\"]?([^&'\"]+)", line)
|
|
if m:
|
|
vqd = m.group(1)
|
|
break
|
|
|
|
if not vqd:
|
|
# Fallback: try extracting from form
|
|
dom = lxml_html.fromstring(token_resp.text)
|
|
vqd_els = dom.xpath('//input[@name="vqd"]/@value')
|
|
if vqd_els:
|
|
vqd = vqd_els[0]
|
|
|
|
results: list[ImageResult] = []
|
|
if not vqd:
|
|
return results
|
|
|
|
img_resp = await client.get(
|
|
f"https://duckduckgo.com/i.js?{urlencode({'q': query, 'vqd': vqd, 'o': 'json'})}",
|
|
headers={"Referer": "https://duckduckgo.com/"},
|
|
)
|
|
if img_resp.is_success:
|
|
data = img_resp.json()
|
|
for item in data.get("results", [])[:20]:
|
|
results.append(ImageResult(
|
|
title=item.get("title", ""),
|
|
url=item.get("url", ""),
|
|
img_src=item.get("image", ""),
|
|
thumbnail_src=item.get("thumbnail", ""),
|
|
source=item.get("source", ""),
|
|
engine=self.name,
|
|
))
|
|
|
|
return results
|
|
|
|
async def autocomplete(self, query: str) -> list[str]:
|
|
client = get_http_client()
|
|
resp = await client.get(
|
|
f"https://duckduckgo.com/ac/?{urlencode({'q': query, 'type': 'list'})}",
|
|
)
|
|
if resp.is_success:
|
|
data = resp.json()
|
|
if isinstance(data, list) and len(data) > 1:
|
|
return data[1][:10]
|
|
return []
|