---
title: Control Rdio from Vim
teaser: Control Rdio from Vim using AppleScript, JavaScript, and duct tape.
tags: vim,osx,unix
author: Gabe Berke-Williams
published_on: 2014-11-21
---

I recently spent a number of hours trying to get Rdio to play playlists from the
command line. My solution is a neat hack, but there's also value in seeing what
didn't work before seeing what did.

My goal was to have Vim key bindings to play a specific playlist in Rdio. It
turned out to be much more difficult than I expected. Here's how I did it, but
first a GIF of it in action:

[![vim-rdio in action][vim-rdio-image]][vim-rdio-image]

[vim-rdio-image]: https://images.thoughtbot.com/vim-rdio/vim-rdio.gif

## rdio-cli

First I found [Wynn Netherland]'s [rdio-cli] gem, which interacts with the Rdio
desktop app via AppleScript. It's great, it works, and if you use the Rdio
desktop app, I recommend it. I considered it, but I use Rdio online, and I
wanted to script that if possible. rdio-cli also doesn't let you select a
playlist. Onwards!

[Wynn Netherland]: http://wynnnetherland.com/
[rdio-cli]: https://github.com/pengwynn/rdio-cli/

## Rdio API

Then I tried the Rdio API. I signed up for a developer key and browsed their
[docs], trying to figure out what "user key" means, and how to get a list of
playlist names for a user. After figuring out how to get playlist names, I
realized that there's no way to play any music via the Rdio API. The only way to
play music outside of the Rdio app on your desktop is via the [Web Playback
API], which is a Flash file. That's a no-go for Vim.

[docs]: http://www.rdio.com/developers/docs/
[this page]: http://rdioconsole.appspot.com/#method=getUserPlaylists
[here]: http://rdioconsole.appspot.com/#url%3D%252Fpeople%252Fgabebw%26method%3DgetObjectFromUrl
[Web Playback API]: http://www.rdio.com/developers/docs/web-playback/overview/

## [Hacking Montage]

Here's where I considered a few crazy things:

* Can I embed a browser in Vim somehow and use the Web Playback API? Emacs could
  probably do it, but I couldn't find anything for Vim.
* Could I use [chromedriver] to attach to an existing Chrome instance and
  control Rdio that way? [Turns out, no].
* OK, Rdio uses a lot of JavaScript, is there any way to send arbitrary
  JavaScript to Chrome?
* Wait, rdio-cli uses AppleScript. AppleScript can interact with Chrome, and it
  can tell Chrome to [run arbitrary JavaScript]...hmmm.

[chromedriver]: https://sites.google.com/a/chromium.org/chromedriver/
[Turns out, no]: https://code.google.com/p/selenium/issues/detail?id=18
[run arbitrary JavaScript]: http://stackoverflow.com/questions/5135609/can-applescript-access-browser-tabs-and-execute-javascript-in-them

## AppleScript

I started writing some AppleScript to get a feel for what was possible with
Chrome. Here's my first script, runnable in `Script Editor.app` or with
`osascript`:

```applescript
tell application "Google Chrome"
  get URL of tab 1 of window 1
end tell
```

I quickly hit a wall with AppleScript: I needed to filter windows and tabs by
title to find the one where Rdio was playing, which is possible but painful in
AppleScript. Then I remembered: Apple added a [JavaScript bridge to AppleScript]
in OS X Yosemite. Here's the same function in JavaScript:

[JavaScript bridge to AppleScript]: https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/index.html

```javascript
var app = Application("Google Chrome");

app.windows[0].tabs[0].url();
```

I used [the official Apple docs] to write a function to find the tab where Rdio
is playing:

[the official Apple docs]: https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/index.html

```javascript
function findRdioTab(){
  var app = Application("Google Chrome");
  var rdioTab = undefined;

  for(var i = 0; i < app.windows().length; i++){
    var window = app.windows[i];
    var possibleRdioTabs = window.tabs.whose({ title: { _endsWith: 'Rdio' } })
    if( possibleRdioTabs.length > 0 ){
      rdioTab = possibleRdioTabs.at(0);
      break;
    }
  }

  return rdioTab;
}
```

Sweet. Note that I'm using the Apple-provided [`.whose` method] to filter the
array. Once I have a reference to the Rdio tab, I can call `rdioTab.execute({
javascript: "alert()"})` to run any JavaScript I want to. Let's find out what JavaScript I
should run.

