Spotify has this really great collaborative playlist feature. I have a few of them with various friends and it's allowed me to find a lot of great new music. I have one with two friends, and because I'm literally in love with Spotify (maybe it's because I can't come up with any other good ideas at the moment). Since I just learned how to work with the EchoNest libraries, I figured I'd do something with that.

I wanted to use Node, and the wrapper libraries for libSpotify that I could find were good, but none of them had ported the whole library, specifically, I wanted to know the users that had added the songs to the playlist, and the times they were added. I suppose I could have contributed to their codebase, but I wanted to get something done quick and dirty, so I used used libSpotify with C, and wrote a CLI program that output information in a ~SV (tilde separated) format which could be parsed by Node. A little hacky, but it got the job done.

//need to link in the libspotify library when compiling
//cc datagrabber.c -I/usr/local/opt/libspotify/include -L/usr/local/opt/libspotify/lib -lspotify -o datagrabber

#import <libspotify/api.h>
#import <stdio.h>
#import <stdlib.h>
#import <unistd.h>
#import <ctype.h>


const uint8_t g_appkey[] = {}; // get your own appkey!

const size_t g_appkey_size = sizeof(g_appkey);
int g_logged_in;
int g_playlist_loaded;
int g_userinfo_loaded;


static void logged_in(sp_session *session, sp_error error)
{
    g_logged_in = 1;
}

static void connection_error(sp_session *session, sp_error error)
{
    fprintf(stderr, "connection error");
}

static void playlist_state_changed(sp_playlist *playlist, void *userdata) {
    if(sp_playlist_is_loaded(playlist)) {
        g_playlist_loaded = 1;
    }
}

int main() {
    char playlistURI[100];
    fscanf(stdin, "%s", playlistURI);

    sp_session_callbacks callbacks = (sp_session_callbacks) {
        .logged_in = &logged_in,
        .logged_out = NULL,
        .metadata_updated = NULL,
        .connection_error = &connection_error,
        .message_to_user = NULL,
        .notify_main_thread = NULL,
        .music_delivery = NULL,
        .play_token_lost = NULL,
        .log_message = NULL,
        .end_of_track = NULL,
        .streaming_error = NULL,
        .userinfo_updated = NULL
    };

    sp_session_config config = (sp_session_config){
        .api_version = SPOTIFY_API_VERSION,
        .cache_location = "",
        .settings_location = "",
        .application_key = g_appkey,
        .application_key_size = g_appkey_size,
        .user_agent = "playlist finder",
        .callbacks = &callbacks,
        .userdata = NULL
    };

    sp_session *session;
    sp_error error = sp_session_create(&config, &session);

    if(error != SP_ERROR_OK) {
        fprintf(stderr, "Error: Cannot create session.\n");
    }
    

    g_logged_in = 0;
    sp_session_login(session, "mattegan", "***", 0, NULL);
    
    int next_timeout = 0;	
    while(!g_logged_in) {
        sp_session_process_events(session, &next_timeout);
    }

    g_playlist_loaded = 0;

    sp_link *playlistLink = sp_link_create_from_string(playlistURI);
    sp_playlist *playlist = sp_playlist_create(session, playlistLink);


    sp_playlist_callbacks playlistCallbacks = (sp_playlist_callbacks){
        .playlist_state_changed = &playlist_state_changed
    };

    sp_playlist_add_callbacks(playlist, &playlistCallbacks, NULL);

    while(!g_playlist_loaded) {
        sp_session_process_events(session, &next_timeout);
    }

    int num_tracks = sp_playlist_num_tracks(playlist);
    for(int i = 0; i < num_tracks; i++) {
        sp_track *track = sp_playlist_track(playlist, i);
        while(!sp_track_is_loaded(track)) {
            sp_session_process_events(session, &next_timeout);
        };

        sp_link *trackLink = sp_link_create_from_track(track, 0);
        char linkString[200];
        sp_link_as_string(trackLink, linkString, 200);
        
        sp_user *user = sp_playlist_track_creator(playlist, i);

        while(!sp_user_is_loaded(user) || !isalpha(sp_user_display_name(user)[0])) {
            sp_session_process_events(session, &next_timeout);
        }

        sp_album *album = sp_track_album(track);

        while(!sp_album_is_loaded(album)) {
            sp_session_process_events(session, &next_timeout);
        }

        sp_artist *artist = sp_album_artist(album);

        while(!sp_artist_is_loaded(artist)) {
            sp_session_process_events(session, &next_timeout);
        }
        
        //line output format
        //	spotifyURI~user_canonical_name~user_display_name~track_name~artist_name~album_name~track_duration~track_add_time

        printf("%s~", linkString);
        printf("%s~%s~", sp_user_canonical_name(user), sp_user_display_name(user));
        printf("%s~%s~%s~", sp_track_name(track), sp_artist_name(artist), sp_album_name(album));
        printf("%i~", sp_track_duration(track));
        printf("%i\n", sp_playlist_track_create_time(playlist, i));

    }

    return 0;
}

