Adding a Spotify "Now Playing" widget to your Jekyll site

10 June 2020

I recently added a widget to my homepage that shares what I’m currently playing on Spotify. It looks like this:

I’m currently listening to Hearts On Fire - Cut Copy.

[The above is just a static example. But if I am listening to something right now, you can see it on my homepage.]

Adding this simple line was more complicated than I had first anticipated, given that this site is made using Jekyll - a static site generator. Also, I didn’t want to use any client-side code.

1. Getting your Spotify credentials

To interact with the Spotify API you will need to get yourself a client_id and client_secret.

First, create a new application through the developer dashboard. You’ll have to log in with your Spotify account to get here. Then click “My New App” and fill in the details.

Then click through to your application to get them:

2. Authorising yourself

The next step is to go through a one-time OAuth dance. This will give you both an access_token, lasting one hour, and a refresh_token, which will let you continously update your access_token once it expires.

There are 3 different flows for authorising yourself with Spotify, depending on your needs.

Long-story short for this widget: because you are accessing a specific user’s data (i.e. yours) you need to go through the Authorization Code Flow -- unfortunately, the longest one:

From Spotify’s docs. It’s a... complicated dance.

To kick things off you need to construct an authorisation URL to visit in your browser. This will let you log in and tell Spotify that you’re giving this newly-created application the thumbs up to access your user data. 👍

The endpoint you need for this widget is documented here. You need to include the user-read-currently-playing scope when constructing the URL.

In an irb console:

require 'faraday'

response = Faraday.get "https://accounts.spotify.com/authorize", {
  client_id: "YOUR_CLIENT_ID", # replace
  scope: "user-read-currently-playing",
  response_type: "code",
  redirect_uri: "https://edforshaw.co.uk/callback" # replace - see below
}

puts response.env.url # Our authorisation URL

# https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fedforshaw.co.uk%2Fcallback&response_type=code&scope=user-read-currently-playing

Important -- Whilst the value of redirect_uri doesn’t matter in terms of functionality, be aware that you will be redirected to it with sensitive details appended. So ideally you should own the domain to avoid it appearing in someone else’s logs.

You must also add the exact same redirect_uri value to your application’s settings:

Spotify settings

If you now grab the authorisation URL and visit it in your browser, you are asked to log in to Spotify (if you aren’t already) and agree.

Spotify accept page

Then the redirect URI kicks in and takes you to https://edforshaw.co.uk/callback?code=VERY_LONG_CODE.

Copy this VERY_LONG_CODE from the URL. This is known as the authorization code.

Note -- this is not the access_token or refresh_token. You now need to request those using this code. The dance goes on... 💃

Back in irb again:

response = Faraday.post "https://accounts.spotify.com/api/token", {
  grant_type: "authorization_code",
  code: 'VERY_LONG_CODE', # replace
  client_id: 'YOUR_CLIENT_ID', # replace
  client_secret: 'YOUR_CLIENT_SECRET', # replace
  # no actual redirection happens, but must match app's setting
  redirect_uri: "https://edforshaw.co.uk/callback"
}


require 'json'

access_token = JSON.parse(response.body)["access_token"]
# => LONG_ACCESS_TOKEN

refresh_token = JSON.parse(response.body)["refresh_token"]
# => LONG_REFRESH_TOKEN

You finally have your access_token and refresh_token.

Thankfully that is a one-time manual job, and the fun can now begin.

3. Getting your current playing song

Now fire up a song on Spotify and hit the currently playing endpoint:

endpoint = 'https://api.spotify.com/v1/me/player/currently-playing'

response = Faraday.get endpoint, {}, {
  'Authorization' => "Bearer #{access_token}"
}

data = JSON.parse(response.body)

artist = data['item']['artists'].first['name']
# => "David Bowie"

title = data['item']['name']
# => "Moonage Daydream - 2012 Remaster"

After parsing the response data you now have your current playing track name and artist. 😎


The remaining steps are specific to my Jekyll site.

4. Adding the widget

On my homepage I use a Jekyll include for this widget, which lives in _includes/now_playing.html:

...

{% include now_playing.html %}

pages/index.md

This include is empty initially - but I have a separate template with liquid tags, which I will use to render content for it:

<a class="icon-link" href="https://open.spotify.com/track/{{ track_id }}"><i class="fab fa-spotify pulsing"></i></a> I'm <a href="/spotify-now-playing">currently listening</a> to <strong>{{ title }}</strong> - <strong>{{ artist }}</strong>.

_includes/now_playing.html.template

On my blog’s server I have the following process running in the background:

JEKYLL_ENV=production jekyll build --watch --incremental > /dev/null 2>&1 &

This process watches for updates to my Jekyll project - meaning that every time I update _includes/now_playing.html, any page that use this include will be rebuilt (and only these pages, thanks to the --incremental flag).

Now all that remains is for me to update _includes/now_playing.html whenever I start or stop playing a song, and everything should Just Work.

