212 lines
6.0 KiB
Python
212 lines
6.0 KiB
Python
#!/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()
|