Saturday, November 27, 2004

Faster Data Entry In Forms

A site I use, Audioscrobbler.com, just introduced a moderation system to correct badly tagged artist names. It has a very simple design of yes/no/abstain as your own choices. But, it sucks! Why? Because of the interface.

People have a tendancy to bite off more than they can chew. I see people with 10, 20, 100, up to 1000 open moderations. If I want to vote on them, it sucks to have to use my mouse. Especially since it appears things are rendered exactly wrong enough to make a single press of my "down" arrow move the choices down to within 3 pixels of my click target. But I still have to move my mouse.

Something must be done! And it was. Inspired by Gmail's keyboard accelerators, and talking it over with some of the Audioscrobbler developers (who have too much work on their hands), I tried to push a SOAP speaking TUI written in C#. Alright, overkill, but it would have been a neat hack.

Instead, they wanted javascript keyboard enhancements. And that's what they got from me.

It's pretty simple. After blundering around a while reading documentation I discovered there was no Keyboard Events module in DOM level 2 Events. That's coming in DOM Level 3. Hmm. Well, if Gmail can do it, so can I!

Much googling, and where do we end up? Sometime back in the misty past of 2000. I know, I'm scared too. Now, we find on quirksmode.org some hints.

document.captureEvents( Event.KEYUP );
document.onkeyup = doSomething;

How simple!

function doSomething(e) {
var code;
if (!e) var e = window.event;
if (e.keyCode) code = e.keyCode;
else if (e.which) code = e.which;
var character = String.fromCharCode(code);

switch (character) {
case "Y":
vote(true);
break;
case "N":
vote(false);
break;
case "H":
vote(null);
break;
case "J":
move(1);
break;
case "K":
move(-1);
break;
default:
break;

}
}

That code explains itself. Move back or forwards, or vote true/false/null.

function vote(value) {
var options = rows.item(index).getElementsByTagName("input");
switch (value) {
case true:
options.item(0).checked="checked";
break;
case false:
options.item(1).checked="checked";
break;
case null:
options.item(2).checked="checked";
break;
default:

break;
}
move(1);
}

Vote, change the TR's relevant INPUT element's checked attribute to checked. Move to the next one.

function move(amount) {

if ((index + amount) <>= rows.length) { return; alert(index + "" + amount); }
else {
repaint(index);
index += amount;
highlight(index);
}
}

Move the index pointer thing, which keeps track of which one we are up to. Think of it like an array[index].


function repaint(n) {
try {
rows.item(n).className="row" + (n%2);
}
catch (e) { }
}


Repaint the GUI by swapping it's CSS classname. Use n%2 to get either "0" or "1" for any value of n. (Modulus, or as you may know it, remainder. 7/2 = 3 Remainder 1, (7 % 2) = 1). If you had 3 css classes, n % 3, etc etc etc.


function highlight(n) {
try {
rows.item(n).className="highlight";
}
catch (e) { }
}


Just to opposite! Highlight it.

Simple kiddies. See it all together now...

As always, Creative Commons pwns you. Or doesn't.

Saturday, November 13, 2004

DOAML and PHPList hack

PHPList isn't great software. It's working software. I don't like it because it's not object oriented. Harder to reuse the code - however, easier to hack up. Less stability, but what can you do.

In this hack, we will be adding DOAML information to PHPList - RDF/XML output about the who's who of mailing lists.

Start: 21:40
Grab the latest PHPList. At this time, it's 2.9.3 - which means I'll have to upgrade the installation on the server. Waiting, waiting, backing up, uploading. Ick. So, I'm here to blog about the process.

What we have to do.
  1. Identify the points in the code for database abstraction. There are none at this time really.
  2. Query the database for all mailing lists
  3. Express mailing list information in RDF.
So... Step 1.

We grab the index.php file under /phplist/public_html/lists/ and rip all of the code right out. We cull what we don't need.
Bye bye function()s. You seem only to deal with the forms component of this interface.

11 minutes in. This looks bad, I'm still upgrading old code. I've poked about and removed all but what appears to be needed to get title and description information from the database - untested so far.

Ah hah. We're on. Next, we ditch the page headers and page footers. Extra HTML. Blech. Say byebye to any instance of $data["header"] and $data["footer"].

The fragment we are hunting is "Unsubscribe from our Mailinglists". Get that and we find where all the information is output. Unfortunately, Internationalization sucks for hunting through code sometimes. The URL however, is "?p=unsubscribe". A "find" for that quickly locates the data. Around line 211. Once you've butchered all of the functions located beneath this, it's the last line of code. Hurrah.

