Using Linked Data and jQuery Mobile to produce a podcast explorer web app

A podcast playing on a mobile phoneIn a recent poll on this site I asked "Do you have, or are you planning to learn, any skills related to Linked Data?". Interestingly 60% of respondents (there were 101 votes) said yes, so I thought I should finally get round to writing up a demonstration app that uses Linked Data to provide the information and jQuery Mobile to provide the looks (and more) for a mobile podcast by subject explorer. The site is written using PHP and was developed quite quickly. Again I will be using the Open University's Linked Data store, but the site could easily be adapted to use other stores, maybe even more than one store. Thanks to the use of jQuery Mobile it would even be possible to take the site and embed it in a thin app on the phone to make it look a bit like a native app. Of course the site is a bit rough and ready and I am sure there are thousands of ways to improve it, so experiment and let me know how you get on in the comments.

The site enables uses to find podcasts by subject. It doesn't have as deep as hierarchy as the official OU podcast web site, though this could be added. The first step is to write a bit of common code. In a file named shared.php I wrote functions for making a web request and converting the results into a PHP object, the method was based on my earlier post An approach to consuming Linked Data with PHP. Notice that there is not a lot of error handling, so before use in a production environment you should add code to handle HTTP errors. The contents of the shared.php file are:

<?php
  
// Perform an HTTP request using CURL
  
function request($url){
     
// is curl installed?
     
if (!function_exists('curl_init')){
        die(
'CURL is not installed!');
      }
     
// get curl handle
     
$ch= curl_init();
     
// set request url
     
curl_setopt($ch, CURLOPT_URL, $url);
     
// return response, don't print/echo
     
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
     
$response = curl_exec($ch);
     
curl_close($ch);
      return
$response;
   }

  

// Send a SPARQL query to the OU endpoint and convert the results into a PHP array
  
function do_LD_Query($sparql) {
    
$requestURL = 'http://data.open.ac.uk/query?query='.urlencode($sparql);
    
$response = request($requestURL);
    
// container for our data
    
$data = array();
    
// initialise SimpleXML object and load it with data
    
$xml = simplexml_load_string($response);
     if (
$xml === FALSE) {
        return
$response;
     }
    
// get the <results> element
    
$results = $xml->results;
    
// loop through <result> elements and extract values
    
if (isset($results->result)) {
       foreach(
$results->result as $result) {
        
$line = array();
         foreach (
$result->binding as $binding) {
           if (isset(
$binding->uri)) {
            
$line[(string) $binding["name"]] = (string) $binding->uri;
           }
           else {
            
// could pick up xsd data type for right cast
            
$line[(string) $binding["name"]] = (string) $binding->literal;
           }
         }
        
$data[] = $line;
       }
     }
     return
$data;
  }
?>

Subject browser screen

The starting page for the app gives the user the chance to pick the subject that they are interested in. Sadly it takes a few seconds to generate so the screen is blank for a little while. A good addition here would be some sort of "Loading..." notification. As with all pages that generate output for the user on this site it uses jQuery Mobile to render the look and feel. Thanks to this wonderful library our web application can easily look half decent, it also gives the user a good experience. A SPARQL query is used to get the subjects and a jQuery Mobile listview is used to render them on the page. Note for convenience I am getting the jQuery files directly from their server, but for a production site you should host your own copy. This protects you against breakages due to changes and upgrades, it is also more polite as you won't be using their bandwidth! The index.php page looks like this:

<?php
/**
* Start page for app
* Displays index of subjects
*/
require_once("shared.php");
ob_start("ob_gzhandler"); // compress output

// generate and SPARQL query to find out subjects

$sparql =
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT DISTINCT ?subject ?subject_label {
?p <http://purl.org/dc/terms/subject> ?subject.
?subject skos:inScheme <http://data.open.ac.uk/topic> .
OPTIONAL {?subject rdfs:label ?subject_label} }
ORDER BY ?subject_label"
;
$results = do_LD_Query($sparql);
// generate a list of unique subjects
$subjects = array();
foreach (
$results as $result) {
 
$title = $result['subject_label'];
  if (empty(
$title) ) {
   
$title = $result['subject'];
  }
 
$subjects[$result['subject']] = $title;
}
?>

<!DOCTYPE html>
<html>
  <head>
    <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE" />
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.css" />
    <link rel="stylesheet" href="style.css" />
    <script src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.js"></script>
    <title>OU Podcasts</title>
  </head>
  <body>
  <div data-role="page" id="index" data-theme="b">
    <div data-role="header">
      <h1>OU Podcasts</h1>
    </div><!-- /header -->
    <div data-role="content">
      <ul data-role="listview" data-theme="c">
        <?php   
    
// generate a link for each subject
    
foreach ($subjects as $subject => $title) {
       
printf("<li><a href='#bysubject.php?subject=%s' data-transition='slide'>%s</a></li>", urlencode($subject), htmlentities($title));
     }
       
?>

      </ul>
    </div><!-- /content -->
    <div data-role="footer">
    </div><!-- /footer -->
  </div><!-- /page -->
  </body>
</html>

Showing all podcasts for a subject

In the index page the subject is used as a key to determine the content of the next page. Once a user selects a subject we need to show them a list of available podcasts. The subject is passed through a query parameter to the page bysubject.php, which is below.

<?php
/**
* Podcasts by subject list
*/
require_once("shared.php");
ob_start("ob_gzhandler"); // compress output
$subject = $_GET['subject'];
// Generate the SPARQL query
$sparql = sprintf("
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT DISTINCT ?podcast ?title ?download ?subject_label
WHERE {
   ?podcast <http://purl.org/dc/terms/title> ?title .
   ?podcast <http://digitalbazaar.com/media/download> ?download .
   ?podcast <http://purl.org/dc/terms/subject> <%s> .
   <%s>  rdfs:label ?subject_label;
}
ORDER BY ?title"
, $subject, $subject);
// run it
$results = do_LD_Query($sparql);
?>

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.css" />
    <link rel="stylesheet" href="style.css" />
    <script src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.js"></script>
    <title>OU Podcasts</title>
  </head>
  <body>
    <div data-role="page" id="picksubject">
      <div data-role="header" data-theme="b">
<h1><?php echo $results[0]['subject_label']; ?></h1>
      </div><!-- /header -->
      <div data-role="content" class="ui-body">
        <?php   
         $list_html
= '<ul data-role="listview" data-theme="c">';
        
// build link for each item
        
foreach ($results as $result) {
          
$podcast_id = str_replace("http://data.open.ac.uk/podcast/", "", $result['podcast']);
      
$list_html .= sprintf("<li><a href='#play.php?podcast_id=%s' data-transition='slide'>%s</a></li>", urlencode($podcast_id), $result['title']);
     }
        
$list_html .= "</ul>";
        
// if there were no results print an apology
        
if (sizeof($results)==0) {
           echo
"<div data-theme='a'><p>Sorry, there are no podcasts available for this subject.</p></div>";
         }
         else {
           echo
$list_html;
         }
       
?>

      </div><!-- /content -->
      <div data-role="footer">
      </div><!-- /footer -->
    </div><!-- /page -->
  </body>
</html>

You might noticed the call to ob_start("ob_gzhandler") in the files. This tells the webserver (if it and the client support it) to compress the output of the web page before sending it to the client, which will hopefully speed up the transfer of the page to the mobile device and save the user money by keeping the data volume down. We know in this case that http://data.open.ac.uk/podcast/ is going to be the start of every URL for the podcasts so here it is removed before being used as a key to be passed to the next page, this helps to reduce the size of the page slightly. Now the user has selected a podcast we can show them some more details and invite them to play it on their device. The code for the play.php page is below.

<?php
/**
* Play a podcast
*/
require_once("shared.php");
ob_start("ob_gzhandler"); // compress output

$podcast_id = $_GET['podcast_id'];
// some podcasts do not have relates to course and transcripts
$podcast_filter = sprintf("<http://data.open.ac.uk/podcast/%s>", $podcast_id);
$sparql = sprintf("SELECT ?title ?depiction ?duration ?comment ?transcript ?format ?download ?course_code ?course_name ?course_url
WHERE {
  ?podcast <http://purl.org/dc/terms/title> ?title .
  ?podcast <http://digitalbazaar.com/media/depiction> ?depiction .
  ?podcast <http://digitalbazaar.com/media/duration> ?duration .
  ?podcast <http://www.w3.org/2000/01/rdf-schema#comment> ?comment .
  ?podcast <http://www.w3.org/TR/2010/WD-mediaont-10-20100608/format> ?format .
  ?podcast <http://digitalbazaar.com/media/download> ?download .
  OPTIONAL {
    ?podcast <http://data.open.ac.uk/podcast/ontology/transcript> ?transcript .
    ?podcast <http://data.open.ac.uk/podcast/ontology/relatesToCourse> ?course .
    ?course <http://purl.org/vocab/aiiso/schema#code> ?course_code .
    ?course <http://purl.org/vocab/aiiso/schema#name> ?course_name .
    ?course <http://purl.org/net/mlo/url> ?course_url .
  }
    FILTER (?podcast = %s)
  } "
, $podcast_filter);

$results = do_LD_Query($sparql);
$title = htmlentities($results['0']['title']);
$depiction = htmlentities($results['0']['depiction']);
$duration = htmlentities($results['0']['duration']);
$comment = htmlentities($results['0']['comment']);
$transcript = htmlentities($results['0']['transcript']);
$format = htmlentities($results['0']['format']);
$download = $results['0']['download'];
$course_code = htmlentities($results['0']['course_code']);
$course_name = htmlentities($results['0']['course_name']);
$course_url = $results['0']['course_url'];
$play_link = sprintf("playfile.php?format=%s&file=%s", $format, urlencode($download));
?>

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.css" />
    <script src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.js"></script>
    <title>OU Podcasts</title>
  </head>
  <body>
    <div data-role="page" id="play">
      <div data-role="header" data-theme="b">
