summaryrefslogtreecommitdiff
path: root/searx/engines/braveapi.py
blob: 767808a0aae3d7afa736bc4ad5b55232b33fcce1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Engine to search using the Brave (WEB) Search API.

.. _Brave Search API: https://api-dashboard.search.brave.com/documentation

Configuration
=============

The engine has the following mandatory setting:

- :py:obj:`api_key`

Optional settings are:

- :py:obj:`results_per_page`

.. code:: yaml

  - name: braveapi
    engine: braveapi
    api_key: 'YOUR-API-KEY'  # required
    results_per_page: 20     # optional

The API supports paging and time filters.
"""

import typing as t

from urllib.parse import urlencode
from dateutil import parser

from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults
from searx.utils import html_to_text

if t.TYPE_CHECKING:
    from searx.extended_types import SXNG_Response
    from searx.search.processors import OnlineParams

about = {
    "website": "https://api.search.brave.com/",
    "wikidata_id": None,
    "official_api_documentation": "https://api-dashboard.search.brave.com/api-reference/web/search/get",
    "use_official_api": True,
    "require_api_key": True,
    "results": "JSON",
}

api_key: str = ""
"""API key for Brave Search API (required)."""

categories = ["general", "web"]
paging = True
safesearch = True
time_range_support = True

results_per_page: int = 20
"""Maximum number of results per page (default 20)."""

base_url = "https://api.search.brave.com/res/v1/web/search"
"""Base URL for the Brave Search API."""

time_range_map = {"day": "past_day", "week": "past_week", "month": "past_month", "year": "past_year"}
"""Mapping of SearXNG time ranges to Brave API time ranges."""

max_page = 10


def setup(_: dict[str, t.Any]) -> bool | None:
    """Initialize the engine."""
    if not api_key:
        raise SearxEngineAPIException("No API key provided")


def request(query: str, params: "OnlineParams") -> None:
    """Create the API request."""
    search_args: dict[str, str | int | None] = {
        "q": query,
        "count": results_per_page,
        "offset": params["pageno"] - 1,
        "text_decorations": False,
    }

    # Apply time filter if specified
    if params["time_range"]:
        search_args["time_range"] = time_range_map.get(params["time_range"])

    # Apply SafeSearch if enabled
    if params["safesearch"]:
        search_args["safesearch"] = "strict"

    params["url"] = f"{base_url}?{urlencode(search_args)}"
    params["headers"]["X-Subscription-Token"] = api_key


def _extract_published_date(published_date_raw: str):
    """Extract and parse the published date from the API response.

    Args:
        published_date_raw: Raw date string from the API

    Returns:
        Parsed datetime object or None if parsing fails
    """
    if not published_date_raw:
        return None

    try:
        return parser.parse(published_date_raw)
    except parser.ParserError:
        return None


def response(resp: "SXNG_Response") -> EngineResults:
    """Process the API response and return results."""
    res = EngineResults()
    data = resp.json()

    for result in (data.get("web") or {}).get("results", []):
        thumbnail_obj = result.get("thumbnail")
        thumbnail = ""
        if thumbnail_obj and not thumbnail_obj.get("logo", False):
            thumbnail = thumbnail_obj.get("src") or ""

        res.add(
            res.types.MainResult(
                url=result["url"],
                title=html_to_text(result["title"]),
                content=html_to_text(result.get("description", "")),
                publishedDate=_extract_published_date(result.get("age")),
                thumbnail=thumbnail,
            ),
        )

    return res