Now what do we have? Simple, plain HTML spitting out all of the lists. Snippy snip time some more. Cull all of this crud that handles sessions. I don't know about you but scutters don't give a stuff about cookies. Or logging in. I don't know about you, but I'm down to about 100 lines of php.


<?php
//header("content-type: text/plain");

ob_start();
$er = error_reporting(0); # some ppl have warnings on
if (isset($_SERVER["ConfigFile"]) && is_file($_SERVER["ConfigFile"])) {
print '<!-- using '.$_SERVER["ConfigFile"].'-->'."\n";
include $_SERVER["ConfigFile"];
} elseif (isset($_ENV["CONFIG"]) && is_file($_ENV["CONFIG"])) {
print '<!-- using '.$_ENV["CONFIG"].'-->'."\n";
include $_ENV["CONFIG"];
} elseif (is_file("config/config.php")) {
print '<!-- using config/config.php -->'."\n";
include "config/config.php";
} else {
print "Error, cannot find config file\n";
exit;
}
if (isset($GLOBALS["developer_email"])) {
error_reporting(E_ALL);
} else {
error_reporting($er);
}
require_once dirname(__FILE__).'/admin/'.$GLOBALS["database_module"];
require_once dirname(__FILE__)."/texts/english.inc";
include_once dirname(__FILE__)."/texts/".$GLOBALS["language_module"];
require_once dirname(__FILE__)."/admin/defaultconfig.inc";
require_once dirname(__FILE__).'/admin/connect.php';
include_once dirname(__FILE__)."/admin/languages.php";

if (!isset($_POST) && isset($HTTP_POST_VARS)) {
require "admin/commonlib/lib/oldphp_vars.php";
}

$id = sprintf('%d',$_GET["id"]);

if ($_GET["uid"]) {
$req = Sql_Fetch_Row_Query(sprintf('select subscribepage,id,password,email from %s where uniqid = "%s"',
$tables["user"],$_GET["uid"]));
$id = $req[0];
$userid = $req[1];
$userpassword = $req[2];
$emailcheck = $req[3];
} elseif ($_GET["email"]) {
$req = Sql_Fetch_Row_Query(sprintf('select subscribepage,id,password,email from %s where email = "%s"',
$tables["user"],$_GET["email"]));
$id = $req[0];
$userid = $req[1];
$userpassword = $req[2];
$emailcheck = $req[3];
} else {
$userid = "";
$userpassword = "";
$emailcheck = "";
}
# make sure the subscribe page still exists
$req = Sql_fetch_row_query(sprintf('select id from %s where id = %d',$tables["subscribepage"],$id));
$id = $req[0];
$msg = "";


if (!$id) {
# find the default one:
$id = getConfig("defaultsubscribepage");
# fix the true/false issue
if ($id == "true") $id = 1;
if ($id == "false") $id = 0;
if (!$id) {
# pick a first
$req = Sql_Fetch_row_Query(sprintf('select ID from %s where active',$tables["subscribepage"]));
$id = $req[0];
}
}


if ($login_required && !$_SESSION["userloggedin"] && !$canlogin) {
print LoginPage($id,$userid,$emailcheck,$msg);
} elseif (preg_match("/(\w+)/",$_GET["p"],$regs)) {
if ($id) {
} else {
FileNotFound();
}
} else {
if ($id) $data = PageData($id);
print '<title>'.$GLOBALS["strSubscribeTitle"].'</title>';

$req = Sql_Query(sprintf('select * from %s where active',$tables["subscribepage"]));
if (Sql_Affected_Rows()) {
while ($row = Sql_Fetch_Array($req)) {
$intro = Sql_Fetch_Row_Query(sprintf('select data from %s where id = %d and name = "intro"',$tables["subscribepage_data"],$row["id"]));
print $intro[0];
printf('<p><a href="./?p=subscribe&id=%d">%s</a></p>',$row["id"],$row["title"]);
}
} else {
printf('<p><a href="./?p=subscribe">%s</a></p>',$strSubscribeTitle);
}

printf('<p><a href="./?p=unsubscribe">%s</a></p>',$strUnsubscribeTitle);
}
?>

More hacking. We've gotten our very basic information. Time to change it from HTML to RDF/XML. We also want to pay attention to '$row["title"]' and '$intro[0]' - doaml:title and doaml:description :)

Ok, I can't be stuffed explaining as I hack any more, let's just paste you some code. Notice, we've replaced whereever HTML is output with RDF/XML. We've added in all of the relevant headers, and changed a few things to make it valid XML. And we are now serving it up as text/xml.


