jump to content

Music mashing with YouTube, YQL and Open Data Tables

BBC’s top 20 chart + YouTube’s music videos + YQL = AddBass top 20 excellence!

The plan

Grab the top 20 chart singles from the BBC web site, look up each song on YouTube and get the most popular matching videos. Show them in the chromeless player and set it so that they play automatically and users can skip ones they don’t like.


Make it so

BBC chart data

The BBC publish chart data at www.bbc.co.uk/radio1/chart/singles/ but this is not available in a raw data format such as XML or JSON. Luckily, Yahoo have introduced Yahoo! Query Language (YQL) that can extract data from HTML pages using an XPath expression. I needed the song title and artist and the BBC has these in the image alt attribute.

The following YQL extracts the top 20 from the chart page:

[codesyntax lang="sql"]

SELECT alt FROM html WHERE
url="http://www.bbc.co.uk/radio1/chart/singles/" and xpath="//li/img" LIMIT 20

[/codesyntax]

The result from trying it in the console:

YouTube data

Now that I had the chart data, an option would be for the web browser to query YouTube for each song title and show the video. This would be a slow process and a bit of a pain as each request would be asynchronous and be returned in a different order to the one sent.

YQL supports the IN operator and sub-queries for Open Data Tables. If I used a YouTube table, I could have all the processing done on Yahoo’s servers in one YQL query. There wasn’t a YouTube community table so I created an Open Table at http://addbass.com/youtube.xml with the following definition:

[codesyntax lang="xml"]

<table>
  <meta>
    <author>Hugh Bassett-Jones</author>
    <documentationURL>http://code.google.com/apis/youtube/2.0/reference.html</documentationURL>
    <sampleQuery>select entry where q="jackson"</sampleQuery>
  </meta>
  <bindings>
    <select itemPath="feed.entry" produces="XML">
      <urls>
        <url env="all">http://gdata.youtube.com/feeds/api/videos</url>
      </urls>
      <inputs>
        <key id="q" type="xs:string" paramType="query" required="true"/>
        <key id="format" type="xs:string" paramType="query" default="5"/>
        <key id="max-results" type="xs:string" paramType="query" default="1"/>
        <key id="start-index" type="xs:string" paramType="query" default="1"/>
        <key id="orderby" as="order" type="xs:string" paramType="query" default="relevance"/>
      </inputs>
    </select>
  </bindings>
</table>

[/codesyntax]

This data table is used in the YQL query

[codesyntax lang="sql"]

USE "http://addbass.com/youtube.xml" AS vids;
SELECT id, title FROM vids WHERE order="viewCount" AND q IN (
  SELECT alt FROM html WHERE url="http://www.bbc.co.uk/radio1/chart/singles/" and xpath="//li/img" LIMIT 20)

[/codesyntax]

The result from trying it in the console:

Putting it all together with javascript: the breakdown

Link to the latest versions of jQuery and swfobject on Google’s CDN and the local js file in the document head:

[codesyntax lang="xml"]

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/swfobject/2/swfobject.js"></script>
<script src="top.js"></script>

[/codesyntax]

Load the YouTube chromeless player before the closing document body:

[codesyntax lang="xml"]

<script type="text/javascript">
  swfobject.embedSWF('http://www.youtube.com/apiplayer?enablejsapi=1&playerapiid=player', 'video', '400', '300', '8', null, null, { allowScriptAccess: 'always', bgcolor: '#000000', wmode: 'transparent' }, { id: 'myplayer' });
</script>;

[/codesyntax]

In top.js, declare a global variable to hold video player details:

[codesyntax lang="javascript" lines_start="1"]

var player;

[/codesyntax]

onYouTubePlayerReady is called when the player has loaded:

[codesyntax lang="javascript" lines_start="3"]

