Wednesday, November 10, 2004

Yet another blog...

... which I shall use for essays, writings and such. There are of course my livejournal and opera community blogs. Actually, this is my first attempt at a professional-seeming blog. Let's see how it pans out.

Each and every week, there should be no less than one cool hack - in anything from PHP to Javascript, Java, C++, whatever... readers are invited to recommend challenges or participate however they like.

For my first trick, I will dive right into the deep end - Webservices & PHP.

We are going to be using available PEAR classes to grab data from Musicbrainz, a music site. We'll probably go so far as to tie in Audioscrobbler data as well.

Let's begin.

First off, we look at the documentation. I certainly know what that's all about, but do you? A quick overview.

Client connects to musicbrainz, and spits out a bunch of RDF/XML to form the query. For instance:

<mq:findalbum>
<mq:depth>1</mq:depth>
<mq:artistname>Pink Floyd</mq:artistName>
<mq:albumname>Dark Side of the Moon</mq:albumName>
</mq:FindAlbum>
Translates to a FindAlbum object with the properties depth, artistName and albumName. It also translates into some triples - 3 part statements.

  1. mq:FindAlbum mq:depth 1
  2. mq:FindAlbum mq:artistName "Pink Floyd"
  3. mq:FindAlbum mq:albumName "Dark Side of the Moon"


Server responds with RDF/XML

<rdf:RDF xmlns:rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc = "http://purl.org/dc/elements/1.1/"
xmlns:mq = "http://musicbrainz.org/mm/mq-1.1#"
xmlns:mm = "http://musicbrainz.org/mm/mm-2.1#">
<mq:result>
<mq:status>OK</mq:status>
<mm:tracklist>
<rdf:seq>
<rdf:li resource="http://musicbrainz.org/track/fda455fb-1b25-4863-8619-10173e721c84">
</rdf:Seq>
</mm:trackList>
</mq:Result>

<mm:track about="http://musicbrainz.org/track/fda455fb-1b25-4863-8619-10173e721c84">
<dc:title>Strangers</dc:title>
<mm:tracknum>3</mm:trackNum>
<dc:creator
rdf:resource="http://musicbrainz.org/artist/8f6bd1e4-fbe1-4f50-aa9b-94c450ec0f11"/>
<mm:trmid>35ab891c-f588-4165-8f76-b6447bfb3c4d</mm:trmid>
</mm:Track>

<mm:artist about="http://musicbrainz.org/artist/8f6bd1e4-fbe1-4f50-aa9b-94c450ec0f11">
<dc:title>Portishead</dc:title>
<mm:sortname>Portishead</mm:sortName>
<mm:albumlist>
<rdf:seq>
<rdf:li resource="http://musicbrainz.org/album/911e3f30-192e-4c3d-aa25-2a89d4202a3e">
<rdf:li resource="http://musicbrainz.org/album/67b24a4a-f1d4-427b-9269-0cfec4b98073">
<rdf:li resource="http://musicbrainz.org/album/16aaeff4-555a-4ce0-8054-d322922355f5">
<rdf:li resource="http://musicbrainz.org/album/9617b352-ab9d-496c-ad6e-9a7f22ef9ee2">
<rdf:li resource="http://musicbrainz.org/album/3677c7a6-03a6-4709-a7aa-edaea95ce473">
<rdf:li resource="http://musicbrainz.org/album/eabd233f-c56f-404e-a16c-760d254d84c3">
<rdf:li resource="http://musicbrainz.org/album/5baf976e-185e-4699-8012-c6f4cec36400">
<rdf:li resource="http://musicbrainz.org/album/8f468f36-8c7e-4fc1-9166-50664d267127">
<rdf:li resource="http://musicbrainz.org/album/e962354a-28f2-44b1-9a26-c8092de4d4f3">
<rdf:li resource="http://musicbrainz.org/album/035d097b-edf9-43fa-a2b9-0282592eb88c">
</rdf:Seq>
</mm:albumList>
</mm:Artist>

</rdf:RDF>
First off, it's a mq:Result object and two properties. The first is mq:status and the second is mq:trackList - which contains an rdf:Seq object. That's a sequenced list in RDF.

Note the use of rdf:resource attributes. Think of this as similar to a HTML href attribute in an A or LINK tag. It's simple pointing to a URL saying "this property's value is url". You don't have to follow the links to use them, an odd concept but one worth exploring.

Next, we have an mm:Track - evidently some kind of song or musical track. It's got it's own properties, which should be self evident, and makes use of the rdf:about attribute.

rdf:resource can only really be used for object property values, and rdf:about is only used on objects.
rdf:about roughly means "This object is about this URI". So, you see the rdf:Seq has a rdf:li property which is talking about a URI. The mm:Track is about that URI, thus it's a list of mm:Track objects.

Have I lost you yet?

Yes, alright, enough piddling about explaining and onto the hacking!

<?php
ini_set('include_path',ini_get('include_path') .':PEAR/:');

require_once 'HTTP/Client.php';
require_once 'HTTP/Request/Listener.php';
require_once 'XML/Serializer/XML_Serializer-0.10.0/Unserializer.php';

/**
* MusicBrainz RDF Query Class
*
* This class makes use of PEAR::HTTP_Request (http://pear.php.net/package/HTTP_Request/)
* and (http://pear.php.net/package/HTTP_Client/)
*
* @author Daniel O'Connor <daniel.oconnor@gmail.com>
*/