[`.whose` method]: https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/index.html#//apple_ref/doc/uid/TP40014508-CH109-SW10

## Delving into Rdio's JavaScript source

Unsurprisingly, this phase took a long time. Rdio is a large, complicated
app, and it's hard to figure out what's happening as an end-user. Fortunately,
Rdio provides [source maps] for their JavaScript, which made understanding it
easier.

[source maps]: http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/

I figured out that Rdio is built on Backbone because I saw telltale
`Backbone.Model` in the source. That gave my search structure, and I discovered
`R.player`, which seemed like a good place to start.  Unfortunately,
getting the list of playlist names requires a lot of information from disparate
sources, and it's not realistic to gather that data as an end-user.

I did find `R.burnItDown()`, which will give you the peaceful Rdio experience
you've always wanted.

I decided to fall back to clicking on UI elements with jQuery, since that
required the least amount of knowledge and was the easiest to script. It's also
the easiest to fix if Rdio changes their code in the future. I inspected
elements to find the correct selectors, and I was off.

It was straightforward to get the names of all the playlists:

```javascript
function getPlaylistNames(){
  // Underscore.js's _.map is available since Rdio is a Backbone app.
  return _.map($('a.playlist'), function(a) { return $(a).prop('title'); })
}
```

It was also straightforward to visit a playlist with a specific name:

```javascript
function selectPlaylist(playlistName){
  $('a.playlist[title="' + playlistName + '"]').click()
}
```

Here's the `rdio-list-playlists.applescript.js` file so far:

```javascript
function findRdioTab(){
  var app = Application("Google Chrome");
  var rdioTab = undefined;

  for(var i = 0; i < app.windows().length; i++){
    var window = app.windows[i];
    var possibleRdioTabs = window.tabs.whose({ title: { _endsWith: 'Rdio' } })
    if( possibleRdioTabs.length > 0 ){
      rdioTab = possibleRdioTabs.at(0);
      break;
    }
  }
  return rdioTab;
}

// The "run" function is automatically run when the file is run
function run(argv) {
  var rdioTab = findRdioTab();
  // The function needs to be passed to `execute` below as a string.
  var defineGetPlaylistNames = "function getPlaylistNames(){ return _.map($('a.playlist'), function(a) { return $(a).prop('title'); }); }";

  rdioTab.execute({javascript: defineGetPlaylistNames});
  var result = rdioTab.execute({javascript: "getPlaylistNames()"})

  // The return value gets printed to stdout
  return result.join("\n");
}
```

Now I have a file that I can run with `osascript -l JavaScript
rdio-list-playlists.applescript.js`, and it will print the playlist names to standard
output (stdout). Let's use [`fzf`] to add fuzzy-finding, and I end up with:

```sh
osascript -l JavaScript rdio-list-playlists.applescript.js | fzf
```

[`fzf`]: https://github.com/junegunn/fzf

Now I can easily get the name of a playlist, with fuzzy-finding to boot. Let's
move on to playing the playlist.

## Actually playing a playlist

There are three steps to playing a playlist: clicking on the playlist name,
waiting for the playlist to load, and then clicking the play button for the
first track in that playlist.

Let's start with clicking the play button. I ran into some problems clicking on
the "play" button once a playlist loaded.  It turns out that Rdio doesn't
destroy old Backbone views, so `$(".PlayButton:first")` is the first "Play"
button ever loaded. This means clicking on it will play the first playlist
you've ever loaded, instead of the latest one. To fix this, I added the
[`:visible` pseudo-selector] to only select play buttons that are visible on the
page. Here's the function:

[`:visible` pseudo-selector]: http://api.jquery.com/visible-selector/

```javascript
function playCurrentPlaylist(){
  // Order of pseudo-selector matters: :first:visible finds the first play
  // button and checks if it's visible, which it's not.
  $('.PlayButton:visible:first').click();
}
```

Here, I ran into another problem: playlists take a few seconds to load once you
click on them. I had to figure out how to run `playCurrentPlaylist` at the right
time. I tried [listening to events] from `R.router`, Rdio's `Backbone.Router`
instance, by extending an empty object with `Backbone.Events`:

[listening to events]: http://stackoverflow.com/questions/19588401/backbone-navigation-callback