function onYouTubePlayerReady(playerId) {

  // youtube player
  player = document.getElementById('myplayer');
  player.addEventListener('onStateChange', 'onPlayerStateChange');
  player.userEnded = false;
  player.currentVideoID = undefined;
  player.playvideo = function() {
    if(player.currentVideoID == undefined)
      player.currentVideoID = $('#tracks li:first').attr('id');

    $('#tracks li').removeClass('selected').find('span').remove();
    $('#' + player.currentVideoID).addClass('selected');
    player.loadVideoById(player.currentVideoID, 0);

    player.userEnded = false; // reset
    $('#loading').fadeOut('slow');
  };

[/codesyntax]

player.updateRemaining() gets the total number of seconds remaining and converts it to minutes and seconds. This function is called once a second.

[codesyntax lang="javascript" lines_start="21"]

  player.updateRemaining = function () {
    var mins = Math.floor((player.getDuration() - player.getCurrentTime()) / 60);
    var secs = Math.floor((player.getDuration() - player.getCurrentTime()) % 60);
    secs = ('00' + secs).slice(-2); // leading zero
    $('#' + player.currentVideoID).find('span').remove();
    $('#' + player.currentVideoID).append('<span>'+ mins + ':' + secs + '</span>');
  };

  setInterval(player.updateRemaining, 1000);

[/codesyntax]

The control click events are wired up:

[codesyntax lang="javascript" lines_start="31"]

    // wire up controls
    $('#previous').click(function() { player.userEnded = true; player.currentVideoID = $('#' + player.currentVideoID).prev().attr('id'); player.playvideo(); return false; });
    $('#back').click(function() { player.seekTo(player.getCurrentTime() - 30, true); return false; });
    $('#play').click(function() { player.userEnded = true; player.currentVideoID = player.currentVideoID; player.playvideo(); return false; });
    $('#stop').click(function() { player.userEnded = true; player.stopVideo(); return false; });
    $('#forward').click(function() { player.seekTo(player.getCurrentTime() + 30, true); return false; });
    $('#next').click(function() { player.userEnded = true; player.currentVideoID = $('#' + player.currentVideoID).next().attr('id'); player.playvideo(); return false; });

[/codesyntax]

jQuery’s ajax method is used to get the YouTube chart data through the YQL query. This allows the response to be cached on the browser making it speedy the next time the data is requested. As the data does not change often, _maxage=86400 is specified to allow Yahoo to cache the results for 24 hours on their servers:

[codesyntax lang="javascript" lines_start="39"]

  // go go go!
  $.ajax({
    url: 'http://query.yahooapis.com/v1/public/yql?q=USE%20%22http%3A%2F%2Faddbass.com%2Fyoutube.xml%22%20AS%20vids%3B%20SELECT%20id%2C%20title%20FROM%20vids%20WHERE%20order%3D%22viewCount%22%20AND%20q%20IN%20(SELECT%20alt%20FROM%20html%20WHERE%20%0Aurl%3D%22http%3A%2F%2Fwww.bbc.co.uk%2Fradio1%2Fchart%2Fsingles%2F%22%20and%20xpath%3D%22%2F%2Fli%2Fimg%22%20LIMIT%2020)%20%7C%20sanitize()&format=json&diagnostics=false&_maxage=86400',
    cache: true,
    dataType: 'jsonp',
    jsonp: 'callback',
    jsonpCallback: 'yqlSuccess',
    success: function(data, text, request) { yqlSuccess(data); },
    error: function(request,status,errorThown) { yqlFail(status);}
  });

[/codesyntax]

If the request fails, show an error message instead of the loading text:

[codesyntax lang="javascript" lines_start="50"]

  function yqlFail(status) {
    $('#loading').addClass('fail').html('Oh noes! ' + status );
  }

[/codesyntax]

If the request is successful, build a list where each list item has the same ID as the music video on YouTube:

[codesyntax lang="javascript" lines_start="54"]

  function yqlSuccess(data) {

    $('#tracks').empty();

    $.each(data.query.results.entry, function(i,item){
      var id = item.id.replace('http://gdata.youtube.com/feeds/api/videos/','');
      var title = item.title.content.indexOf(" - ") == -1 ? '<b>' + item.title.content + '</b>' : '<b>' +     item.title.content.substring(0,item.title.content.indexOf(" - ")) + '</b> '+ item.title.content.substring(item.title.content.indexOf(" - "));
      var $li = $('<li id="' + id + '" ><a title="Play ' + item.title.content + '" href="http://www.youtube.com/watch#!v=' + id + '">' + title + ' </li></a>');
      $li.click(function() { player.userEnded = true; player.currentVideoID = this.id; player.playvideo(); return false;  });
      $('#tracks').append($li);
    });

    player.playvideo();
  }

};

[/codesyntax]

onPlayerStateChange has been wired up on line 7 to fire every time there’s a change in state. This could be from the music video starting, ending, or a user skipping a track.

[codesyntax lang="javascript" lines_start="70"]

function onPlayerStateChange(newState) {

  if (newState == 0 && !player.userEnded) { // track finished
    player.currentVideoID = $('#' + player.currentVideoID).next().attr('id');
    player.playvideo();
  }

};

[/codesyntax]

View the complete listing at http://addbass.com/top20/top.js.

Leave a reply