Build a collaborative playlist over WhatsApp with Rails, Twilio, and Spotify

Phil Nash - Aug 28 '18 - - Dev Community

Sharing music over playlists is a great way to discover new and old music. Spotify has collaborative playlists, but I don't like how they let your friends re-order and delete songs from the list. We can fix this by building our own collaborative playlist that only allows additions using the Spotify Web API. With the Twilio API for WhatsApp we can let our friends send in a song whenever they are struck by inspiration.

In this post we are going to build a WhatsApp bot that can do all of the above using Ruby on Rails.

Getting started

To build this application we will need a few things:

Spotify API Credentials

To work with the Spotify API we will need to generate some API credentials. From the Spotify developer dashboard create a new application and follow the 3 step process.

  1. First, fill in some details about the app. Name it, I called mine WhatsPlaying, provide a short description and check what app type it is, I chose "website"
  2. Next we are asked if this is a commercial integration; choose "no"
  3. Finally, check the agreements

Once these steps are complete we will see the application dashboard. We will need the Client ID and Client Secret from this page.

There's one more thing we need to do here. Click on "edit settings", enter the redirect URL http://localhost:3000/auth/spotify/callback and click "add".

Now that credentials are sorted, we can start to build our app.

Preparing the project

I've started the project off, so clone or download the repo from GitHub and checkout the getting-started branch.

git clone -b getting-started https://github.com/philnash/whats_playing.git
Enter fullscreen mode Exit fullscreen mode

Change into the directory and install the project's dependencies.

cd whats_playing
bundle install
Enter fullscreen mode Exit fullscreen mode