<h1><?php echo $title; ?></h1>
      </div><!-- /header -->
      <div data-role="content" data-theme="c">
<p><img height="200" width="200" src="<?php echo $depiction; ?>" /></p>
         <h4>Duration</h4>
         <p><?php echo $duration; ?></p>
         <h4>Comment</h4>
         <p><?php echo $comment; ?></p>
         <p><a href="<?php echo $play_link; ?>" rel="external" data-role="button">Play</a>
         <?php if (!empty($transcript)): ?>
           <a href="<?php echo $transcript; ?>" rel="external" data-role="button">Download transcript</a>
         <?php endif; ?>
         <?php if (!empty($course_url)): ?>
           <a href="<?php echo $course_url; ?>" rel="external" data-role="button"><?php echo $course_code." ".$course_name; ?></a></p>
         <?php endif; ?>
      </div><!-- /content -->
      <div data-role="footer" data-theme="a">
      </div><!-- /footer -->
    </div><!-- /page -->
  </body> 
</html>

Play option in web appThings get slightly more complicated at this point. As well as a play button, the user also gets buttons to download a transcript of the podcast and view the web page for the course that the podcast relates to. However, not every podcast has information for a transcript or course link, so in the SPARQL query these attributes are surrounded by an OPTIONAL keyword to make sure we get as many details as we can. The buttons for these options are also only generated if this information is available. The FILTER keyword is used here to restrict the results to the podcast that the user selected (thanks to Mathieu d’Aquin for his help with this bit!). Using this approach we can make additional information available to the user which can enhance the value of using the site. This would be a good place to add things like related podcasts, OpenLearn units and courses to make it really easy for the user to explore what is on offer.

Once the user taps the Play button ideally we want to be able to pass the media file to the mobile device and it can then play it. This works really well for audio, but of course for video nothing is ever this simple! It turns out that Android devices do not like the MIME type that is sent by the OU Podcast server and will refuse to play video files. However all is not lost, and there is a way round it by changing the MIME type and passing the file through our PHP script. This might use quite a bit of your bandwidth though, so it is an approach to be used sparingly and carefully. The playfile.php file is below.

<?php
// Determine if browser is on an Android machine
function is_android() {
 
// note ! = = not != to test equality based on type
  // "Linux Ventana" is an Asus EEE Pad Transformer browser in desktop nomode
 
return stristr( $_SERVER['HTTP_USER_AGENT'], "Android") !== FALSE
    
|| stristr( $_SERVER['HTTP_USER_AGENT'], "Linux Ventana") !== FALSE;
}

// main
$format = $_GET['format'];
$file = $_GET['file'];
// check file is from OU pocasts
if (!strpos("http://podcast.open.ac.uk/", $file) == 0) {
  echo
"Unsupported.";
}
else {
 
// change format header so android can play file
 
if ($format == "video/x-m4v" && is_android()) {
    
$format "video/mp4";
    
header("Content-Type: ".$format);
    
readfile($file);
  }
  else {
    
header("Location: ".$file);
  } 
}
?>

It is worth testing out a variety of devices with the site as it may be necessary to add similar workarounds for other devices.

This approach could be used as the basis for developing all sorts of mobile web sites that deliver an experience optimised for touch screens. Combining the power of Linked Data and Jquery mobile means the development time to produce a site with a good look and feel is dramatically reduced. Using SPARQL queries gives you fine grained control over the data you get back which is very useful for building mobile web pages. Of course users do not need to know about any of this, all they need to know is that you have brought them the information they want in a decent looking way.

Comments

You should not host your own copy because when a user visits site A and requests googles jquery library and then visits site B that also requests googles jquery library the library is either cached on the phone or at the ISP.
The library names are version specific so you will never have the problem of getting a file with breaking changes.

That's a valid argument, however with libraries like this I think you have to be aware of the amount of somebody else's bandwidth you are using, they might not be happy if your app makes a million requests per day for example! Also hosting it yourself can mean you are not relying on someone else to not change the file, removing one possible cause of errors.

Add new comment

Comments are always very welcome, but please note the following:
  • Sadly due to the high number of spam comments recently all comments are now manually moderated. You comment will therefore not appear on the site instantly.
  • Comments on this web site are monitored for spam using Mollom. By posting a comment, you accept that your message and other personal details about you will be analysed and stored for anti-spam and quality monitoring purposes, in accordance with Mollom's privacy policy.
  • Please use your own name not a company or website name to submit comments. Your comment will be removed if you don't do this.
  • All links in comments will be marked with a no follow attribute. That means posting a link to your site here won't help your search engine rankings.
  • By submitting a comment you agree that your comment can be reproduced under the same licensing terms as the rest of the content on the site.
  • Comments can be removed at any time without explanation, but won't be removed just because you disagreed with something I said.