defmodule SerpCheap do @moduledoc """ Elixir client for the [serp.cheap](https://serp.cheap) Google Search API. Configure it via application env (the Phoenix way): config :serpcheap, api_key: System.get_env("SERPCHEAP_API_KEY") Then call from anywhere — options can also be overridden per call: {:ok, results} = SerpCheap.search("best running shoes", gl: "us") """ alias SerpCheap.Client @type opts :: keyword() @type response :: {:ok, map()} | {:error, SerpCheap.Error.t()} @doc "Run a Google search. Retries transient errors (429/503/timeout) with backoff." @spec search(String.t(), opts()) :: response() def search(query, opts \\ []) when is_binary(query) do payload = %{"q" => query, "gl" => Keyword.get(opts, :gl, "us"), "page" => Keyword.get(opts, :page, 1)} |> put(opts, :hl, "hl") |> put(opts, :tbs, "tbs") Client.request("/v1/search", payload, opts, &is_list(&1["organic"])) end @doc "Fetch and extract a single page (content + optional screenshot)." @spec scrape(String.t(), opts()) :: response() def scrape(url, opts \\ []) when is_binary(url) do payload = %{"url" => url} |> put(opts, :render_js, "render_js") |> put(opts, :screenshot, "screenshot") |> put(opts, :wait_for, "wait_for") |> put(opts, :wait_ms, "wait_ms") Client.request("/v1/scrape", payload, opts, &is_binary(&1["url"])) end @doc "Find where a url/domain ranks for a keyword across Google result pages." @spec rank(String.t(), String.t(), opts()) :: response() def rank(url, query, opts \\ []) when is_binary(url) and is_binary(query) do payload = %{ "url" => url, "q" => query, "gl" => Keyword.get(opts, :gl, "us"), "pages" => Keyword.get(opts, :pages, 1), "match_type" => Keyword.get(opts, :match_type, "domain") } |> put(opts, :hl, "hl") |> put(opts, :tbs, "tbs") Client.request( "/v1/rank", payload, opts, &(is_list(&1["organic"]) and is_list(&1["matches"])) ) end @doc "Like `search/2` but returns the result or raises `SerpCheap.Error`." @spec search!(String.t(), opts()) :: map() def search!(query, opts \\ []), do: unwrap(search(query, opts)) @doc "Like `scrape/2` but returns the result or raises `SerpCheap.Error`." @spec scrape!(String.t(), opts()) :: map() def scrape!(url, opts \\ []), do: unwrap(scrape(url, opts)) @doc "Like `rank/3` but returns the result or raises `SerpCheap.Error`." @spec rank!(String.t(), String.t(), opts()) :: map() def rank!(url, query, opts \\ []), do: unwrap(rank(url, query, opts)) defp unwrap({:ok, value}), do: value defp unwrap({:error, error}), do: raise(error) defp put(payload, opts, key, field) do case Keyword.get(opts, key) do nil -> payload value -> Map.put(payload, field, value) end end end