```javascript
var eventProxy = {};
_.extend(eventProxy, Backbone.Events);
eventProxy.listenTo(R.router, 'contentChanged', playCurrentPlaylist);
```

But that fired `playCurrentPlaylist` either right before the playlist loaded, or
at least before the playlist fully loaded. So I kicked it old-school by using
`setTimeout` to wait a few seconds before hitting play:

```javascript
function selectAndPlayPlaylist(playlistName){
  setTimeout(playCurrentPlaylist, 3000);

  $("a.playlist[title='" + playlistName + "']").click()
}
```

Here's the full `rdio-play-specific-playlist.applescript.js` file:

```javascript
function findRdioTab(){
  // Exactly the same implementation as before.
}

var definePlayCurrentPlaylist = "function playCurrentPlaylist(){$('.PlayButton:visible:first').click(); };";
var defineSelectAndPlayPlaylist = "function selectAndPlayPlaylist(playlistName){setTimeout(playCurrentPlaylist, 3000); $('a.playlist[title=\"'+playlistName+'\"]').click(); }"
var defineFunctions = definePlayCurrentPlaylist + defineSelectAndPlayPlaylist;

function run(argv){
  var rdioTab = findRdioTab();
  // Get the first argument
  var playlistName = argv[0];
  rdioTab.execute({javascript: defineFunctions});
  rdioTab.execute({javascript: 'selectAndPlayPlaylist("' + playlistName + '")'})
}
```

## Putting it all together

So now I tried stringing it all together:

```bash
osascript -l JavaScript rdio-list-playlists.applescript.js | fzf | \
  osascript -l JavaScript rdio-play-specific-playlist.applescript.js
```

That didn't work, and it took me a second to figure out why. Can you figure it
out?

Pipes pass the output (stdout) of one function as input to another (stdin). But
`argv` in the AppleScript files refers to arguments on the command line, not
input from stdin. We need to take the piped output from `fzf` and pass it to
`osascript` as if it were typed on the command line. Fortunately, `xargs` (`man
xargs`) does exactly that:

```bash
osascript -l JavaScript rdio-list-playlists.applescript.js | fzf | \
  xargs osascript -l JavaScript rdio-play-specific-playlist.applescript.js
```

One last problem: playlists with spaces in them are interpreted as two
arguments, because xargs splits on spaces. So we want the first argument, but
`Car Singalongs` is read as two arguments, `Car` and `Singalongs`. Let's add
double quotes around the result with `sed` before passing it along:

```bash
osascript -l JavaScript rdio-list-playlists.applescript.js | fzf | \
  sed -e 's/^/"/g' -e 's/$/"/g' | \
  xargs osascript -l JavaScript rdio-play-specific-playlist.applescript.js
```

Now we have a working script that fuzzy-finds a playlist name, then plays that
playlist.

## Vim

Remember when I said I wanted to use this in Vim? Here's that same command in
idiomatic Vim, using `fzf#run` from `fzf`'s [Vim plugin]:

[Vim plugin]: https://github.com/junegunn/fzf#install-as-vim-plugin

```vim
function! RdioPlaylist()
  let items = fzf#run({'source': 'osascript -l JavaScript rdio-list-playlists.applescript.js'})
  let playlistName = shellescape(items[0])
  call system("osascript -l JavaScript rdio-play-specific-playlist.applescript.js " . playlistName)
endfunction

" Allows us to do :RdioPlaylist directly instead of :call RdioPlaylist()
command! RdioPlaylist call RdioPlaylist()
```

## Whew.

Finally, we can play a specific playlist using `:RdioPlaylist` in Vim. Let's
recap how it all works:

* `rdio-play-playlists.applescript.js` sends JavaScript to the Rdio tab in
  Chrome that gets the name of every playlist. It then outputs each playlist on
  its own line to standard output.
* `fzf#run` takes that output and provides a fuzzy-finding interface to allow
  you to select one of the playlist names, then returns that playlist name.
* We run `shellescape` over the playlist name to put quotes around it, escape
  special characters, etc.
* `rdio-play-specific-playlist.applescript.js` takes in the shellescaped
  playlist name as the argument and plays that playlist.

## Try vim-rdio

I've turned this code into a Vim plugin called [vim-rdio]. In addition to
playing playlists, it can play/pause the current track and skip to the next
track. Check out the Rakefile, which does some neat things with Erb.

[vim-rdio]: https://github.com/gabebw/vim-rdio