5. Updating the widget

In a perfect world, every time I hit play/pause on Spotify it would trigger a webhook to my server, allowing me to update my widget only when I need to.

Sadly, webhooks for player events are not yet supported by the API [although they are apparently high priority].

Therefore, I have to poll for changes at a suitable time interval. I decided on 30 seconds.

Firstly, I tidied up the earlier code into a single Ruby file, with some additions:

require 'faraday'
require 'json'
require 'yaml'
require 'base64'
require 'liquid'

class NowPlayingUpdate
  CONFIG_PATH = "#{Dir.home}/.spotify_nowplaying.yml"
  AUTH_ENDPOINT = 'https://accounts.spotify.com/api/token'
  CURRENTLY_PLAYING_ENDPOINT =
    'https://api.spotify.com/v1/me/player/currently-playing'
  TRACK_PLAYING_TYPE = 'track'

  attr_reader :config, :client_id, :client_secret, :access_token,
    :refresh_token, :response

  def initialize
    @config = YAML.load_file(CONFIG_PATH)

    @client_id = config["client_id"]
    @client_secret = config["client_secret"]
    @access_token = config["access_token"]
    @refresh_token = config["refresh_token"]
  end

  def run
    @response = currently_playing_response

    if unauthorized_response?
      refresh_access_token
      run # try again
    elsif track_playing?
      update_currently_playing_track
    else
      update_no_track_playing
    end
  end

  private

  def unauthorized_response?
    response.status == 401
  end

  def update_currently_playing_track
    JekyllNowPlayingIncludeUpdate.new.run(
      title: current_title, artist: current_artist, track_id: current_track_id
    )
  end

  def update_no_track_playing
    JekyllNowPlayingIncludeUpdate.new.clear
  end

  def currently_playing_data
    JSON.parse(response.body)
  end

  def track_playing?
    !response.body.empty? &&
      currently_playing_data["is_playing"] && # false if paused
      currently_playing_data["currently_playing_type"] == TRACK_PLAYING_TYPE
  end

  # allow multiple artists to be combined
  def current_artist
    currently_playing_data["item"]["artists"].
      collect { |artist| artist["name"] }.join(" & ")
  end

  # throw away sub-strings containing 'Remastered'
  def current_title
    currently_playing_data["item"]["name"].split(" - ").reject do |sub_str|
      sub_str.include?("Remaster")
    end.join(" - ")
  end

  def current_track_id
    currently_playing_data["item"]["id"]
  end

  def refresh_access_token
    @access_token = JSON.parse(refresh_token_response.body)["access_token"]
    persist_access_token
  end

  def currently_playing_response
    Faraday.get CURRENTLY_PLAYING_ENDPOINT, {}, {
      'Authorization' => "Bearer #{access_token}"
    }
  end

  def refresh_token_response
    Faraday.post AUTH_ENDPOINT, {
      grant_type: "refresh_token",
      refresh_token: refresh_token
    }, {
      'Authorization' => "Basic #{bearer}"
    }
  end

  def persist_access_token
    @config["access_token"] = access_token
    write_config
  end

  def write_config
    File.open(CONFIG_PATH, 'w') { |file| file.write config.to_yaml }
  end

  def bearer
    Base64.strict_encode64("#{client_id}:#{client_secret}")
  end
end

class JekyllNowPlayingIncludeUpdate
  LIQUID_TEMPLATE_PATH = '../_includes/now_playing.html.template'
  INCLUDE_PATH = '../_includes/now_playing.html'

  def clear
    File.open(path_to(INCLUDE_PATH), 'w') { |file| file.write }
  end

  def run(title:, artist:, track_id:)
    File.open(path_to(INCLUDE_PATH), 'w') do |file|
      file.write liquid_template.render(
        'title' => title, 'artist' => artist, 'track_id' => track_id)
    end
  end

  private

  def liquid_template
    Liquid::Template.parse File.read(path_to(LIQUID_TEMPLATE_PATH))
  end

  def path_to(path)
    File.join(File.dirname(__FILE__), path)
  end
end

NowPlayingUpdate.new.run

tasks/spotify_nowplaying.rb

A few notes about this:

---
client_id: MY_CLIENT_ID
client_secret: MY_CLIENT_SECRET
access_token: MY_ACCESS_TOKEN
refresh_token: MY_REFRESH_TOKEN

~/.spotify_nowplaying.yml

And finally, on the server we add a cron task to run this ruby file every 30 seconds:

# crontab -e
* * * * * ruby /PATH_TO_PROJECT/tasks/spotify_nowplaying.rb
* * * * * sleep 30; ruby /PATH_TO_PROJECT/tasks/spotify_nowplaying.rb

Cron’s minimum unit is one minute, hence the sleep 30 workaround.

And we’re done. All that for one line. Totally worth it, of course. 😅

Possible future improvements: