Files

212 lines
6.0 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "rich"]
# ///
"""SearXNG CLI - Privacy-respecting metasearch via your local instance."""
import argparse
import os
import sys
import json
import warnings
import httpx
from rich.console import Console
from rich.table import Table
from rich import print as rprint
from urllib.parse import urlencode
# Suppress SSL warnings for local self-signed certificates
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
console = Console()
SEARXNG_URL = os.getenv("SEARXNG_URL", "http://localhost:8080")
def search_searxng(
query: str,
limit: int = 10,
category: str = "general",
language: str = "auto",
time_range: str = None,
output_format: str = "table"
) -> dict:
"""
Search using SearXNG instance.
Args:
query: Search query string
limit: Number of results to return
category: Search category (general, images, news, videos, etc.)
language: Language code (auto, en, de, fr, etc.)
time_range: Time range filter (day, week, month, year)
output_format: Output format (table, json)
Returns:
Dict with search results
"""
params = {
"q": query,
"format": "json",
"categories": category,
}
if language != "auto":
params["language"] = language
if time_range:
params["time_range"] = time_range
try:
# Disable SSL verification for local self-signed certs
response = httpx.get(
f"{SEARXNG_URL}/search",
params=params,
timeout=30,
verify=False # For local self-signed certs
)
response.raise_for_status()
data = response.json()
# Limit results
if "results" in data:
data["results"] = data["results"][:limit]
return data
except httpx.HTTPError as e:
console.print(f"[red]Error connecting to SearXNG:[/red] {e}", file=sys.stderr)
return {"error": str(e), "results": []}
except Exception as e:
console.print(f"[red]Unexpected error:[/red] {e}", file=sys.stderr)
return {"error": str(e), "results": []}
def display_results_table(data: dict, query: str):
"""Display search results in a rich table."""
results = data.get("results", [])
if not results:
rprint(f"[yellow]No results found for:[/yellow] {query}")
return
table = Table(title=f"SearXNG Search: {query}", show_lines=False)
table.add_column("#", style="dim", width=3)
table.add_column("Title", style="bold")
table.add_column("URL", style="blue", width=50)
table.add_column("Engines", style="green", width=20)
for i, result in enumerate(results, 1):
title = result.get("title", "No title")[:70]
url = result.get("url", "")[:45] + "..."
engines = ", ".join(result.get("engines", []))[:18]
table.add_row(
str(i),
title,
url,
engines
)
console.print(table)
# Show additional info
if data.get("number_of_results"):
rprint(f"\n[dim]Total results available: {data['number_of_results']}[/dim]")
# Show content snippets for top 3
rprint("\n[bold]Top results:[/bold]")
for i, result in enumerate(results[:3], 1):
title = result.get("title", "No title")
url = result.get("url", "")
content = result.get("content", "")[:200]
rprint(f"\n[bold cyan]{i}. {title}[/bold cyan]")
rprint(f" [blue]{url}[/blue]")
if content:
rprint(f" [dim]{content}...[/dim]")
def display_results_json(data: dict):
"""Display results in JSON format for programmatic use."""
print(json.dumps(data, indent=2))
def main():
parser = argparse.ArgumentParser(
description="SearXNG CLI - Search the web via your local SearXNG instance",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
Examples:
%(prog)s search "python asyncio"
%(prog)s search "climate change" -n 20
%(prog)s search "cute cats" --category images
%(prog)s search "breaking news" --category news --time-range day
%(prog)s search "rust tutorial" --format json
Environment:
SEARXNG_URL: SearXNG instance URL (default: {SEARXNG_URL})
"""
)
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Search command
search_parser = subparsers.add_parser("search", help="Search the web")
search_parser.add_argument("query", nargs="+", help="Search query")
search_parser.add_argument(
"-n", "--limit",
type=int,
default=10,
help="Number of results (default: 10)"
)
search_parser.add_argument(
"-c", "--category",
default="general",
choices=["general", "images", "videos", "news", "map", "music", "files", "it", "science"],
help="Search category (default: general)"
)
search_parser.add_argument(
"-l", "--language",
default="auto",
help="Language code (auto, en, de, fr, etc.)"
)
search_parser.add_argument(
"-t", "--time-range",
choices=["day", "week", "month", "year"],
help="Time range filter"
)
search_parser.add_argument(
"-f", "--format",
choices=["table", "json"],
default="table",
help="Output format (default: table)"
)
args = parser.parse_args()
if not args.command:
parser.print_help()
return
if args.command == "search":
query = " ".join(args.query)
data = search_searxng(
query=query,
limit=args.limit,
category=args.category,
language=args.language,
time_range=args.time_range,
output_format=args.format
)
if args.format == "json":
display_results_json(data)
else:
display_results_table(data, query)
if __name__ == "__main__":
main()