lib/spotify/search.ex

defmodule Spotify.Search do
  @moduledoc """
  Spotify search endpoints. Spotify allows querying for artists, albums, playlists, and tracks.
  """

  use Spotify.Responder
  import Spotify.Helpers

  alias Spotify.{
    Album,
    Artist,
    Client,
    Paging,
    Playlist,
    Track
  }

  @keys ~w[albums artists playlists tracks]

  @doc """
  Search for a playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/search-item/)

  **Method**: `GET`

  **Required Params:** `q`, `type`

  **Optional Params:** `limit`, `offset`, `market`

      Spotify.Search.query(conn, q: "foo", type: "playlist")
      # => {:ok, %{ items: [%Spotify.Playlist{..} ...]}}
  """
  def query(conn, params) do
    url = query_url(params)
    conn |> Client.get(url) |> handle_response
  end

  @doc """
  Search for a playlist, artist, album, or track.
      iex> Spotify.Search.query_url(q: "foo", type: "playlist")
      "https://api.spotify.com/v1/search?q=foo&type=playlist"
  """
  def query_url(params) do
    "https://api.spotify.com/v1/search" <> query_string(params)
  end

  @doc """
  Implements the hook required by the Responder behaviour
  """
  def build_response(body) do
    body
    |> map_paging
    |> append_items
  end

  defp map_paging(body), do: {Paging.response(body, []), body}

  defp append_items({paging, body}) do
    body
    |> Map.take(@keys)
    |> Map.to_list()
    |> Enum.flat_map_reduce([], &reducer/2)
    |> update_paging(paging)
  end

  defp reducer({key, data}, acc), do: {map_to_struct(key, data["items"]), acc}
  defp update_paging({items, _rest}, paging), do: paging |> Map.put(:items, items)

  defp map_to_struct("artists", artists), do: Artist.build_artists(artists)
  defp map_to_struct("tracks", tracks), do: Track.build_tracks(tracks)
  defp map_to_struct("playlists", playlists), do: Playlist.build_playlists(playlists)
  defp map_to_struct("albums", albums), do: Enum.map(albums, &to_struct(Album, &1))
end