<?php
header("content-type: text/xml");
print '<' . '?xml version="1.0" encoding="iso-8859-1"?' . '>' . "\n";
?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dct="http://purl.org/dc/terms/"
xmlns:doaml="http://ns.balbinus.net/doaml#">
<?php
ob_start();
$er = error_reporting(0); # some ppl have warnings on
if (isset($_SERVER["ConfigFile"]) && is_file($_SERVER["ConfigFile"])) {
print '<!-- using '.$_SERVER["ConfigFile"].'-->'."\n";
include $_SERVER["ConfigFile"];
} elseif (isset($_ENV["CONFIG"]) && is_file($_ENV["CONFIG"])) {
print '<!-- using '.$_ENV["CONFIG"].'-->'."\n";
include $_ENV["CONFIG"];
} elseif (is_file("config/config.php")) {
print '<!-- using config/config.php -->'."\n";
include "config/config.php";
} else {
print "Error, cannot find config file\n";
exit;
}
if (isset($GLOBALS["developer_email"])) {
error_reporting(E_ALL);
} else {
error_reporting($er);
}
require_once dirname(__FILE__).'/admin/'.$GLOBALS["database_module"];
require_once dirname(__FILE__)."/texts/english.inc";
include_once dirname(__FILE__)."/texts/".$GLOBALS["language_module"];
require_once dirname(__FILE__)."/admin/defaultconfig.inc";
require_once dirname(__FILE__).'/admin/connect.php';
include_once dirname(__FILE__)."/admin/languages.php";

if (!isset($_POST) && isset($HTTP_POST_VARS)) {
require "admin/commonlib/lib/oldphp_vars.php";
}

$id = sprintf('%d',$_GET["id"]);

if ($_GET["uid"]) {
$req = Sql_Fetch_Row_Query(sprintf('select subscribepage,id,password,email from %s where uniqid = "%s"',
$tables["user"],$_GET["uid"]));
$id = $req[0];
$userid = $req[1];
$userpassword = $req[2];
$emailcheck = $req[3];
} elseif ($_GET["email"]) {
$req = Sql_Fetch_Row_Query(sprintf('select subscribepage,id,password,email from %s where email = "%s"',
$tables["user"],$_GET["email"]));
$id = $req[0];
$userid = $req[1];
$userpassword = $req[2];
$emailcheck = $req[3];
} else {
$userid = "";
$userpassword = "";
$emailcheck = "";
}
# make sure the subscribe page still exists
$req = Sql_fetch_row_query(sprintf('select id from %s where id = %d',$tables["subscribepage"],$id));
$id = $req[0];
$msg = "";


if (!$id) {
# find the default one:
$id = getConfig("defaultsubscribepage");
# fix the true/false issue
if ($id == "true") $id = 1;
if ($id == "false") $id = 0;
if (!$id) {
# pick a first
$req = Sql_Fetch_row_Query(sprintf('select ID from %s where active',$tables["subscribepage"]));
$id = $req[0];
}
}


if ($login_required && !$_SESSION["userloggedin"] && !$canlogin) {
print LoginPage($id,$userid,$emailcheck,$msg);
} elseif (preg_match("/(\w+)/",$_GET["p"],$regs)) {
if ($id) {
} else {
FileNotFound();
}
} else {
if ($id) $data = PageData($id);
$req = Sql_Query(sprintf('select * from %s where active',$tables["subscribepage"]));
if (Sql_Affected_Rows()) {
while ($row = Sql_Fetch_Array($req)) {
$intro = Sql_Fetch_Row_Query(sprintf('select data from %s where id = %d and name = "intro"',$tables["subscribepage_data"],$row["id"]));

print '<doaml:Newsletter rdf:type="http://ns.balbinus.net/doaml#MemberOnlyNewsletter">' . "\n";

printf(' <doaml:description-page rdf:resource="./?p=subscribe&id=%d" />' . "\n",$row["id"]);
print ' <doaml:name>' . $row["title"] . '</doaml:name>' . "\n";
print ' <doaml:topic>' . $row["title"] . '</doaml:topic>' . "\n";
print ' <doaml:description>' . $intro[0] . '</doaml:description>' . "\n";
printf(' <rdfs:seeAlso><rdf:Description rdf:about="./?p=subscribe&id=%d"><dc:title>%s</dc:title></rdf:Description></rdfs:seeAlso>' . "\n",$row["id"],$row["title"]);

print '</doaml:Newsletter>' . "\n";

}
} else {
print '<doaml:Newsletter rdf:type="http://ns.balbinus.net/doaml#MemberOnlyNewsletter">' . "\n";
print ' <doaml:description-page rdf:resource="./?p=subscribe" />' . "\n";
print ' <doaml:name>' . $strSubscribeTitle . '</doaml:name>' . "\n";
printf(' <rdfs:seeAlso><rdf:Description rdf:about="./?p=subscribe"><dc:title>%s</dc:title></rdf:Description></rdfs:seeAlso>' . "\n",$strSubscribeTitle);
print '</doaml:Newsletter>' . "\n";
}

}
?>
</rdf:RDF>
<!--
<doaml:requests rdf:resource="mailto:doaml-interest-request@lists.sourceforge.net" />
<doaml:mbox rdf:resource="mailto:doaml-interest@lists.sourceforge.net" />