Copy the config/env.yml.example file to config/env.yml (we're using envyable to manage environment variables for the application). Open config/env.yml and fill in the Spotify Client ID and Client Secret we just generated. We'll fill in the rest soon.

We're now going to generate a Spotify access token for your account on Spotify as well as a new playlist, called "WhatsPlaying", that we are going to use as our collaborative playlist. Run the Rails app with:

bundle exec rails server
Enter fullscreen mode Exit fullscreen mode

Open the app at http://localhost:3000. There will be a link to "Sign in with Spotify". Click the link and follow the OAuth process. All of this is handled by the RSpotify gem and omniauth, you can check out the config required to make this work in config/initializers/rspotify.rb.

Once you have been directed back to the application you will see your Spotify user ID, access token, refresh token, and playlist ID on the page in front of you. Copy these into config/env.yml, restart the application, and open http://localhost:3000 again. You will find the playlist that the script just created empty and embedded in the page. Now it's time to build the integration between WhatsApp, Twilio, and Spotify.

Getting to grip with webhooks

When you send a message to a WhatsApp number that you control with Twilio, Twilio will send an HTTP request, or webhook, to an application URL we provide. We will use this to respond to incoming messages and add songs to our playlist. Let's set up our webhook endpoint first.

To do so we will install the twilio-ruby gem. This will make writing TwiML, the XML that Twilio understands and takes direction from, easier. Open up the Gemfile and under the line that includes RSpotify, add twilio-ruby:

gem 'rspotify', '~> 2.1.1'
gem 'twilio-ruby', '~> 5.12.3'
Enter fullscreen mode Exit fullscreen mode

Install the gem by running bundle install in the project directory again.

We now need a new controller to handle the incoming webhooks. Use the Rails generator to create a new one:

bundle exec rails generate controller twilio/messages
Enter fullscreen mode Exit fullscreen mode

Open up the newly generated app/controllers/twilio/messages_controller.rb and start by removing the default Rails CSRF protection. Webhooks are, by definition, a cross site request and we should secure them by validating that the request came from Twilio. We can do so later using the Twilio request validation Rack middleware provided by the twilio-ruby gem.

class Twilio::MessagesController < ApplicationController
  skip_before_action :verify_authenticity_token
end
Enter fullscreen mode Exit fullscreen mode

Next we need an action to handle the incoming webhook requests. In the context of Rails, I consider webhooks to be create actions, so make a new create method.

class Twilio::MessagesController < ApplicationController
  skip_before_action :verify_authenticity_token
  def create
  end
end
Enter fullscreen mode Exit fullscreen mode

To make sure we're on track, let's make this respond with a simple message first. We'll use the TwiML generation capability of the twilio-ruby gem to respond to any incoming message with the customary "Hello World!".

class Twilio::MessagesController < ApplicationController
  skip_before_action :verify_authenticity_token
  def create
    response = Twilio::TwiML::MessagingResponse.new
    response.message(body: "Hello World!")
    render xml: response.to_xml
  end
end
Enter fullscreen mode Exit fullscreen mode

This action needs a route too. Open config/routes.rb and add the following:

Rails.application.routes.draw do
  namespace :twilio do
    post 'messages', to: 'messages#create'
  end
  # other routes
end
Enter fullscreen mode Exit fullscreen mode

This is all we need to get a response, so let's hook up the Twilio WhatsApp sandbox to this application.

Start your app:

bundle exec rails server
Enter fullscreen mode Exit fullscreen mode

Start up an ngrok tunnel so that the Twilio webhook can reach your app with a public URL. In a new terminal window on the command line run:

ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

Take the URL ngrok generates, add the /twilio/messages path and enter it as the URL when "A MESSAGE COMES IN" for the WhatsApp channel.

Send a message to the WhatsApp sandbox number and you will get a response back saying "Hello World!". Now it's time to hook up the Spotify API.

Connecting with the Spotify API

We're going to build a conversational interface to interact with the Spotify API. We'll assume that when a user sends their first message it is a song title they want to add to the playlist. We'll use the Spotify API to search for that song and send the user back the first result to see if that's what they wanted. If they confirm with a positive response then we'll add the track to the playlist. If they say no we'll ask them to search again. If they send anything else back we can assume that is a new search.

We're using the excellent RSpotify library for this application, but let's create a class that is more suited to our use. Create a new directory in the app directory called services and a file in there called spotify.rb. Open spotify.rb and create a new class:

class Spotify
end
Enter fullscreen mode Exit fullscreen mode

In the initializer we will use the credentials we created earlier to create an authenticated user.

class Spotify
  def initialize(user_id, user_token, user_refresh_token)
    @user = RSpotify::User.new({
      'id' => user_id,
      'credentials' => {
        'token' => user_token,
        'refresh_token' => user_refresh_token
      }
    })
  end
end
Enter fullscreen mode Exit fullscreen mode

Searching for a track is performed by passing a query to the RSpotify::Track.search method and we only want to return the first result.

class Spotify
  def initialize(user_id, user_token, user_refresh_token)
  end

  def track_search(query)
    RSpotify::Track.search(query).first
  end
end
Enter fullscreen mode Exit fullscreen mode

We can add tracks to a playlist by getting the track ID from the result of track_search, searching for the playlist by ID, and then adding the track to the playlist.

class Spotify
  def initialize(user_id, user_token, user_refresh_token)
  end

  def track_search(query)
  end

  def add_to_playlist(playlist_id, track_id)
    playlist = RSpotify::Playlist.find(@user.id, playlist_id)
    playlist.add_tracks!([track_id])
  end
end
Enter fullscreen mode Exit fullscreen mode

Let's use this class in our controller now, open app/controllers/twilio/messages_controller.rb again. At the start of the create method, get the body of the message, which we'll use as the search term and instantiate a new Spotify object using the credentials in the environment.

Searching for songs

We'll start by testing the search functionality and returning the name and artist of the first track we find. Spotify returns the artists as a list as there could be more than one responsible for a track. We can turn that into a sentence with a bit of ActiveSupport trickery. WhatsApp also supports some basic message formatting; if we surround our track and artist names with underscores, they will appear in italics.

class Twilio::MessagesController < ApplicationController
  skip_before_action :verify_authenticity_token
  def create
    body = params['Body']
    spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])

    track = spotify.track_search(body)
    message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"

    response = Twilio::TwiML::MessagingResponse.new
    response.message(body: message)
    render xml: response.to_xml
  end
