defmodule Nola.Icecast do use GenServer require Logger @hackney_pool :default @httpoison_opts [hackney: [pool: @hackney_pool]] @fuse __MODULE__ def start_link, do: GenServer.start_link(__MODULE__, [], []) def init(_) do GenServer.cast(self(), :poll) {:ok, nil} end def handle_cast(:poll, state) do state = poll(state) {:noreply, state} end def handle_info(:poll, state) do state = poll(state) {:noreply, state} end defp poll(state) do state = case request(base_url(), :get) do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> #update_json_stats(Jason.decode(body)) stats = update_stats(body) if state != stats do Logger.info "Icecast Update: " <> inspect(stats) Nola.IcecastAgent.update(stats) Registry.dispatch(Nola.BroadcastRegistry, "icecast", fn ws -> for {pid, _} <- ws, do: send(pid, {:icecast, stats}) end) stats else state end error -> Logger.error "Icecast HTTP Error: #{inspect error}" state end interval = Application.get_env(:nola, :icecast_poll_interval, 60_000) :timer.send_after(interval, :poll) state end defp update_stats(html) do raw = Floki.find(html, "div.roundbox") |> Enum.map(fn(html) -> html = Floki.raw_html(html) [{"h3", _, ["Mount Point /"<>mount]}] = Floki.find(html, "h3.mount") stats = Floki.find(html, "tr") |> Enum.map(fn({"tr", _, tds}) -> [{"td", _, keys}, {"td", _, values}] = tds key = List.first(keys) value = List.first(values) {key, value} end) |> Enum.into(Map.new) {mount, stats} end) |> Enum.into(Map.new) live? = if Map.get(raw["live"], "Content Type:", false), do: true, else: false np = if live? do raw["live"]["Currently playing:"] else raw["autodj"]["Currently playing:"] end genre = raw["live"]["Genre:"] || nil %{np: np || "", live: live? || false, genre: genre} end defp update_json_stats({:ok, body}) do Logger.debug "JSON STATS: #{inspect body}" end defp update_json_stats(error) do Logger.error "Failed to decode JSON Stats: #{inspect error}" end defp request(uri, method, body \\ [], headers \\ []) do headers = [{"user-agent", "Nola-API[115ans.net, sys.115ans.net] href@random.sh"}] ++ headers options = @httpoison_opts case :ok do #:fuse.ask(@fuse, :sync) do :ok -> run_request(method, uri, body, headers, options) :blown -> :blown end end # This is to work around hackney's behaviour of returning `{:error, :closed}` when a pool connection has been closed # (keep-alive expired). We just retry the request immediatly up to five times. defp run_request(method, uri, body, headers, options), do: run_request(method, uri, body, headers, options, 0) defp run_request(method, uri, body, headers, options, retries) when retries < 4 do case HTTPoison.request(method, uri, body, headers, options) do {:error, :closed} -> run_request(method, uri, body, headers, options, retries + 1) other -> other end end defp run_request(method, uri, body, headers, options, _exceeded_retries), do: {:error, :unavailable} # # -- URIs # defp stats_json_url do base_url() <> "/status-json.xsl" end defp base_url do "http://91.121.59.45:8089" end end