<doaml:moderator rdf:nodeID="vincent" />

<doaml:name>DOAML-interest</doaml:name>
<doaml:description>Discussions about DOAML (Description Of A Mailing List)</doaml:description>
<doaml:created>2004-11-10</doaml:created>
<doaml:creator rdf:nodeID="vincent" />
<doaml:topic rdf:resource="http://www.doaml.net/" />

<foaf:Person rdf:nodeID="vincent">
<foaf:name>Vincent Tabard</foaf:name>
<foaf:mbox_sha1sum>ef755f7a687f4a443e47295cc1b3ac3b8c935037</foaf:mbox_sha1sum>
<foaf:homepage rdf:resource="http://www.balbinus.net/" />
<rdfs:seeAlso rdf:resource="http://foaf.balbinus.net/" />
</foaf:Person>
-->

Now we have to peel back one more layer of the orange. We've had a little peek at the code of users.php and felt kind of queasy - this, kids, is why Object Oriented programming is good. You take the mess out of everything. It would take us minutes, if not hours, to follow the convoluted code here only to realise we are looking in the wrong place. Screw it. Directly accessing the database time!


<?php
$req = Sql_Query(sprintf('select * from %s',$tables["user"]));
if (Sql_Affected_Rows()) {
while ($row = Sql_Fetch_Array($req)) {
print_r($row);
}
}
?>


Grab all of the users. All of the information. Blech, it's too much. Grab only the relevant fields - id, email, uniqid. Spit out a sha1 of the mbox for each user, so we have a FOAF IFP (Inverse Functional Property).


<?php
$req = Sql_Query(sprintf('select id, email, uniqid from %s',$tables["user"]));
if (Sql_Affected_Rows()) {
while ($row = Sql_Fetch_Array($req)) {
//print_r($row);
print sha1('mailto:' . $row['email']) . "\n";
//print sha1($row['email']) . "\n";
}
}
?>
Mmm, tasty. Let's step one little bit further.


<?php
$req = Sql_Query(sprintf('select id, email, uniqid from %s',$tables["user"]));
if (Sql_Affected_Rows()) {
while ($row = Sql_Fetch_Array($req)) {
//print_r($row);
print sha1('mailto:' . $row['email']) . "\n";
print $row['id'] . "\n";
//print sha1($row['email']) . "\n";
$query = Sql_Query(sprintf("select listid, userid from %s WHERE userid = '" . $row["id"] . "'",$tables["listuser"]));
if (Sql_Affected_Rows()) {
while ($rows = Sql_Fetch_Array($query)) {
print_r($rows);
}
}
}
}
?>
So now, we are spitting out a list of every user, and all of the lists they are subscribed to. Useful, we'll comment that out however and save it for when we wish to examine one person in particular by their sha1 hash.

So what's next? Take the above code, and reverse the order of the queries - select all users subscribed to a particular list.

Clean it all up, stick it into functions.

Ah, screw it. I'm already finished.

Thursday, November 11, 2004

The latest ongoing hack

Update: it's called google earth.
Imagine: You are looking to buy a house. You want to be able to look inside of the house. You want to be able to search for things you like and things you do not like in a house.

Current Solutions:
Hodge podge realestate sites. A few inside photos taken with a digital camera and posted on the web. A price tag, an agent summary of the house.

Why this is bad:
Agents lie. You often have to do a physical inspection of a property to get a feel for it. It's a waste of your time and day.

Proposed Solution:
Semantic Web Friendly X3D renderings of houses. Ontology to describe housing attributes. X3D model tools to combine digital photos into a 3d model with a wireframing process. You can then "float" through a house, from room to room and get an idea of each object.

Can be also used in VRML type things, and could be ported to games (ie, in C++). Real world games.

Starting Points:
X3D - successor to VRML, which died in the arse. No killer app. No browser support. Better yet, X3D is open. They have an Open Source browser written in Java. PERFECT.


So, we'll start with a snapshot of the codebase. 10 Oct 2004:

Current progress - just installed. Future updates will track this progress.

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.