Format: text/x-python
⬇ Download File (13.2 KB)
   1  from __future__ import annotations
   2  
   3  import csv
   4  import logging
   5  import os
   6  from concurrent.futures import ThreadPoolExecutor, as_completed
   7  from datetime import datetime
   8  from pathlib import Path
   9  from urllib.parse import unquote
  10  
  11  import click
  12  import primp
  13  
  14  from .duckduckgo_search import DDGS
  15  from .utils import _expand_proxy_tb_alias, json_dumps
  16  from .version import __version__
  17  
  18  logger = logging.getLogger(__name__)
  19  
  20  COLORS = {
  21      0: "black",
  22      1: "red",
  23      2: "green",
  24      3: "yellow",
  25      4: "blue",
  26      5: "magenta",
  27      6: "cyan",
  28      7: "bright_black",
  29      8: "bright_red",
  30      9: "bright_green",
  31      10: "bright_yellow",
  32      11: "bright_blue",
  33      12: "bright_magenta",
  34      13: "bright_cyan",
  35      14: "white",
  36      15: "bright_white",
  37  }
  38  
  39  
  40  def _save_data(keywords: str, data: list[dict[str, str]], function_name: str, filename: str | None) -> None:
  41      filename, ext = filename.rsplit(".", 1) if filename and filename.endswith((".csv", ".json")) else (None, filename)
  42      filename = filename if filename else f"{function_name}_{keywords}_{datetime.now():%Y%m%d_%H%M%S}"
  43      if ext == "csv":
  44          _save_csv(f"{filename}.{ext}", data)
  45      elif ext == "json":
  46          _save_json(f"{filename}.{ext}", data)
  47  
  48  
  49  def _save_json(jsonfile: str | Path, data: list[dict[str, str]]) -> None:
  50      with open(jsonfile, "w", encoding="utf-8") as file:
  51          file.write(json_dumps(data))
  52  
  53  
  54  def _save_csv(csvfile: str | Path, data: list[dict[str, str]]) -> None:
  55      with open(csvfile, "w", newline="", encoding="utf-8") as file:
  56          if data:
  57              headers = data[0].keys()
  58              writer = csv.DictWriter(file, fieldnames=headers, quoting=csv.QUOTE_MINIMAL)
  59              writer.writeheader()
  60              writer.writerows(data)
  61  
  62  
  63  def _print_data(data: list[dict[str, str]]) -> None:
  64      if data:
  65          for i, e in enumerate(data, start=1):
  66              click.secho(f"{i}.\t    {'=' * 78}", bg="black", fg="white")
  67              for j, (k, v) in enumerate(e.items(), start=1):
  68                  if v:
  69                      width = 300 if k in ("content", "href", "image", "source", "thumbnail", "url") else 78
  70                      k = "language" if k == "detected_language" else k
  71                      text = click.wrap_text(
  72                          f"{v}", width=width, initial_indent="", subsequent_indent=" " * 12, preserve_paragraphs=True
  73                      )
  74                  else:
  75                      text = v
  76                  click.secho(f"{k:<12}{text}", bg="black", fg=COLORS[j], overline=True)
  77              input()
  78  
  79  
  80  def _sanitize_keywords(keywords: str) -> str:
  81      keywords = (
  82          keywords.replace("filetype", "")
  83          .replace(":", "")
  84          .replace('"', "'")
  85          .replace("site", "")
  86          .replace(" ", "_")
  87          .replace("/", "_")
  88          .replace("\\", "_")
  89          .replace(" ", "")
  90      )
  91      return keywords
  92  
  93  
  94  def _download_file(url: str, dir_path: str, filename: str, proxy: str | None, verify: bool) -> None:
  95      try:
  96          resp = primp.Client(proxy=proxy, impersonate="random", impersonate_os="random", timeout=10, verify=verify).get(
  97              url
  98          )
  99          if resp.status_code == 200:
 100              with open(os.path.join(dir_path, filename[:200]), "wb") as file:
 101                  file.write(resp.content)
 102      except Exception as ex:
 103          logger.debug(f"download_file url={url} {type(ex).__name__} {ex}")
 104  
 105  
 106  def _download_results(
 107      keywords: str,
 108      results: list[dict[str, str]],
 109      function_name: str,
 110      proxy: str | None = None,
 111      threads: int | None = None,
 112      verify: bool = True,
 113      pathname: str | None = None,
 114  ) -> None:
 115      path = pathname if pathname else f"{function_name}_{keywords}_{datetime.now():%Y%m%d_%H%M%S}"
 116      os.makedirs(path, exist_ok=True)
 117  
 118      threads = 10 if threads is None else threads
 119      with ThreadPoolExecutor(max_workers=threads) as executor:
 120          futures = []
 121          for i, res in enumerate(results, start=1):
 122              url = res["image"] if function_name == "images" else res["href"]
 123              filename = unquote(url.split("/")[-1].split("?")[0])
 124              f = executor.submit(_download_file, url, path, f"{i}_{filename}", proxy, verify)
 125              futures.append(f)
 126  
 127          with click.progressbar(  # type: ignore
 128              length=len(futures), label="Downloading", show_percent=True, show_pos=True, width=50
 129          ) as bar:
 130              for future in as_completed(futures):
 131                  future.result()
 132                  bar.update(1)
 133  
 134  
 135  @click.group(chain=True)
 136  def cli() -> None:
 137      """duckduckgo_search CLI tool"""
 138      pass
 139  
 140  
 141  def safe_entry_point() -> None:
 142      try:
 143          cli()
 144      except Exception as ex:
 145          click.echo(f"{type(ex).__name__}: {ex}")
 146  
 147  
 148  @cli.command()
 149  def version() -> str:
 150      print(__version__)
 151      return __version__
 152  
 153  
 154  @cli.command()
 155  @click.option("-k", "--keywords", required=True, help="text search, keywords for query")
 156  @click.option("-r", "--region", default="wt-wt", help="wt-wt, us-en, ru-ru, etc. -region https://duckduckgo.com/params")
 157  @click.option("-s", "--safesearch", default="moderate", type=click.Choice(["on", "moderate", "off"]))
 158  @click.option("-t", "--timelimit", type=click.Choice(["d", "w", "m", "y"]), help="day, week, month, year")
 159  @click.option("-m", "--max_results", type=int, help="maximum number of results")
 160  @click.option("-o", "--output", help="csv, json or filename.csv|json (save the results to a csv or json file)")
 161  @click.option("-d", "--download", is_flag=True, default=False, help="download results. -dd to set custom directory")
 162  @click.option("-dd", "--download-directory", help="Specify custom download directory")
 163  @click.option("-b", "--backend", default="auto", type=click.Choice(["auto", "html", "lite"]))
 164  @click.option("-th", "--threads", default=10, help="download threads, default=10")
 165  @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150")
 166  @click.option("-v", "--verify", default=True, help="verify SSL when making the request")
 167  def text(
 168      keywords: str,
 169      region: str,
 170      safesearch: str,
 171      timelimit: str | None,
 172      backend: str,
 173      output: str | None,
 174      download: bool,
 175      download_directory: str | None,
 176      threads: int,
 177      max_results: int | None,
 178      proxy: str | None,
 179      verify: bool,
 180  ) -> None:
 181      """CLI function to perform a text search using DuckDuckGo API."""
 182      data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).text(
 183          keywords=keywords,
 184          region=region,
 185          safesearch=safesearch,
 186          timelimit=timelimit,
 187          backend=backend,
 188          max_results=max_results,
 189      )
 190      keywords = _sanitize_keywords(keywords)
 191      if output:
 192          _save_data(keywords, data, "text", filename=output)
 193      if download:
 194          _download_results(
 195              keywords,
 196              data,
 197              function_name="text",
 198              proxy=proxy,
 199              threads=threads,
 200              verify=verify,
 201              pathname=download_directory,
 202          )
 203      if not output and not download:
 204          _print_data(data)
 205  
 206  
 207  @cli.command()
 208  @click.option("-k", "--keywords", required=True, help="keywords for query")
 209  @click.option("-r", "--region", default="wt-wt", help="wt-wt, us-en, ru-ru, etc. -region https://duckduckgo.com/params")
 210  @click.option("-s", "--safesearch", default="moderate", type=click.Choice(["on", "moderate", "off"]))
 211  @click.option("-t", "--timelimit", type=click.Choice(["Day", "Week", "Month", "Year"]))
 212  @click.option("-size", "--size", type=click.Choice(["Small", "Medium", "Large", "Wallpaper"]))
 213  @click.option(
 214      "-c",
 215      "--color",
 216      type=click.Choice(
 217          [
 218              "color",
 219              "Monochrome",
 220              "Red",
 221              "Orange",
 222              "Yellow",
 223              "Green",
 224              "Blue",
 225              "Purple",
 226              "Pink",
 227              "Brown",
 228              "Black",
 229              "Gray",
 230              "Teal",
 231              "White",
 232          ]
 233      ),
 234  )
 235  @click.option("-type", "--type_image", type=click.Choice(["photo", "clipart", "gif", "transparent", "line"]))
 236  @click.option("-l", "--layout", type=click.Choice(["Square", "Tall", "Wide"]))
 237  @click.option(
 238      "-lic",
 239      "--license_image",
 240      type=click.Choice(["any", "Public", "Share", "ShareCommercially", "Modify", "ModifyCommercially"]),
 241  )
 242  @click.option("-m", "--max_results", type=int, help="maximum number of results")
 243  @click.option("-o", "--output", help="csv, json or filename.csv|json (save the results to a csv or json file)")
 244  @click.option("-d", "--download", is_flag=True, default=False, help="download results. -dd to set custom directory")
 245  @click.option("-dd", "--download-directory", help="Specify custom download directory")
 246  @click.option("-th", "--threads", default=10, help="download threads, default=10")
 247  @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150")
 248  @click.option("-v", "--verify", default=True, help="verify SSL when making the request")
 249  def images(
 250      keywords: str,
 251      region: str,
 252      safesearch: str,
 253      timelimit: str | None,
 254      size: str | None,
 255      color: str | None,
 256      type_image: str | None,
 257      layout: str | None,
 258      license_image: str | None,
 259      download: bool,
 260      download_directory: str | None,
 261      threads: int,
 262      max_results: int | None,
 263      output: str | None,
 264      proxy: str | None,
 265      verify: bool,
 266  ) -> None:
 267      """CLI function to perform a images search using DuckDuckGo API."""
 268      data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).images(
 269          keywords=keywords,
 270          region=region,
 271          safesearch=safesearch,
 272          timelimit=timelimit,
 273          size=size,
 274          color=color,
 275          type_image=type_image,
 276          layout=layout,
 277          license_image=license_image,
 278          max_results=max_results,
 279      )
 280      keywords = _sanitize_keywords(keywords)
 281      if output:
 282          _save_data(keywords, data, function_name="images", filename=output)
 283      if download:
 284          _download_results(
 285              keywords,
 286              data,
 287              function_name="images",
 288              proxy=proxy,
 289              threads=threads,
 290              verify=verify,
 291              pathname=download_directory,
 292          )
 293      if not output and not download:
 294          _print_data(data)
 295  
 296  
 297  @cli.command()
 298  @click.option("-k", "--keywords", required=True, help="keywords for query")
 299  @click.option("-r", "--region", default="wt-wt", help="wt-wt, us-en, ru-ru, etc. -region https://duckduckgo.com/params")
 300  @click.option("-s", "--safesearch", default="moderate", type=click.Choice(["on", "moderate", "off"]))
 301  @click.option("-t", "--timelimit", type=click.Choice(["d", "w", "m"]), help="day, week, month")
 302  @click.option("-res", "--resolution", type=click.Choice(["high", "standart"]))
 303  @click.option("-d", "--duration", type=click.Choice(["short", "medium", "long"]))
 304  @click.option("-lic", "--license_videos", type=click.Choice(["creativeCommon", "youtube"]))
 305  @click.option("-m", "--max_results", type=int, help="maximum number of results")
 306  @click.option("-o", "--output", help="csv, json or filename.csv|json (save the results to a csv or json file)")
 307  @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150")
 308  @click.option("-v", "--verify", default=True, help="verify SSL when making the request")
 309  def videos(
 310      keywords: str,
 311      region: str,
 312      safesearch: str,
 313      timelimit: str | None,
 314      resolution: str | None,
 315      duration: str | None,
 316      license_videos: str | None,
 317      max_results: int | None,
 318      output: str | None,
 319      proxy: str | None,
 320      verify: bool,
 321  ) -> None:
 322      """CLI function to perform a videos search using DuckDuckGo API."""
 323      data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).videos(
 324          keywords=keywords,
 325          region=region,
 326          safesearch=safesearch,
 327          timelimit=timelimit,
 328          resolution=resolution,
 329          duration=duration,
 330          license_videos=license_videos,
 331          max_results=max_results,
 332      )
 333      keywords = _sanitize_keywords(keywords)
 334      if output:
 335          _save_data(keywords, data, function_name="videos", filename=output)
 336      else:
 337          _print_data(data)
 338  
 339  
 340  @cli.command()
 341  @click.option("-k", "--keywords", required=True, help="keywords for query")
 342  @click.option("-r", "--region", default="wt-wt", help="wt-wt, us-en, ru-ru, etc. -region https://duckduckgo.com/params")
 343  @click.option("-s", "--safesearch", default="moderate", type=click.Choice(["on", "moderate", "off"]))
 344  @click.option("-t", "--timelimit", type=click.Choice(["d", "w", "m", "y"]), help="day, week, month, year")
 345  @click.option("-m", "--max_results", type=int, help="maximum number of results")
 346  @click.option("-o", "--output", help="csv, json or filename.csv|json (save the results to a csv or json file)")
 347  @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150")
 348  @click.option("-v", "--verify", default=True, help="verify SSL when making the request")
 349  def news(
 350      keywords: str,
 351      region: str,
 352      safesearch: str,
 353      timelimit: str | None,
 354      max_results: int | None,
 355      output: str | None,
 356      proxy: str | None,
 357      verify: bool,
 358  ) -> None:
 359      """CLI function to perform a news search using DuckDuckGo API."""
 360      data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).news(
 361          keywords=keywords, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results
 362      )
 363      keywords = _sanitize_keywords(keywords)
 364      if output:
 365          _save_data(keywords, data, function_name="news", filename=output)
 366      else:
 367          _print_data(data)
 368  
 369  
 370  if __name__ == "__main__":
 371      cli(prog_name="ddgs")
 372