This looks super complicated, but in reality it's not. Most of it is just dealing with the specifics of libSpotify, which is fairly straightforward. The only confusing bit may be the following:

while(!g_logged_in) {
    sp_session_process_events(session, &next_timeout);
}

This is libSpotify's strange event handler. It's blocking. CocoaLibSpotify abstracts all of this event handling out to GCD threads, which is pretty nice. Here it doesn't really matter, the node code just waits however long it's going to take, there's not much it can do in the meantime really. The output of this program is a few hundred lines of:

spotify:track:7AvbfnZNXylSfjDgFG5vyW~mattegan~mattegan~Wooden Heart~Listener~Wooden Heart~248000~1389368504
spotify:track:7dbgHt3XQgALzwA1BqGMdi~mattegan~mattegan~Honeybee~Seahaven~Winter Forever~250000~1389368504

Each line contains a Spotify URI, which can be used to locate the song using the foreign library IDs on the EchoNest, the username and the display name for the user who added the song, the track's artist, title and album name, the duration of the song, and the time that it was added to the playlist. For each of these lines, the JS code requests information about each song from the Echonest. Instead of making hundreds of requests, it adds the songs into a taste profile, because the EchoNest has an endpoint for getting track information for all of the songs in a profile. I construct a taste profile for each user that contributing to the playlist, this way I can do some more interesting things in terms of providing each user recommendations, which I haven't actually used yet. Mainly, it allowed for me to place the information I got from libSpotify into the foreign key item for each track in the taste profile, which meant that once I got information back from the EchoNest, I didn't have to re-correlate it to the data I got from Spotify. Kind of cheating I suppose.

You have to wait a while for the EchoNest to processes each song in the taste profile. So the JS code makes requests every half second or so to an endpoint that allows for checking the processing status of a profile, I do this for each profile I've assembled, using a status code that the EchoNest gives every time you add information into it.

//this function checks the status of a taste profile and calls the callback when the update progress is 100%
function waitForTasteProfileUpdate(tasteProfileUpdateTicket, callback) {
  var check = function() {
    var checkTasteProfileStatusRequest = {
      url:  'http://developer.echonest.com/api/v4/tasteprofile/status',
      qs:   {
        api_key: echonestApiKey,
        ticket: tasteProfileUpdateTicket
      }
    }
    request(checkTasteProfileStatusRequest, function(error, response, body) {
      var response = JSON.parse(body).response;
      if(response.ticket_status === 'complete') {
        callback();
      } else{
        console.log(tasteProfileUpdateTicket + 'percent complete: ' + response.percent_complete);
        setTimeout(check, 500);
      }
    })
  }
  check();
}

Okay, so, to the point really. After I had all this data back I made some pretty graphs. That's about it. Here's one showing the times that people had been adding songs into the playlist. As you can see, I need to get to bed earlier.

image

Here's a tempo distribution for the collaborative playlist.

image

Turns out, a lot of the plots are not very interesting. The EchoNest also keeps track of a "hotttness" rating for a track and it's artist, and lots of us listen to artists that aren't very hot. We're dirty hipsters. Here's one that's particularly telling though (though, I have to admit, a total misapplication of the plot type).

image

I told you we were all dirty hipsters.