# SPDX-License-Identifier: AGPL-3.0-or-later """Kagi_ is a paid, privacy-focused search engine. Using it requires an API key. If you have a Kagi account, you can obtain an API key in the `API portal`_. To enable Kagi, add the following to the ``engines`` seciton of ``settings.yml``: .. code:: yaml - name: kagi engine: kagi categories: [general, web] shortcut: kg api_key: "" kagi_categ: search - name: kagi.news engine: kagi categories: [news, web] shortcut: kgn api_key: "" kagi_categ: news - name: kagi.images engine: kagi categories: [images, web] shortcut: kgi paging: false api_key: "" kagi_categ: images - name: kagi.videos engine: kagi categories: [videos, web] shortcut: kgv api_key: "" kagi_categ: videos .. _Kagi: https://kagi.com .. _Api Portal: https://help.kagi.com/kagi/api/overview.html """ from datetime import datetime, timedelta import typing as t import html from searx.extended_types import SXNG_Response from searx.result_types import EngineResults from searx.utils import parse_duration_string if t.TYPE_CHECKING: from searx.search.processors import OnlineParams TimeRangeType = t.Literal["day", "week", "month", "year"] about = { "website": "https://kagi.com", "wikidata_id": "Q26000117", "official_api_documentation": "https://kagi.com/api/docs/openapi", "use_official_api": True, "require_api_key": True, "results": "JSON", } paging = True """All categories except the ``images`` category support paging.""" safesearch = True time_range_support = True categories = ["general"] kagi_categ: t.Literal["search", "images", "news", "videos"] = "search" """Search category. Supported values: "search" (general), "images", "news", "videos".""" base_url = "https://kagi.com" safe_search_map = {0: False, 1: True, 2: True} time_range_to_days_map: dict[TimeRangeType, int] = {"day": 1, "week": 7, "month": 30, "year": 365} api_key = "" """Kagi API key. Required for using this engine.""" def init(_): if not api_key: raise ValueError("api_key is required for using kagi") if kagi_categ not in ("search", "images", "news", "videos"): raise ValueError(f"Unsupported category: {kagi_categ}") # pyright: ignore[reportUnreachable] def request(query: str, params: "OnlineParams"): # According to the API docs, Kagi supports at maximum page 10 if params["pageno"] > 10: return params["headers"]["Authorization"] = f"Bearer {api_key}" params["url"] = f"{base_url}/api/v1/search" filters = {} time_range = params.get("time_range") if time_range: # Kagi expects the minimum date to return results from as argument to `after` time_period = timedelta(days=time_range_to_days_map[time_range]) oldest_result_date = datetime.now() - time_period filters["after"] = oldest_result_date.strftime("%Y-%m-%d") # there doesn't seem to be a list of languages anywhere, # so we just assume that it supports all languages filters["region"] = "no_region" if params["searxng_locale"] != "all": _locale = params["searxng_locale"].split("-") if len(_locale) > 1: filters["region"] = _locale[-1].lower() args: dict[str, t.Any] = { "query": query, "page": params["pageno"], "workflow": kagi_categ, "safe_search": safe_search_map[params["safesearch"]], "filters": filters, } params["method"] = "POST" params["json"] = args def response(resp: "SXNG_Response") -> EngineResults: res = EngineResults() json_data: dict[str, t.Any] = resp.json() if kagi_categ in ("images", "videos"): # the JSON key is "image" for "images" and "video" for "videos" json_results = json_data["data"][kagi_categ[:-1]] else: json_results = json_data["data"][kagi_categ] for result in json_results: published_date: datetime | None = None if result.get("time"): published_date = datetime.fromisoformat(result["time"]) if kagi_categ in ("search", "news"): res.add( res.types.MainResult( url=result["url"], title=html.unescape(result["title"]), content=html.unescape(result["snippet"]), thumbnail=result.get("image", {}).get("url") or "", publishedDate=published_date, ) ) elif kagi_categ == "images": res.add( res.types.Image( url=result["url"], title=html.unescape(result.get("title")), img_src=result.get("image", {}).get("url"), resolution=f"{result['image']['width']}x{result['image']['height']}", thumbnail_src=result.get("props", {}).get("thumbnail", {}).get("url"), ) ) elif kagi_categ == "videos": length: timedelta | None = None if result["props"].get("duration"): length = parse_duration_string(result["props"]["duration"]) res.add( res.types.LegacyResult( { "template": "videos.html", "url": result["url"], "title": html.unescape(result["title"]), "content": html.unescape(result["snippet"]), "thumbnail": result.get("image", {}).get("url"), "publishedDate": published_date, "author": result["props"].get("creator_name"), "length": length, } ) ) for suggestion in json_data["data"].get("related_search", []): res.add(res.types.LegacyResult({"suggestion": suggestion["title"]})) return res