Build­ing a Now Play­ing” Feature

Publisher: TJ Fogarty

Modified: 2018-03-13

As part of the rebuild of this site (which I’ll write more about when things have set­tled) I thought it would be fun to add a fea­ture that shows what music I’m cur­rent­ly lis­ten­ing to, or what I last lis­tened to. The peo­ple need to know.

I most­ly use Spo­ti­fy, and I thought they didn’t sup­port grab­bing the cur­rent­ly play­ing song from their API. Turns out they sup­port fetch­ing the cur­rent­ly play­ing, and recent­ly played tracks.

I assumed it wasn’t avail­able and jumped straight in with some­thing I had imple­ment­ed before by using Last​.fm. This works from the set­tings in Spo­ti­fy to con­nect it to your Last​.fm account. You can sign up for an API account if you want to go this route. Make sure to make a copy of the key you’re giv­en, as there’s no facil­i­ty avail­able to see it again once you leave that page.

Putting it all togeth­er #

Here’s the plan:

  • CSS cus­tom prop­er­ties are mag­ic, and I’d like to use them to learn more about them
  • JavaScript async/​await for the request
  • Not every­one will see this, or care about it. It should be opt-in to save on a request

CSS: More than style #

I’m using CSS cus­tom prop­er­ties to dis­play con­tent. As far as I’m aware it can be read by screen read­ers, but I’m hap­py to be cor­rect­ed on that.

The idea is to have a cus­tom prop­er­ty that has an ini­tial val­ue of Loading...:

:root {
    --current-track: 'Loading...';
}

Then when the user trig­gers the request, the prop­er­ty will be updat­ed with the track infor­ma­tion, which is dis­played like this:

...::after {
    content: var(--current-track);
}

So before the request has com­plet­ed, you’re see­ing Loading....

Here’s the CSS we need:

.c-now-playing {
  position: fixed;
  z-index: 10;
  bottom: 50px;
  right: 50px;
}

.c-now-playing__trigger {
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  border-radius: 50%;
  background-color: #fff;
  width: 45px;
  height: 45px;
  transition: background ease 0.3s;
}

.c-now-playing__trigger:focus {
  outline: none;
}

.c-now-playing__trigger:after {
  content: var(--current-track);
  background-color: #ff3b3f;
  color: #f3f3f3;
  font-size: .875rem;
  padding: 0.75rem;
  position: absolute;
  right: 0;
  pointer-events: none;
  border-radius: 2px;
  line-height: 1.4;
  bottom: 100%;
  width: 230px;
  opacity: 0;
  transform: translateY(-15px);
  transition: opacity ease 0.3s, transform ease 0.3s;
}

.c-now-playing__trigger:hover,
.c-now-playing__trigger:focus {
  background-color: #ff3b3f;
}

.c-now-playing__trigger:hover:after,
.c-now-playing__trigger:focus:after {
  transform: translateY(-5px);
  opacity: 1;
}

.c-now-playing__trigger:hover .c-now-playing__icon,
.c-now-playing__trigger:focus .c-now-playing__icon {
  color: #f3f3f3;
}

And the cor­re­spond­ing HTML:

<button type="button" class="c-now-playing__trigger js-lt-trigger">
    Now Playing
</button>

The JavaScript #

The code required for this to work is min­i­mal — we need a way to request the lat­est track, and update the cus­tom prop­er­ty with the result.

const endpoint =
  'https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=NAME&api_key=API_KEY&format=json&limit=1'

export const ListeningTo = {
  hoverTrigger: document.querySelector('.js-lt-trigger'),

  init() {
    this.updateRecentTrackVariable('Loading...')

    this.hoverTrigger.addEventListener(
      'mouseenter',
      this.fetchLatestTrack.bind(this),
      { once: true }
    )
  },

  async fetchLatestTrack() {
    try {
      let { recenttracks } = await fetch(endpoint).then(res => res.json())
      let { artist, name } = recenttracks.track[0]

      this.updateRecentTrackVariable(`Currently listening to: ${name} by ${artist['#text']}`)
    } catch (e) {
      this.updateRecentTrackVariable(`Error loading track: ${e}`)
    }
  },

  updateRecentTrackVariable(value) {
    document.documentElement.style.setProperty(
      '--current-track',
      `'${value}'`
    )
  }
}

// when imported, you can call ListeningTo.init() to start.

In the init func­tion, it only fires when the trig­ger is hov­ered, and it’ll only hap­pen once per page load. There’s no need to keep mak­ing the request after that.

Sin é (that’s it) #

You can see the result in the bot­tom right cor­ner of the site, in the future I’ll prob­a­bly refac­tor it to use the Spo­ti­fy API. In the mean­time, how­ev­er, it works just fine.