end
Enter fullscreen mode Exit fullscreen mode

Restart the application and send a message, searching for a track you want to hear. You should get a message back listing the track you wanted. If you get a different song, try adding the artist or album to narrow down the search.

We do need to to handle the case if we don't find any songs though.

  def create
    body = params['Body']
    spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])

    track = spotify.track_search(query)
    if track
      message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"
    else
      message = "I couldn't find any songs by searching for '#{body}'. Try something else."
    end

    response = Twilio::TwiML::MessagingResponse.new
    response.message(body: message)
    render xml: response.to_xml
  end
Enter fullscreen mode Exit fullscreen mode

Adding to the playlist

We can handle search requests. Now we need to be able to add the track if the user responds with a positive answer.

Like SMS conversations on Twilio, we can use the session to retain data between messages. For this application we can store the track URI, which identifies it to Spotify, in the session. Then, if we have a track in the session we can look for a "yes" in the response. If we get "yes", we add the track to the playlist and clear the session.

  def create
    body = params['Body']
    spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])

    if session[:track]
      answer = body.split(' ').first.downcase.strip
      if ['yes', 'yeah', 'yep', 'yup', '👍'].include? answer
        message = "OK, adding that track now."
        spotify.add_to_playlist(ENV['SPOTIFY_PLAYLIST_ID'], session[:track])
        session[:track] = nil
      end
    end

    if !message
      track = spotify.track_search(body)
      if track
        session[:track] = track.uri
        message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"
      else
        message = "I couldn't find any songs by searching for '#{body}'. Try something else."
      end
    end
    response = Twilio::TwiML::MessagingResponse.new
    response.message(body: message)
    render xml: response.to_xml
  end
Enter fullscreen mode Exit fullscreen mode

I added some other options for saying "yes" including the emoji thumbs up 👍. Who doesn't love emoji?

You can test this path through the application by sending a song to the WhatsApp number and following up with "yes". Then check that the song appears in the playlist.

Other responses

To complete our Spotify WhatsApp bot we need to handle the other potential responses. If the user sends back a negative response, then we want to ask what track they want to add instead. Any other response and we'll assume it's a new search.

  def create
    body = params['Body']
    spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])

    if session[:track]
      answer = body.split(' ').first.downcase.strip
      if ['yes', 'yeah', 'yep', 'yup', '👍'].include? answer
        message = "OK, adding that track now."
        spotify.add_to_playlist(ENV['SPOTIFY_PLAYLIST_ID'], session[:track])
        session[:track] = nil
      elsif ['no', 'nah', 'nope', '👎'].include? answer
        session[:track] = nil
        message = "What do you want to add?"
      end
    end

    if !message
      track = spotify.track_search(body)
      if track
        session[:track] = track.uri
        message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"
      else
        message = "I couldn't find any songs by searching for '#{body}'. Try something else."
      end
    end
    response = Twilio::TwiML::MessagingResponse.new
    response.message(body: message)
    render xml: response.to_xml
  end
Enter fullscreen mode Exit fullscreen mode

Restart the app one more time and start searching and confirming or denying tracks. You can see the songs you add appear on your playlist and in the embedded playlist on the homepage of your app too.

A bot and a brand new playlist

With the Spotify API and the Twilio API for WhatsApp we've just built a collaborative playlist. The interaction is quite simple right now, but you could build on this to include voting on songs to move them up in the playlist or ways to better share the playlist with more friends.

If you want to see all the code for this application, check out the repo on GitHub.

I thought it would be fun to see what you would add to the playlist, so I've deployed my version of this app. You can add songs to it by connecting to my WhatsApp sandbox by messaging " join beaver-wrasse" to this Twilio WhatsApp number: +14155238886. Then start sending me all your favourite songs! Check out the playlist online here!

Got any ideas for collaborative applications you can build with WhatsApp and Twilio? Let me know in the comments, on Twitter or by email at philnash@twilio.com.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .