#!/usr/bin/env python3 """Execute searches via a user-supplied SearXNG instance's JSON API. Calls GET/POST /search?q=...&format=json on the chosen instance. Auto-retries on failure with exponential backoff + tries next instance. Falls back to HTML scraping if the instance blocks JSON output. Instance URLs are REQUIRED (see --instance / SEARXNG_INSTANCE / config file). """ import argparse import json import logging import os import random import sys import time import urllib.error import urllib.parse import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed from html.parser import HTMLParser from pathlib import Path # Allow running standalone from any working directory sys.path.insert(0, str(Path(__file__).resolve().parent)) from _config import SCHEMA_VERSION, USER_AGENT, VERSION from common import ( RETRYABLE_STATUS, RETRY_BACKOFF_BASE, MAX_RETRIES, RECOVERY_HINTS, apply_proxy, build_auth_headers, classify_error, emit_progress, force_utf8_stdout, resolve_auth_basic, resolve_auth_bearer, set_progress_enabled, setup_logging, E_CONFIG, E_AUTH, E_NETWORK, E_RATE_LIMIT, E_PARSE, E_EMPTY, E_INPUT, E_INTERNAL, ) from fetch import extract_text, fetch_url import cache as cache_module logger = logging.getLogger("searxng.search") # ----- Auth helpers ----- def _merge_headers(*header_dicts: dict) -> dict: """Merge multiple header dicts, with later dicts overriding earlier ones.""" result = {} for d in header_dicts: if d: result.update(d) return result # ----- HTML fallback: result extractor ----- class SearXNGHTMLParser(HTMLParser): """Extract search results + metadata from SearXNG's simple theme HTML. Matches:
...

Title

Snippet...

...
...
The ``published_date`` field is populated from the ``