info-beamer

Spotify and Facebook event packages

Currently, we run a single infobeamer device in our bar/society setting to display various information. I created two new packages to display the current Spotify track playing and to display a list of our upcoming Facebook events. As our different days may have different branding and associated accounts and settings, these packages work with an arbitrary number of accounts. We mainly use the Scheduled Player package to set up everything.

As these might also be useful for other people, I wanted to share these. The packages are still rough around the edges and can change in the future.

Spotify package

The Spotify package currently only displays the current track playing. And has the option to have the background color determined by the cover image. And an additional ‘widget’ mode when using a small part of the screen. It’s now successfully running for about 2 weeks.

Import

Facebook package

The first version of this has been running for now about 2 years. Still have to be improved upon a lot (at some point) to make it work better and provide more customization.

Import

1 Like

That’s pretty awesome. Thanks for sharing. If you want, I can also add them to Digital Signage Solution Store - info-beamer.

Regarding the Spotify package: I wonder if there would be a way to make the OAuth flow work within the info-beamer dashboard somehow. Authorization Code With PKCE (see spotify doc) seems the only viable flow, as it doesn’t require keeping the client secret within the package’s source code and it allows access to the refresh token. The issue then of course is that you then need a redirect_uri to jump back to info-beamer (or precisely your currently edited setup). I’ll think about that one a bit. If would be great to have a single Connect to spotify button within the setup’s configuration. It’s already possible to securely embed custom iframes (See custom config value) for doing the custom button and logic. The only part missing is a way to retrieve the values passed in the OAuth redirect.

Quick update. There will soon be a mechanism to implement OAuth flows within a setup’s configuration UI. Here’s how it will work once released:

First, use a custom config value. Within node.json for example use:

{
  "title": "Spotify integration",
  "name": "spotify",
  "type": "custom",
  "page": "spotify.html",
  "ui_width": 3,
  "default": {}
}

Then add the referenced spotify.html to your package. A minimal example implementation is give below. The result neatly integrate a button into the rest of the configuration interface:

image

The code that powers the embedded iframe might look as follows:

<html>
  <body>
    <button id='btn' class='btn btn-default'>Connect to Spotify</button>
    <div id='bottom'></div>
    <script src='hosted.js'></script>
    <script>
      function nonce() {
        const array = new Uint32Array(32)
        window.crypto.getRandomValues(array)
        return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('')
      }
      function sha256(plain) {
        const encoder = new TextEncoder()
        const data = encoder.encode(plain)
        return window.crypto.subtle.digest('SHA-256', data)
      }
      function base64url_encode(str) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
          .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
      }
      async function pkce_verifier_to_challenge(v) {
        return base64url_encode(await sha256(v))
      }

      let btn = document.getElementById('btn')

      function update_btn() {
        if (ib.config.refresh_token) {
          btn.innerText = "Connected"
        } else {
          btn.innerText = "Connect to Spotify"
        }
      }

      ib.ready.then(function() {
        ib.setDefaultStyle()

        update_btn()

        btn.addEventListener('click', async () => {
          if (ib.config.refresh_token) {
            ib.setConfig({})
            update_btn()
            return
          }

          const redirect_uri = 'https://info-beamer.com/oauth/callback'
          const client_id = '<YOUR CLIENT ID>'
          const code_verifier = nonce()
          const code_challenge = await pkce_verifier_to_challenge(code_verifier)
          const url = "https://accounts.spotify.com/authorize"
              + '?response_type=code'
              + `&client_id=${encodeURIComponent(client_id)}`
              + '&state={STATE}'
              + `&redirect_uri=${encodeURIComponent(redirect_uri)}`
              + '&code_challenge_method=S256'
              + `&code_challenge=${encodeURIComponent(code_challenge)}`
          const redir = (new URL(await ib.oauth_redirect(url))).searchParams
          const body = new URLSearchParams()
          body.append('client_id', client_id)
          body.append('grant_type', 'authorization_code')
          body.append('code', redir.get('code'))
          body.append('redirect_uri', redirect_uri)
          body.append('code_verifier', code_verifier)
          const token_res = await fetch('https://accounts.spotify.com/api/token', {
            method: 'POST',
            body: body,
          })
          const res = await token_res.json()
          ib.setConfig({refresh_token: res.refresh_token})
          update_btn()
        })
      })
    </script>
  </body>
</html>

The new feature added here is the ib.oauth_redirect function. When used as await ib.oauth_redirect(url) like in the code above, it replaces any reference to {STATE} in the provided url value with an info-beamer generated oauth state value and opens the url in a new window. When redirected back to https://info-beamer.com/oauth/callback the complete url is passed back to the caller of ib.oauth_redirect and can be handled. The above code uses the provided code value to grab the refresh_token and stores that in the setup’s configuration.

In the generated config.json, the result will be

{
   ....<rest of config.json>....
   "spotify": {
      "refresh_token": "<the retrieved refresh token>"
   }
}

This feature is now live. Feedback welcome.

I’m fine with adding it to the store if you think it might be usefull.

Having some sensitive stuff in the config is unavoidable and didn’t really mind. Would have also been less of an issue if Spotify apps would have limited scope. But this definitely makes it much easier for other people to use and will look into implementing it. Thank you for quickly adding this new feature.

Worse at the moment is actually creating a long-lived access token for the facebook package, which still has to be redone from time to time. Ironically, that could perhaps be made simpler by having the client secret in the config.

A bit off topic, but one thing that would be really nice to have is the possibility to let a tile/package skip a page in the scheduled player package. Fullscreen event promotions would then be possible without having to display most of the time some kind of fallback asset. That is not really possible right now, right?

Took me a while to figure out that it wasn’t really possible to retrieve any of the config values outside the custom config option. Without having to make the whole config a custom thing.

Pkce authentication was sort of working, but as the refresh token will each time only be once valid it’s more buggy and will need to be stored back into the config. Will fix that at a later point, but for know at least it’s working again with client id and secret. At least dropping the need for any external tools to set up the package.

Especially, the Spotify package will (at the moment and likely in still in the future) not really work for running the same setup on multiple devices. It would perhaps be nice if such a thing could be specified in the package.json as a limitation.

Neat.

Correct. The custom config html for a single option is then (at least currently) limited to only setting its own option value.

I guess that’s Refresh Token Rotation (see RFC). In that case one is lost without a proxy backend :-\

Probably. But that of course then makes it impossible to use the package without each user individually creating their own “app” in the developer section of the service. Not exactly easy to use :frowning:

You can have a method:

function M.can_show(config)
    -- true: Whole page is scheduled as normal
    -- false: Whole page is skipped if one of the tiles returns false
    return true / false
end

info-beamer - Digital Signage for the Raspberry Pi community forum - Imprint