lib/spotify/playlist.ex

defmodule Spotify.Playlist do
  @moduledoc """
    Functions for retrieving information about a user’s playlists and for
    managing a user’s playlists.

    Some endpoints return collections. Spotify wraps the collection in a paging object,
    this API does the same. A single piece of data will not be wrapped.

    There are two functions for each endpoint, one that actually makes the request,
    and one that provides the endpoint:

        Spotify.Playist.create_playlist(conn, "foo", "bar") # makes the POST request.
        Spotify.Playist.create_playlist_url("foo", "bar") # provides the url for the request.


    https://developer.spotify.com/web-api/playlist-endpoints/
  """

  import Spotify.Helpers
  use Spotify.Responder

  alias Spotify.{
    Client,
    Paging
  }

  defstruct ~w[
    collaborative
    description
    external_urls
    followers
    href
    id
    images
    name
    owner
    public
    snapshot_id
    tracks
    type
    uri
  ]a

  @doc """
  Get a list of featured playlists.
  [Spotify Documenation](https://developer.spotify.com/web-api/get-list-featured-playlists/)

  **Valid params**: `locale`, `country`, `timestamp`, `limit`, `offset`

  **Method**: `GET`

      Spotify.Playlist.featured(country: "US")
      # => {:ok, %{ items: [%Spotify.Playlist{..} ...]}}
  """
  def featured(conn, params \\ []) do
    url = featured_url(params)
    conn |> Client.get(url) |> handle_response
  end

  @doc """
  Get a list of featured playlists.

      iex> Spotify.Playlist.featured_url(country: "US")
      "https://api.spotify.com/v1/browse/featured-playlists?country=US"
  """
  def featured_url(params \\ []) do
    "https://api.spotify.com/v1/browse/featured-playlists" <> query_string(params)
  end

  @doc """
  Get a category's playlists.
  [Spotify Documentation](https://developer.spotify.com/web-api/get-categorys-playlists/)

  **Valid params**: `country`, `limit`, `offset`

  **Method**: `GET`

  ## Example:
      Spotify.by_category(conn, "123")
      # => {:ok, %{ items: [%Spotify.Playlist{..} ...]}}
  """
  def by_category(conn, id, params \\ []) do
    url = by_category_url(id, params)
    conn |> Client.get(url) |> handle_response
  end

  @doc """
  Get a category's playlists.

      iex> Spotify.Playlist.by_category_url("123", [country: "US", limit: 5])
      "https://api.spotify.com/v1/browse/categories/123/playlists?country=US&limit=5"
  """
  def by_category_url(id, params \\ []) do
    "https://api.spotify.com/v1/browse/categories/#{id}/playlists" <> query_string(params)
  end

  @doc """
  Add the current user as a follower of a playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/follow-playlist/)

  **Optional Body Params**: `public`

  **Method**: `PUT`
      Spotify.Playlist.follow_playlist(conn, "123", "456")
      # => :ok
  """
  def follow_playlist(conn, owner_id, playlist_id, body \\ "") do
    url = follow_playlist_url(owner_id, playlist_id)
    conn |> Client.put(url, body) |> handle_response
  end

  @doc """
  Add the current user as a follower of a playlist.

      iex> Spotify.Playlist.follow_playlist_url("123", "456")
      "https://api.spotify.com/v1/users/123/playlists/456/followers"
  """
  def follow_playlist_url(owner_id, playlist_id) do
    get_playlist_url(owner_id, playlist_id) <> "/followers"
  end

  @doc """
  Remove the current user as a follower of a playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/unfollow-playlist/)

  **Method**: `DELETE`

      Spotify.Playlist.unfollow_playlist(conn, "123", "456")
      # => :ok
  """
  def unfollow_playlist(conn, owner_id, playlist_id) do
    url = unfollow_playlist_url(owner_id, playlist_id)
    conn |> Client.delete(url) |> handle_response
  end

  @doc """
  Remove the current user as a follower of a playlist.

      iex> Spotify.Playlist.unfollow_playlist_url("123", "456")
      "https://api.spotify.com/v1/users/123/playlists/456/followers"
  """
  def unfollow_playlist_url(owner_id, playlist_id) do
    follow_playlist_url(owner_id, playlist_id)
  end

  @doc """
  Get a list of the playlists owned or followed by a Spotify user.
  [Spotify Documentation](https://developer.spotify.com/web-api/get-list-users-playlists/)

  **Method**: `GET`

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

      Spotify.Playlist.get_users_playlists(conn, "123", q: "foo", limit: 5)
      # => {:ok, %{
      #     cursor: nil,
      #     href: ..,
      #     items: [%Spotify.Playlist{..} ...],
      #     limit: 20,
      #     next: nil,
      #     offset: 0,
      #     previous: nil,
      #     total: 20
      #   }
      # }
  """
  def get_users_playlists(conn, user_id, params \\ []) do
    url = get_users_playlists_url(user_id, params)
    conn |> Client.get(url) |> handle_response
  end

  @doc """
  Get a list of the playlists owned or followed by a Spotify user.

      iex> Spotify.Playlist.get_users_playlists_url("123", limit: 5)
      "https://api.spotify.com/v1/users/123/playlists?limit=5"
  """
  def get_users_playlists_url(user_id, params \\ []) do
    "https://api.spotify.com/v1/users/#{user_id}/playlists" <> query_string(params)
  end

  @doc """
  Get a list of the playlists owned or followed by the current Spotify user.
  [Spotify Documentation](https://developer.spotify.com/documentation/web-api/reference/playlists/get-a-list-of-current-users-playlists/)

  **Method**: `GET`

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

      Spotify.Playlist.get_users_playlists(conn, "123", q: "foo", limit: 5)
      # => {:ok, %{
      #     cursor: nil,
      #     href: ..,
      #     items: [%Spotify.Playlist{..} ...],
      #     limit: 20,
      #     next: nil,
      #     offset: 0,
      #     previous: nil,
      #     total: 20
      #   }
      # }
  """
  def get_current_user_playlists(conn, params \\ []) do
    url = get_current_user_playlists_url(params)
    conn |> Client.get(url) |> handle_response
  end

  @doc """
  Get a list of the playlists owned or followed by a Spotify user.

      iex> Spotify.Playlist.get_current_user_playlists_url(limit: 5)
      "https://api.spotify.com/v1/me/playlists?limit=5"
  """
  def get_current_user_playlists_url(params \\ []) do
    "https://api.spotify.com/v1/me/playlists" <> query_string(params)
  end

  @doc """
  Get a playlist owned by a Spotify user.
  [Spotify Documentation](https://developer.spotify.com/web-api/get-playlist/)

  **Method**: `GET`

  **Optional Params `fields, market`

      Spotify.Playlist.get_playlist(conn, "123", "456")
      # => {:ok, %Spotify.Playlist{..}}
  """
  def get_playlist(conn, user_id, playlist_id, params \\ []) do
    url = get_playlist_url(user_id, playlist_id, params)
    conn |> Client.get(url) |> handle_response
  end

  @doc """
  Get a playlist owned by a Spotify user.

      iex> Spotify.Playlist.get_playlist_url("123", "456", market: "foo")
      "https://api.spotify.com/v1/users/123/playlists/456?market=foo"
  """
  def get_playlist_url(user_id, playlist_id, params \\ []) do
    "https://api.spotify.com/v1/users/#{user_id}/playlists/#{playlist_id}" <> query_string(params)
  end

  @doc """
  Get full details of the tracks of a playlist owned by a Spotify user.
  [Spotify Documentation](https://developer.spotify.com/web-api/get-playlists-tracks/)

  **Method**: `GET`

  **Optional Params `fields`, `market`, `limit`, `offset`
      Spotify.Playlist.get_playlist_tracks(conn, "123", "456")
      # => {:ok, %Spotify.Playlist{..}}
  """
  def get_playlist_tracks(conn, user_id, playlist_id, params \\ []) do
    alias Spotify.Playlist.Track, as: Track

    url = get_playlist_tracks_url(user_id, playlist_id, params)
    conn |> Client.get(url) |> Track.handle_response()
  end

  @doc """
  Get full details of the tracks of a playlist owned by a Spotify user.

      iex> Spotify.Playlist.get_playlist_tracks_url("123", "456", limit: 5, offset: 5)
      "https://api.spotify.com/v1/users/123/playlists/456/tracks?limit=5&offset=5"
  """
  def get_playlist_tracks_url(user_id, playlist_id, params \\ []) do
    playlist_tracks_url(user_id, playlist_id) <> query_string(params)
  end

  @doc false
  def playlist_tracks_url(user_id, playlist_id) do
    get_playlist_url(user_id, playlist_id) <> "/tracks"
  end

  @doc """
  Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.)
  [Spotify Documentation](https://developer.spotify.com/web-api/create-playlist/)

  **Method**: `POST`

  **Body Params**: `name`, `public`

      body = "\{\"name\": \"foo\"}"
      Spotify.Playlist.create_playlist(conn, "123", body)
      # => %Spotify.Playlist{..}
  """
  def create_playlist(conn, user_id, body) do
    url = create_playlist_url(user_id)
    conn |> Client.post(url, body) |> handle_response
  end

  @doc """
  Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.)

      iex> Spotify.Playlist.create_playlist_url("123")
      "https://api.spotify.com/v1/users/123/playlists"
  """
  def create_playlist_url(user_id) do
    "https://api.spotify.com/v1/users/#{user_id}/playlists"
  end

  @doc """
  Change a playlist’s name and public/private state. (The user must, of course, own the playlist.)
  [Spotify Documentation](https://developer.spotify.com/web-api/change-playlist-details/)

  **Method**: `PUT`

  **Request Data)**: `name`, `public`

      body = "{ \"name\": \"foo\", \"public\": true }"
      Spotify.Playlist.change_playlist(conn, "123", "456", body)
      # => :ok
  """
  def change_playlist(conn, user_id, playlist_id, body) do
    url = change_playlist_url(user_id, playlist_id)
    conn |> Client.put(url, body) |> handle_response
  end

  @doc """
  Change a playlist’s name and public/private state. (The user must, of course, own the playlist.)

      iex> Spotify.Playlist.change_playlist_url("123", "456")
      "https://api.spotify.com/v1/users/123/playlists/456"
  """
  def change_playlist_url(user_id, playlist_id) do
    get_playlist_url(user_id, playlist_id)
  end

  @doc """
  Add one or more tracks to a user’s playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/add-tracks-to-playlist/)

  **Method**: `POST`

  **Optional Params**: `uris`, `position`

  You can also pass the URI param in the request body. If you don't want to
  pass params, use an empty list. See Spotify docs and add_tracks/5.

      body = Poison.encode!(%{ uris: [ "spotify:track:755MBpLqJqCO87PkoyBBQC", "spotify:track:1hsWu8gT2We6OzjhYGAged" ]})

      Spotify.Playlist.add_tracks("123", "456", body, [])
      # => {:ok, %{"snapshot_id" => "foo"}}
  """
  def add_tracks(conn, user_id, playlist_id, body, params) do
    url = add_tracks_url(user_id, playlist_id, params)
    conn |> Client.post(url, body) |> handle_response
  end

  @doc """
  Add one or more tracks to a user’s playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/add-tracks-to-playlist/)

  **Method**: `POST`

  **Optional Params**: `uris`, `position`

  You can also pass the URI param in the request body. See Spotify docs and add_tracks/4.
      Spotify.Playlist.add_tracks("123", "456", uris: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh")
      # => {:ok, %{"snapshot_id" => "foo"}}
  """
  def add_tracks(conn, user_id, playlist_id, params) do
    url = add_tracks_url(user_id, playlist_id, params)
    conn |> Client.post(url) |> handle_response
  end

  @doc """
  Add one or more tracks to a user’s playlist.

      iex> Spotify.Playlist.add_tracks_url("123", "456", uris: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh")
      "https://api.spotify.com/v1/users/123/playlists/456/tracks?uris=spotify%3Atrack%3A4iV5W9uYEdYUVa79Axb7Rh"
  """
  def add_tracks_url(user_id, playlist_id, params \\ []) do
    playlist_tracks_url(user_id, playlist_id) <> query_string(params)
  end

  @doc """
  Remove one or more tracks from a user’s playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/remove-tracks-playlist/)

  **Method**: `DELETE`

  **Request Data**: `tracks`

      Spotify.Playlist.remove_tracks(conn, "123", "456")
      # => {:ok, %{"snapshot_id": "foo"}}
  """
  def remove_tracks(conn, user_id, playlist_id) do
    url = playlist_tracks_url(user_id, playlist_id)
    conn |> Client.delete(url) |> handle_response
  end

  @doc """
  Remove one or more tracks from a user’s playlist.

      iex> Spotify.Playlist.remove_tracks_url("123", "456")
      "https://api.spotify.com/v1/users/123/playlists/456/tracks"
  """
  def remove_tracks_url(user_id, playlist_id) do
    playlist_tracks_url(user_id, playlist_id)
  end

  @doc """
  Reorder a track or a group of tracks in a playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/reorder-playlists-tracks/)

  **Method**: `PUT`

  **Required Request Body Data**: `range_start`, `insert_before`

  **Optional Request Body Data**: `range_length`, `snapshot_id`

      body = { \"range_start\": \"...\" }
      Spotify.Playlist.change_playlist(conn, "123", "456", body)
      # => {:ok, %{"snapshot_id" => "klq34klj..."} }
  """
  def reorder_tracks(conn, user_id, playlist_id, body) do
    url = playlist_tracks_url(user_id, playlist_id)
    conn |> Client.put(url, body) |> handle_response
  end

  @doc """
  Reorder a track or a group of tracks in a playlist.

      iex> Spotify.Playlist.reorder_tracks_url("123", "456")
      "https://api.spotify.com/v1/users/123/playlists/456/tracks"
  """
  def reorder_tracks_url(user_id, playlist_id) do
    playlist_tracks_url(user_id, playlist_id)
  end

  @doc """
  Replace all the tracks in a playlist, overwriting its existing tracks. This
  powerful request can be useful for replacing tracks, re-ordering existing
  tracks, or clearing the playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/replace-playlists-tracks/)

  **Method**: `PUT`

  **Optional Query Params**: `uris`

  You can also pass the URI param in the request body. Use `replace_tracks/2`. See Spotify docs.
      Spotify.Playlist.replace_tracks(conn, "123", "456", uris: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh,spotify:track:adkjaklsd94h")
      :ok
  """
  def replace_tracks(conn, user_id, playlist_id, params) when is_list(params) do
    url = replace_tracks_url(user_id, playlist_id, params)
    conn |> Client.put(url) |> handle_response
  end

  def replace_tracks(conn, user_id, playlist_id, body) when is_binary(body) do
    url = replace_tracks_url(user_id, playlist_id)
    conn |> Client.put(url, body) |> handle_response
  end

  @doc """
  Replace all the tracks in a playlist, overwriting its existing tracks. This

      iex> Spotify.Playlist.replace_tracks_url("123", "456", uris: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh,spotify:track:adkjaklsd94h")
      "https://api.spotify.com/v1/users/123/playlists/456/tracks?uris=spotify%3Atrack%3A4iV5W9uYEdYUVa79Axb7Rh%2Cspotify%3Atrack%3Aadkjaklsd94h"
  """
  def replace_tracks_url(user_id, playlist_id, params \\ []) do
    playlist_tracks_url(user_id, playlist_id) <> query_string(params)
  end

  @doc """
  Check to see if one or more Spotify users are following a specified playlist.
  [Spotify Documentation](https://developer.spotify.com/web-api/check-user-following-playlist/)

  **Method**: `GET`

  **Query Params: `ids`

      Spotify.Playlist.check_followers(conn, "123", "456", ids: "foo,bar")
      # => {:ok, boolean}
  """
  def check_followers(conn, owner_id, playlist_id, params) do
    url = check_followers_url(owner_id, playlist_id, params)
    conn |> Client.get(url) |> handle_response
  end

  @doc """
  Check to see if one or more Spotify users are following a specified playlist.

      iex> Spotify.Playlist.check_followers_url("123", "456", ids: "foo,bar")
      "https://api.spotify.com/v1/users/123/playlists/456/followers/contains?ids=foo%2Cbar"
  """
  def check_followers_url(owner_id, playlist_id, params) do
    "https://api.spotify.com/v1/users/#{owner_id}/playlists/#{playlist_id}/followers/contains" <>
      query_string(params)
  end

  @doc """
  Implements the hook expected by the Responder behaviour
  """
  def build_response(body) do
    case body do
      %{"items" => _items} = response -> build_paged_response(response)
      %{"playlists" => playlists} -> build_paged_response(playlists)
      _ -> to_struct(__MODULE__, body)
    end
  end

  @doc false
  defp build_paged_response(response) do
    %Paging{
      href: response["href"],
      items: build_playlists(response["items"]),
      limit: response["limit"],
      next: response["next"],
      offset: response["offset"],
      previous: response["previous"],
      total: response["total"]
    }
  end

  @doc false
  def build_playlists(playlists) do
    Enum.map(playlists, &to_struct(__MODULE__, &1))
  end
end