class MusicBrainzQuery {

var $req;


/**
* Creates a new MusicBrainzQuery object
*/
function MusicBrainzQuery()
{
$this->req = &new HTTP_Request('http://mm.musicbrainz.org:80/cgi%2dbin/mq%5f2%5f1.pl');
$this->req->setMethod(HTTP_REQUEST_METHOD_POST);
$this->req->addHeader("Content-Type","text/plain");
}

/**
* @access private
*/
function _getHeader()
{

$rdf = '<' . '?xml version="1.0" encoding="iso-8859-1"?' . '>' . "\n";
$rdf .= '<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:mq="http://musicbrainz.org/mm/mq-1.1#"
xmlns:mm="http://musicbrainz.org/mm/mm-2.1#">' . "\n";

return $rdf;
}

/**
* @access private
*/
function _getFooter()
{
return "\n" . '</rdf:RDF>';
}

/**
* Returns
*/
function findArtist($artist, $depth=2)
{

$rdf = $this->_getHeader();

$rdf .= '<mq:findartist>';
$rdf .= '<mq:depth>' . $depth . '</mq:depth>';
$rdf .= '<mq:artistname>' . $artist . '</mq:artistName>';
$rdf .= '</mq:FindArtist>';

$rdf .= $this->_getFooter();

$this->req->addRawPostData($rdf, true);

$this->req->sendRequest();

return new MusicBrainzResult($this->req->getResponseBody());

}

function findTrack($track, $artist="", $album="", $depth=2)
{

$rdf = $this->_getHeader();

$rdf .= '<mq:findtrack>';
$rdf .= '<mq:depth>' . $depth . '</mq:depth>';
if ($artist) {
$rdf .= '<mq:artistname>' . $artist . '</mq:artistName>';
}
if ($album) {
$rdf .= '<mq:albumname>' . $album . '</mq:albumName>';
}
$rdf .= '<mq:trackname>' . $track . '</mq:trackName>';
$rdf .= '</mq:FindTrack>';

$rdf .= $this->_getFooter();

$this->req->addRawPostData($rdf, true);

$this->req->sendRequest();

return new MusicBrainzResult($this->req->getResponseBody());
}

function getCdInfo()
{

}

function findTrmId()
{

}

function findAlbum()
{
$this->client->Post("", $rdf);
}

function query($query)
{
$result = new MusicBrainzResult($rdf);

return $result;
}

}



/**
* MusicBrainzResult class
*
* @author Daniel O'Connor <daniel.oconnor@gmail.com>
*/
class MusicBrainzResult {

var $_rdf;
var $_attributes;
var $_attributeIterator;
var $_attributeIteratorCurrentPosition;
var $_unserializer;

function MusicBrainzResult($rdf)
{
$this->_rdf = $rdf;

$this->_unserializer = &new XML_Unserializer(array('parseAttributes' => true));

$this->_unserializer->unserialize($rdf);

//Parse RDF/XML into arrays
$this->_data = $this->_unserializer->GetUnserializedData();

//$this->_attributeIterator = count($this->_data["mm:Track"]);
$this->_attributeIterator = count($this->_data["mq:Result"]["mm:trackList"]["rdf:Bag"]["rdf:li"]);

for ($i=0;$i<$this->_attributeIterator; $i++) {

//$this->setAttribute("Artist",$this->_data["mm:Track"][$i]["dc:creator"]);
$this->setAttribute("title",$this->_data["mm:Track"][$i]["dc:title"]);
$this->setAttribute("url",$this->_data["mm:Track"][$i]["rdf:about"]);
$this->moveNext();
}

$this->_attributeIteratorCurrentPosition = 0;
}

/**
* Get the value of the Nth property (default 0) as a string.
* If a property exists, rather than overwrite its value it is appended to the list
*
* @param $property String of property name
* @param $value Mixed value of property
* @param $n Set a specific occurance of the property value. Defaults to length of the stack+1;
*/
function setAttribute($property, $value)
{
$n = $this->_attributeIteratorCurrentPosition;

$this->_attributes[$property][$n] = $value;

return true;

}

/**
* Get the value of the Nth property (default 0) as a string.
*
* @param $property String of property name
* @param $n Non Negative Integer specifying which occurance of the property to retrieve.
*/
function getAttribute($property)
{
$n = $this->_attributeIteratorCurrentPosition;
return $this->_attributes[$property][$n];
}

/**
* Return size of stack
*/
function getSize()
{
return $this->_attributeIterator;
}

/**
* Increment Iterator
*/
function moveNext()
{
if ($this->_attributeIteratorCurrentPosition <= $this->getSize()) {
$this->_attributeIteratorCurrentPosition++;
return true;
}

return false;
}


/**
* Decrement Iterator
*/
function movePrev()
{

if ($this->_attributeIteratorCurrentPosition >= 0) {
$this->_attributeIteratorCurrentPosition--;
return true;
}
return false;
}
}

ob_start();
?>

<?php
$music = new MusicBrainzQuery();
//$result = $music->findArtist("Eminem");
//$result = $music->findTrack("Ocean of Emotion","DJ Session One");
$result = $music->findTrack($_GET["title"],$_GET["artist"]);

print '<h1>' . $result->getSize() . ' results for <cite>' . $_GET["title"] . '</cite> by ' . $_GET["artist"] . '</h1>';

while ($result->moveNext()) {
print '<a href="">getAttribute("url") . '">'. $result->getAttribute("title") . '</a><br />';
}
print_r($result->_rdf);
/*
$music->getCdInfo();
$music->findTrmId();
$music->findAlbum();
*/

?>

WHOA! That's a lot of code. It makes use of PEAR's HTTP_Client, HTTP_Request and XML_Serializer classes. It creates two main objects, MusicBrainzQuery and MusicBrainzResult.

You can examine the code and see what it does for yourself. It's fairly straight forward if you know your object oriented stuff.

The basic idea here is to get a Query object that handles all of the RDF/XML for you and a Result object, which returns an array of just strings.

Give it a try.

No comments: