By now you should have a good grasp of how to identify a good PEAR package to use in your project, how to install it and any of its dependencies, and how to incorporate it into your own code. Let's put that knowledge to work to build a working application using a single PEAR component and a few more common built-in PHP routines.
Try it Out: Build an Application Using a Single PEAR Component
The application you're going to write is designed for use on a personal Web site to show site visitors what kind music you've been listening to lately. It works by ferreting through your MP3 collection. Many people collect MP3s—it is still by far the most popular format for encoding music for computer-based listening despite proprietary offerings from some enormous industry players—many people have MP3 collections running to thousands of songs.
This particular bit of PHP is designed to be run on your desktop computer simply because it needs access to wherever you keep your MP3 files for day-to-day listening. This may not be practical for any number of reasons, but as a working example of a stand-alone application, it will suffice.
Let's assume that you keep your MP3s in C:\MP3—if that isn't the case (or you're running under UNIX), you can change the $MY_MP3_DIR constant in the code.
The PEAR Package: MP3_ID
The MP3_ID package is used to read what is known as the IDv3 Tag information from an MP3 file. This tag contains information on the song's genre, date, and country of origin, and much more. The tag is present in every MP3 file, but may be blank—it all depends on what was used to produce the MP3 in the first place. For the purposes of this project, assume all your MP3s are properly tagged up!
The package provides methods for both reading and writing the IDv3 Tags, but in this example you are only going to be using its read methods to discover certain information about the MP3 files in your collection—namely the artist and title of the song.
It is installed in the usual manner:
root@genesis:~# pear install MP3_ID downloading MP3_Id-1.0.tgz ... ...done: 7,517 bytes install ok: MP3_Id 1.0 root@genesis:~#
The Application
You'll build this application as a single PHP page that, when accessed, produces a list of the top five MP3 files in the directory, ordered by the last time each file was accessed (which should be, in theory, the same order they were last played). If more than five files are found in the directory, only the first five will be shown.
Create a PHP file named mp3id.php, and place it where your Web server can see it. Enter the following code in that file. Don't forget to change the constant $MY_MP3_DIR to the path to where your MP3s are actually kept (make sure there are some MP3s in that folder). Don't worry if anything doesn't make sense—we'll pick it apart straight after.
<?php require_once 'MP3/Id.php'; static $MY__MP3_DIR = "/home/ed/mp3"; # Change this to your MP3 directory! $objDir = dir($MY _MP3_DIR); $intNumFiles = 0; $arFileTimeHash = Array(); # Loop through all the files we've found while (false !== ($strEntry = $objDir->read())) { # Check this is actually an MP3 file, and not a directory entry or similar! if (eregi("\.mp3$", $strEntry)) { $arFileTimeHash[$strEntry] = fileatime($MY_MP3_DIR . "/" . $strEntry); $intNumFiles++; }; }; # Now sort into order of date accessed and put into a traditional array structure for display later arsort ($arFileTimeHash); $arFileList = Array(); $intThisArrayIndex = 0; foreach ($arFileTimeHash as $strFilename => $intAccessTime) { $arFileList[$intThisArrayIndex]["FILENAME"] = $strFilename; $arFileList[$intThisArrayIndex]["ACCESSED"] = $intAccessTime; $intThisArrayIndex ++; }; # If we found more than 5 MP3s, just show the first 5 if ($intNumFiles > 5) { $intNumFiles = 5; }; ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>My MP3 Collection</title> </head> <body> <H1>My MP3 Collection</H1> <HR> Here's the top 5 songs I've been listening to lately! <BR><BR> <table border="1" cellpadding="3" cellspacing="3"> <tr> <td>Position</td> <td>Artist</td> <td>Name</td> <td>Last listened to on:</td> </tr> <?php $objMP3ID = new MP3_Id(); for ($i = 0; $i<=($intNumFiles)-1; $i++) { ?> <tr> <?php $strThisFile = $arFileList[$i]["FILENAME"]; $strPath = $MY_MP3_DIR . "/" . $strThisFile; $intResull: = $objMP3ID->read ($strPath); $strArtist = $objMP3ID->getTag ("artists", "Unknown Artist"); $sLrName = $objMF3ID->geLTag("name", "unknown Track"); $intAcccessTime = $arFileList [$i] ["ACCESSED"]; ?> <td><?=$i+1?></td> <td><?=$strArtist?></td> <td><?=$strName?></td> <td><?=date("m/d/Y II:i", $intAccessTime) ?></td> </tr> <?php }; ?> </body> </html>
Fire up your Web browser, and point it at the PHP you've just created.
How it Works
Just like the HTML_TreeMenu example, this little application does the bulk of the clever stuff right at the top, and then displays the information when it is needed, neatly embedded within the HTML. MP3_ID is neat in that it has no real dependencies; it's very much plug'n'go.
The first line—the require line—loads the module:
require_once 'MP3/Id.php';
You don't instantiate it because you don't need it yet.
The first job is to traverse the MP3 directory and build up an associative array of every MP3 file in it along with its access time:
static $strMyMP3Directory = "/home/ed/mp3";
$objDir = dir($strMyMP3Directory);
$intNumFiles = 0;
$arFileTimeHash = Array();
# Loop through all the files we've found
while (false !== ($strEntry = $objDir->read())) {
# Check this is actually an MP3 file, and not a directory entry or similar!
if (eregi("\.mp3$", $strEntry)) {
$arFileTimeHash[$strEntry] = fileatime($strMyMP3Directory . "/"
. $strEntry);
$intNumFiles++;
};
};
The helpful Dir object lets you traverse your directory of choice. Unfortunately, it doesn't enable you to filter its output. When listing the contents of the directory, there's a good chance that you're going to get a bunch of results you don't want.
The workaround is to pass every result you get back from the Dir object through a regular expression. Regular expressions are a huge topic and handled briefly back in Chapter 5. The regular expression used here is .mp3$, which is passed into the case-insensitive regular expression match check method eregi that checks whether the entry ends with the four-character string .mp3. If it does, you can be fairly sure it's an MP3 file and include it in your associative array.
The associative array is keyed with the filename (minus the path) of each file, the value is the time that file was last accessed, which is retrieved by using the fileatime function. The value of $intNumFiles is incremented with each file successfully found, and will be of use to you later.
With the associative array intact, you can make use of PHP's arsort function, which sorts an associative array, in reverse, by the values of its key-value pairs:
# Now sort into order of date accessed and put into a traditional array
structure for display later
arsort($arFileTimeHash);
In this example, the key is the filename, and the value is the access time, so in effect, you're sorting the array by most-recently-accessed first.
You translate the sorted array into a traditional numbered array, each component of which is an associative array in its own right, with just two keys: FILENAME (the name of the file) and ACCESSED (when it was accessed):
$intThisArrayIndex = 0;
foreach ($arFileTimeHash as $strFilename => $intAccessTime)
{
$arFileList[$intThisArrayIndex]["FILENAME"] = $strFilename;
$arFileList[$intThisArrayIndex]["ACCESSED"] = $intAccessTime;
$intThisArrayIndex ++;
};
This step isn't strictly necessary—it just makes the logic a bit easier to follow when it comes to display the files.
Finally, you add a simple rule to limit our output—if there are more than five MP3s in the array, show only the first 5, if there are fewer than five, show all of them:
# If we found more than 5 MP3s, just show the first 5
if ($intNumFiles > 5)
{
$intNumFiles = 5;
};
Now you're ready to kick off the HTML. With a table in place to display the results, you cycle through your array from 0 (the first MP3) to the value of intNumFiles—the number of files you determined earlier. Of course, because you start counting at zero, you deduct one from the total to work out your upper limit.
With each cycle, you make use of the instantiated MP3_ID class by feeding it a filename to process, and extracting the "artists" and "name" tags from the MP3, using the read and getTag methods, respectively:
<?php
$objMP3ID = new MP3_Id();
for ($i = 0; $i<=($intNumFiles)-1; $i++)
{
?>
<tr>
<?php
$strThisFile = $arFileList[$i]["FILENAME"];
$strPath = $strMyMP3Directory . "/" . $strThisFile;
$intResult = $objMP3ID->read ($strPath);
$strArtist = $objMP3ID->getTag ("artists", "Unknown Artist");
$strName = $objMP3ID->getTag ("name", "Unknown Track");
$intAccessTime = $arFileList[$i]["ACCESSED"];
?>
<td><?=$i+1?></td>
<td><?=$strArtist?></td>
<td><?=$strName?></td>
<td><?=date("m/d/Y H:i", $intAccessTime)?></td>
</tr>
<?php
};
?>
Note that you display the access time here, too. That information isn't derived from the IDv3 tag of the file, but from the file list array itself. You retrieve and record this information when you actually perform the directory listing, and you can simply refer back to it here.
You display the access time, which was presented as an "epoch timestamp" (a value in seconds relative to January 1, 1970) in a more friendly format using PHP's excellent date function.
And so, in a few simple steps, you created an original diversion for your home page (and it's certainly a bit more engaging than "Sign my guestbook!").
Spotted a Problem?
Some of you may have experienced a small problem with this lovely application. Run it once, and all works fine. Run it a second time (hit Refresh), and the "last listened to" column seems strangely ... recent.
It's not a bug, but more of an accidental feature. By reading the ID tag of the MP3, you are in fact—you guessed it—accessing the file. By accessing the file, you are updating its last accessed attribute on the file system. As a result, when you next go to run your app, the last accessed property of your MP3 reflects the last time you ran the script, not the last time you listened to the MP3.
There is a clever way around this, using a native PHP function called touch. The principle is that after accessing the file to retrieve its IDv3 contents, you immediately use the touch function to set its access time back to what it was previously. If you feel strongly about access times, and want to experiment adding this functionality as an exercise, please do so.
Unfortunately, this method only works if the "owner" of the MP3 files is the same as the "owner" of the Web server process, which is rarely the case in a production environment.
A more sophisticated workaround involves periodically indexing your MP3 files into a database, so that it isn't necessary to read the IDv3 tags every time somebody hits the Web page; instead, the database can be consulted, leaving the access time of the file intact. Again, this is left as an exercise!
A file's access time is a property that is offered to PHP by the operating system. Depending on the operating system you're running, and the file system with which your server's disk is formatted, this property may not be available to PHP. In that case, the operating system may offer up the last modified time instead. This means the data returned by the script in such cases will not be wholly accurate, so if you are planning to deploy this onto a real-world production environment, you may want to sanity check the results you get before you open it up to general access.
Building an Application using Two PEAR Components
Let's put all your PEAR skills together now, and build a fresh application from the ground up. Let's see if you can combine the use of the two PEAR components you've met so far—HTML_TreeMenu and MP3_ID—to build a genuinely useful application.
The Application
Some of the more enlightened Internet radio stations these days, as well as traditional broadcast radio stations with an online presence, offer some facility for its listeners to request music via the station's Web site.
With a limited play list, it would be unwise for a station to allow a free-text request form because many of its listeners' requests would have to go unfulfilled. Generally speaking, such stations would normally want to offer a list of all tracks in the station archive, and let the listener to choose from that list.
Constant new additions to that archive, which is normally made up of MP3 files, could make maintaining the request part of the Web site pretty tricky. As a result, some means for the request list to be generated dynamically and be arranged in some sensible manner would be just swell.
This application permits just that. It presents all the MP3 files in a server's directory in a hierarchical list (using the HTML_TreeMenu PEAR component), ordered by artist. It enables the submitting user to click a particular song to request it, and then render an e-mail to the DJ of the radio station instructing him to play that file.
You'll use PHP's built-in mail() function to send this e-mail. This is a very simple way to send e-mail, and in most real-world applications, you'll need something a little chunkier. In the next chapter, you'll hook up with some PEAR classes that can help you out.
Because you've used XML to drive the contents of the output of the HTML_TreeMenu component so far, let's stick with that method. You'll still need to use the helper class, however, to turn the XML into PHP methods that HTML_TreeMenu can understand.
Architecture
The application can be divided into two distinct PHP files, just to make things a bit neater. The first, radiogeneratexml.php, generates an XML representation of the MP3 folder on the server, organized and ordered by artist. The second, radiorequest.php, actually displays that list, and handles any form submissions by the user.
The XML gets from one script to the other by means of an HTTP request. Yes, your script will cause your server to make a request from itself! If you've ever played with the idea of XML-based Web services, this might not seem so crazy. The idea is to allow the feed of XML data to come from practically any source, in practically any location. For example, in a real live radio station you might use one Web server to serve the public Web site, but a totally separate server to hold the MP3 files. The server holding the MP3s might not even be accessible to the outside world. Instead of trying to read over any kind of network share, which is incredibly inefficient, you could place radiogeneratexml.php on the server with the MP3s, and keep radiorequest.php on the public Web server, and tell it to request the XML straight off the other server.
Should you ever decide to extend your application so that third-party Web sites could access the data, you just might make that XML feed public.
The XML, for the purposes of this application, conforms to the standards required by the HTML_TreeMenu component, which you have already seen. With the possibility of some kind of secondary use of the XML feed in mind, it may be wise to consider, as a project for a rainy day, upgrading the application to produce XML in a more service-driven than display-driven format. You could then use a simple XSL style sheet to convert the service-driven XML into XML that the PEAR component could understand.
Generating the XML
radiogeneratexml.php not only is in charge of producing XML that can be used by the make-a-request page, but also is in charge of reading the MP3 directory, extracting the useful artist and title data from each MP3 file, and then grouping and sorting the list of MP3s in an appropriate way.
Let's just quickly review the kind of XML you want to produce for the HTML_TreeMenu component. Take a look at this example output:
<?xml version="1.0"?> <treemenu> <node text="Extreme Metal Grinding" icon="folder.gif"> <node text="Extreme Noises" icon="document.gif" link="radiorequest.php?requestfile=9991885931034.mp3" /> <node text="Extreme Temper Loss" icon="document.gif" link="radiorequest.php?requestfile=9991885931035.mp3" /> </node> <node text="Massive Pitch Correction" icon="folder.gif"> <node text="How long ago" icon="document.gif" link="radiorequest.php?requestfile=9991885931036.mp3" /> <node text="Ten minutes to Sunrise" icon="document.gif" link="radiorequest.php?requestfile=9991885931037.mp3" /> </node> </treemenu>
In this output, you can see that two artists have been identified in the MP3 collection: "Extreme Metal Grinding" and "Massive Pitch Correction" (this isn't the greatest radio station in the world). The available songs by each of these two fine artists have been placed under their nodes in the hierarchy.
The name of each song is the caption for the link itself—to radiorequest.php—passing a GET parameter called requestFile. The actual MP3 filename is passed because it is of more use to the DJ than the information from the IDv3 tag of the file. PHP's urlencode() function will make sure the MP3 filename is URL-friendly before it's passed, of course.
If you want to see what this will look like when rendered by HTML_TreeMenu, use your code from the first "Try It Out" example in this chapter and paste this XML into treemenutest.xml. You then need to modify treemenutest.php to look at the correct XML file:
$objXMLTree = new XMLHTMLTree("treemenutest.xml");
Now we're happy with the output of our request page (we'll probably want to fiddle with surrounding HTML a bit, though), we just have to figure out how we're going to produce some similar output—dynamically, of course—from the contents of our MP3 collection folder.
radiogeneratexml.php
Create a new file, insert the following code, and save it as radiogeneratexml.php. This particular tier of the application generates the XML for your tree navigation component automatically from the MP3 folder on your own computer. Place it on your server where you can reach it easily.
<?php header("Content-Type: to xt/xml\n\n"); require_once 'MP3/Id.php'; static $strMyMP3Directory = " /home/ed/mp3"; $objDir = dir ($strMyMP3Directory); $intNumFiles = 0; $arMP3Files = Array(); // Loop through all the files we've found and put them into an array while (false !== ($strEntry = $objDir->read())) { // Check this is actually an MP3 file, and not a directory entry or similar! if (eregi ("\.mp3$" , $strEntry)) { $arMP3Files[] = $strMyMP3Directory . "/" . $strEntry; }; }; // Instantiate our MP3, ID Class $objMP3ID = new MP3_Id(); // Set up an array of unique artists $arArtists = Array(); $arTestArtists = Array(); for ($i=0; $i<=sizeof ($arMP3Files) -1; $i++) { $strPath = $arMP3Files[$i]; $intResult = $objMP3ID->read ($strPath); $strArtist = $objMP3ID->getTag ("artists", "Unknown Artist"); $strTestArtist = strtoupper(preg_replace("/[^A-Za-z0-9] /", "", SstrArtist)); // Check to see if this artist (when uncommon characters are made irrelevant and all letters are capitalized) is in our array of artists - if not, add them if (in_array($strTestArtist, $arTestArtists) == false) { array_push($arArtists, $strArtist); // Note we use the original spacing and capitalization for our addition to $arArtists ... array_push($arTestArtists, $strTestArtist); }; }; // For each artist, create an array containing all the song filenames written by that artist, and their titles $arTracks = Array(); // We'll also create an array of all the song indices we've already accounted for, to save reading their details more than once $arAlreadyAccountedForSongIndices = Array(); for ($i=0; $i<=sizeof($arArtists)-1; $i++) { $strArtistName = $arArtists[$i]; $strTestArtistName = $arTestArtists[$i]; $arTracks[$strArtistName] = Array(); // See which songs are written by this artist for ($j=0; $j<=sizeof($arMP3Files)-1; $j++) { if (in_array($j, $arAlreadyAccountedForSongIndices) == false) { $strPath = $arMP3Files[$j]; $intResult = $objMP3ID->read ($strPath); $strThisArtist = $objMP3ID->getTag ("artists", "Unknown Artist"); $strThisTestArtist = strtoupper(preg_replace("/[^A-'Za-z0-9]/", "", $strThisArtist)); if ($strThisTestArtist == $strTestArtistName) { // This song is indeed by the artist we're testing, so slap its index into $arAlreadyAccountedForSongIndices so we won't ever test it again array_push($arAlreadyAccountedForSongIndices, $j); // Get its title and request link and push them into a temporary hash $arSongHash["TITLE"] = $objMP3ID->getTag ("name", "Unknown Title"); $strSongFilename = str_replace("$strMyMP3Directory" . "/", "", $arMP3Piles[$j]); // Create a link based on the filename by making the filename URL friendly using urlencode $arSongHash["LINK"] = "radiorequest.php?requestfile=" . urlencode($strSongFilename); // Push this hash as the result of the next available index of $arTracks[name of artist] array_push ($arTracks [$strArtistName] , $arSongHash); }; }; }; }; // Sort this hash by artist name, ascending A-Z - use ksort rather than asort to sort by key ksort ($arTracks); // Now output this in the appropriate XML format ?> <treemenu> <?php foreach ($arTracks as $artist_name => $arHash) { ?> <node text=" <?=$artist_name?>" icon="folder.git"> <?php for ($i=0; $i<=sizeof ($arHash)-1; $i++) { ?> <node text="<?=htmlentities($arHash[$i]["TITLE"])?>" icon="document.gif" link="<?=htmlentities($arHash[$i]["LINK"])?>" /> <?php }; ?> </node> <?php }; ?> </treemenu>
radiorequest.php
Create a new file and enter the following code. Save it as radiogeneratexml.php and put it in the same folder on your server as radiogeneratexml.php. This script displays the tree navigation of the MP3 tracks available to request generated by radiogeneratexml.php, and handles the requests submitted by the user.
<?php static $strMyMP3Directory = "/home/ed/mp3"; static $strMyDJsEmailAddress = "ed@example.com"; // Require our necessary PEAR objects require_once('HTML/TreeMenu.php'); require_once('xmlhtmltree.phpm'); // Define XML URL for retrieval and retrieve it - you can modify this if you have the XML generator on another server! $<strXMLURL = "http://" . $_SERVER ["SERVER_NAME"] , str_replace("request", "generatexml", $_SERVER["SCRIPT_NAME"]}; $strXML = implode (false, file{$strXMLURL)); $objXMLTree = new XMLHTMLTree(" ", $strXML); $objXMLTree->GenerateHandOffs(); $objXMLTree->ParseXML(); SobjTreeMenu = $objXMLTree->GetTreeHandoff(); // Let's see if we've made a request - if we do, we should alter our output slightly $requestMade = false; $requestSuccessful = false; if (!empty($_GET['requestfile'})) { $requestMade = true; // Get the filename $strRequestFilename = $_GET['requestfile']; // Check this file actually exists $strFullPath = $strMyMP3Directory . "/" . $strRequestFilename; } if (@filesize($strFullPath) > 0) { $requestSuccessful = true; // It's all worked - let's email the DJ mail($strMyDJsEmailAddress, "New song request", "A request has been made for: " . $strFullPath); } else { $requestSuccessful = false; } ?> <HTML> <HEAD> <SCRIPT LANGUAGE="JavaScript" SRC="TreeMenu.js"></SCRIPT> </HEAD> <BODY> <H1>Radio PHP</H1> <?php if ($requestMade) ( ?> <B>Thanks for your request!</B> <BR><BR> <?php if ($requestSuccessful) { ?> You'll be pleased to hear your request was successful, and we'll try and play your song as soon as we can. <?php } else { ?> Unfortunately we weren't able to play the song you requested. We may have just recently removed it from our collection. Please feel free to try again! <?php }; ?> <BR><BR> <?php }; ?> <?php if (!($requestSuccessful)) { ?> Request your song from our list below and it will be emailed to our DJ - if we've not played it too recently we'll try and incorporate it for you as soon as we can! <HR> <?php $objTreeMenu->printMenu(); ?> <?php }; ?> </BODY> </HTML>
How it Works: radiogeneratexml.php
Now you've seen the source in full, so let's now go through radiogeneratexml.php step by step.
Strictly speaking, you're sending XML, not HTML, to the client, so you should tell the client to expect it (not that PHP's built-in HTTP request object really cares) by using the correct Content Type HTTP header:
It's very important that no white space outside of delimiting <?php and ?> tags comes before this header, because the second you incorporate any non-PHP code—even a space or carriage return—PHP helpfully sends a text/html content type header, and you're committed to using it.
Then, include your PEAR class (MP3_ID):
require_once 'MP3/Id.php';
In this file you're simply outputting plain XML so you don't need to include the HTML_TreeMenu class.
Now let's start listing the contents of your MP3 folder. Change the constant to reflect wherever you keep your MP3s (for this example to work, you need to have at least three in there). Then use a slightly modified version of your code from the earlier MP3_ID project to create a one-dimensional array of all the MP3s—by filename—in that folder:
static $strMyMP3Directory = "/home/ed/mp3"; # Change this to your MP3 directory!
$objDir = dir($strMyMP3Directory);
$intNumFiles = 0;
$arMP3Files = Array();
while (false !== ($strEntry = $objDir->read())) {
if (eregi("\.mp3$", $strEntry)) {
$arMP3Files[] = $strMyMP3Directory . "/" . $strEntry;
};
};
So far, so good.
You're ready to put your linear collection of MP3 files to use. Because the physical structure of what you're creating is going to be on an artist-by-artist basis, it makes sense to try to amass a single array of all the individual artists that comprise your MP3 collection. There may be thousands of files in there, but with a bit of luck, there should only a hundred or so distinct artists.
Retrieving the artist name of each file is your first use of the MP3_ID class:
$objMP3ID = new MP3_Id();
$arArtists = Array();
$arTestArtists = Array();
for ($i=0; $i<=sizeof($arMP3Files)-1; $i++) {
$strPath = $arMP3Files[$i];
$intResult = $objMP3ID->read ($strPath);
$strArtist = $objMP3ID->getTag ("artists", "Unknown Artist");
$strTestArtist = strtoupper(preg_replace("/[^A-Za-z0-9]/", "",
$strArtist));
if (in_array($strTestArtist, $arTestArtists) == false) {
array_push($arArtists, $strArtist);
array_push($arTestArtists, $strTestArtist);
};
};
Note that you create two arrays —arArtists, which holds the actual artist names, and arTestArtists, which holds a mangled version of each artist's name, whereby we have made it all-capitals, and stripped out any characters (including spaces) other than A-Z and 0-9. You do this for a very good reason: you want to make certain each MP3 file's artist appears only once. By stripping away spaces and punctuation, and standardizing capitalization, you can test the artist name extracted from the IDv3 tag against those we have already witnessed in the lifespan of the loop, with a great deal of certainty.
Consider the fictional artist Extreme Metal Grinding for a moment. Let's say that you have three MP3 files purportedly by this artist, which have been encoded on three separate dates, by three separate pieces of software. The artist could easily be represented by three slightly different ways in the tag, such as Extreme Metal Grinding, EXTREME 'Metal' Grinding, and Extreme Metalgrinding. By performing the regular expression and conversion to uppercase in the statement $strTestArtist = strtoupper (preg_replace("/[^A-Za-z0-9]/", "", $strArtist)) on each of these tags, you always arrive at the same result: EXTREMEMETALGRINDING. This may not look too nice on screen, but it's great to test against to establish membership of this particular group of artists.
So you loop through each MP3 file, retrieving the artist tag and applying the regular expression to it. You check to see whether this mangled artist name appears in your test array. If it doesn't, you add to both the test array (using the mangled artist name) and the real artist array (using the unsullied artist name). If it does, there's no need to add it again.
Now you get to the clever bit, which is where you push every song you've captured into a single hash. The output you want to create is going to be keyed by unique artists, so it makes sense to have an associative array with those artist names as keys. In terms of what to use as a value, there will almost certainly be more than one song by each artist, so the value of your hash is another linear array, with each slot in the array representing a single song. Because you want to store both the actual title and the filename of each song, you split each slot using another hash. The resulting data structure is quite complex (take a look with var_dump() if you're curious), but it is perfect for producing XML, and it makes the code in radiorequest.php very simple indeed.
You loop through each of the artists you established in the array you have just created, as shown in the following code:
$arTracks = Array();
$arAlreadyAccountedForSongIndices = Array();
for ($i=0; $i<=sizeof($arArtists)-1; $i++) {
$strArtistName = $arArtists[$i];
$strTestArtistName = $arTestArtists[$i];
$arTracks[$strArtistName] = Array();
for ($j=0; $j<=sizeof($arMP3Files)-1; $j++) {
if (in_array($j, $arAlreadyAccountedForSongIndices) == false) {
$strPath = $arMP3Files[$j];
$intResult = $objMP3ID->read ($strPath);
$strThisArtist = $objMP3ID->getTag ("artists", "Unknown Artist");
$strThisTestArtist = strtoupper(preg_replace("/[^A-Za-z0-9]/", "",
$strThisArtist));
if ($strThisTestArtist == $strTestArtistName) {
array_push($arAlreadyAccountedForSongIndices, $j);
$arSongHash["TITLE"] = $objMP3ID->getTag("name", "UnknownTitle");
$strSongFilename = str_replace("$strMyMP3Directory" . "/", "",
$arMP3Files[$j]);
$arSongHash["LINK"] = "radiorequest.php?requestfile=" .
urlencode($strSongFilename);
array_push($arTracks[$strArtistName], $arSongHash);
};
};
};
};
Essentially, you loop through your list of artists (the proper list instead of the test list) and establish a key in your hash based on the artist name.
Then for each key, you loop through your entire list of MP3s and test to see whether each MP3 is authored by that artist. You use the artist name held at the corresponding index of your test list, and test it against the artist name of the MP3 file, passing it through the regular expression first. Again, this means that deviations in spacing, punctuation, and capitalization are ignored.
Because you're looping through the entire collection to determine what each artist has and hasn't written, this script could become quite resource-intensive, even slow. So a clever feature is introduced to improve performance. Each index of the MP3 array ($arMP3Files) needs to be linked to an artist just once—after all, an MP3 can be written only by a single artist—at least insofar as an IDv3 tag is concerned. A check is made of the index you're searching, and if it's already linked to an artist, there's no point wasting time on that index. As soon as you positively establish a given MP3 file (at index j in your original $arMP3Files array), you add that index j to a further array, called $arAlreadyAccountedForSongIndices. The result is that you can choose not to test that MP3 file on subsequent iterations if you've already established that it's been written by a given artist. This greatly increases performance by limiting the number of IDv3 reads that have to be performed throughout the script.
If the artist name of the track you're testing matches the artist whose authorship you're currently assessing, you add a suitable hash representing that track to the array of the current artist's key of the hash. The hash that you add consists of the title of the song, and a link to radiorequest.php offering its filename (less its path), the idea being that when that link is clicked, the user is positively asserting that she wants to hear that song. You use the urlencode function to ensure that the URL you generate is Web-browser friendly.
Finally, you use ksort to sort the array by its keys into alphabetical order. Much like arsort, which you used in the previous MP3_ID application, this sort maintains the intrinsic link between each key in an associative array and its value.
ksort($arTracks);
With your data structure in place, it's time to output it as XML.
Because the XML you're producing is incredibly simple, you won't take advantage of PHP5's built-in libxml-provided XML support to produce it. Instead, you're going to cheat and code it just as if it were HTML. If you feel strongly about this, feel free to rewrite the relevant routine to use libxml2 (see Chapter 8).
Quite simply, you recurse through the structure and output directly to the browser. Justification for the seemingly awkward data structure in the previous block of code should now be evident, in that you can convert your data structure to XML in just a few lines of code. In effect, you've made life slightly more complicated for yourself so that life can be slightly easier in producing your XML. This is a good thing; should you need to tweak the XML later for some reason, because you can easily do so without disrupting many lines of complex logic.
The following code prints the XML to the browser:
<treemenu>
<? foreach ($arTracks as $artist_name => $arHash) { ?>
<node text="<?=$artist_name?>" icon="folder.gif">
<? for ($i=0; $i<=sizeof($arHash)-1; $i++) { ?>
<node text="<?=htmlentities($arHash[$i]["TITLE"])?>" icon="document.gif"
link="<?=htmlentities($arHash[$i]["LINK"])?>" />
<? }; ?>
</node>
<? }; ?>
</treemenu>
You use htmlentities when rendering both values (title and link) to ensure that you are fully XML-compliant. This might screw up the link URL in literal terms, but the parser in the HTMLTree_Menu component will fix this for you.
If you like, go ahead and request radiogeneratexml.php using your Web browser. If you want to force Internet Explorer to render it as XML (which makes it prettier to read, and lets you expand and collapse individual nodes) you can use the following URL trick:
http://your_server_name/radiogeneratexml.php/.xml
The Web server ignores the extraneous /.xml at the end, but Internet Explorer thinks you're definitely requesting an XML page, and so does its pretty rendering. (Quite why it doesn't just look at the MIME header that you took so much trouble to issue at the start of your script is beyond me.)
If you want to be really sure things are working, you can copy and paste the XML output here into treemenutest.xml and check out treemenutest.php again. To retrieve it, simply open the preceding URL in your own Web browser. You should see XML rendered in your browser window, and you should be able to pick out some of your own MP3 files within the XML data structure. If all looks good, you're more than half way there.
How it Works: radiorequest.php
radiorequest.php is by far the simpler of the two files. If you can get your head around how radiogeneratexml.php works, you'll probably be fine here.
Let's go through it together just to be on the safe side.
You kick off with some constants:
<?php
static $strMyMP3Directory = "/home/ed/mp3"; # Change to your MP3 dir!
static $strMyDJsEmailAddress = "ed@example.com";
?>
You should probably replace the e-mail address with something sensible here, and insert the directory on your Web server where you keep your MP3s, as you did in the XML generation script.
If you really don't have any MP3s anywhere, and you'd like to download some to play with (within copyright limitations, of course) check out www.iuma.com. If you want to play with the MP3s' IDv3 tags, download Winamp (www.winamp.com), which is an MP3 player that also enables you to edit the IDv3 tags of the songs in your play list.
Next, you include your PEAR objects. No need to mess with MP3_ID here—just the HTML-Treemenu and helper components:
require_once('HTML/TreeMenu.php');
require_once('xmlhtmltree.phpm');
You use some trickery to work out the URL of the XML generator script. In the example script, you can assume it's on your own server, in the same folder as wherever you saved the generator script. You know the URL of the request script, because it's being called by the user. The correct URL for the generator script is going to be identical to that, except the word request will be replaced with generatexml. You use a bit of search and replace to make that word change, and get the URL of your generator script as a result.
Once you have the URL, you retrieve its contents just as you would a file on your own file system—PHP is smart enough to request the URL for you. You then feed the output XML you receive straight into the HTML_TreeMenu object just as you did at the beginning of the chapter. Here's a code snippet demonstrating this:
$strXMLURL = "http://" . $_SERVER["SERVER_NAME"] . str_replace("request",
"generatexml", $_SERVER["SCRIPT_NAME"]);
$strXML = implode ('' , file($strXMLURL));
$objXMLTree = new XMLHTMLTree("", $strXML);
$objXMLTree->GenerateHandOffs();
$objXMLTree->ParseXML();
$objTreeMenu = $objXMLTree->GetTreeHandoff();
Next, you include the conditional logic telling the script what to do if you are passed a chosen song to request. Remember in our output XML, how we generated links for our tree that looked like:
/radiorequest.php?requestfile=9991885931037.mp3
The radiorequest.php script actually fulfills two functions: it displays the tree of MP3 files, and it displays your "thank you" message if the request is successful. You must account for requests for this PHP script that include the parameter requestfile, which specifies the filename of the MP3 being requested. Not only do you want to know if a request has been made, but you want to know if that request was valid. You do this through a fairly cursory check to see whether the file exists:
$requestMade = false;
$requestSuccessful = false;
if (!empty($_GET['requestfile']))
{
$requestMade = true;
$strRequestFilename = $_GET["requestfile"];
$strFullPath = $strMyMP3Directory . "/" . $strRequestFilename;
if (@filesize($strFullPath) > 0) {
Notice that you test for the existence of the file by calculating what its full path would be were you to open it, and then using the filesize function to test its size. A size greater than one indicates that the file does exist. You use the @ operator because PHP considers a nonexistent file in calls to filesize to be a critical error, worthy of ceasing script execution—which, of course, isn't something you particularly want to happen.
If the file exists, you mark the request as successful and go ahead and e-mail the DJ, using the constant you defined at the top of the file as the target e-mail address:
$requestSuccessful = true;
// It's all worked - let's email the DJ
mail($strMyDJsEmailAddress, "New song request", "A request has been
made for: " .
$strFullPath);
} else {
$requestSuccessful = false;
};
};
The mail() function lets you specify a target e-mail address, subject line, and message body—and much more, which you'll learn about in Chapter 15.
With your logic taken care of, you can commence your display logic—that is, rendering something that the user can actually see.
Notice that a combination of $requestMade and $requestSuccessful is used to determine what to display. If no request has been made, you invite the user to make a request and show the HTML_TreeMenu component. If a request has been made and it is not successful, you explain that to the user and invite him to try again, showing the HTML_TreeMenu component. If a request has been made and it is successful, you thank the user, but don't show the HTML_TreeMenu component because, for the sake of this exercise, we assume that the user does not want to request another title. In practice, some greater security, perhaps using cookies, would be required should the radio station genuinely want to limit the number of requests that an individual user is able to make each day.
Here's the display logic:
<HTML>
<HEAD>
<SCRIPT LANGUAGE="JavaScript" SRC="TreeMenu.js"></SCRIPT>
</HEAD>
<BODY>
<H1>Radio PHP</H1>
<?php
if ($requestMade) {
?>
<B>Thanks for your request!</B>
<BR><BR>
<?php
if (!($requestSuccessful)) {
?>
You'll be pleased to hear your request was successful, and we'll
try to play your song as soon as we can.
<?php
} else {
?>
Unfortunately we weren't able to play the song you requested.
We may have just recently removed it from our collection. Please feel
free to try again!
<?php
};
?>
<BR><BR>
<?php
};
?>
<?php
if (!($requestsuccessful)) {
?>
Request your song from our list below and it will be emailed to our
DJ - if we've not played it too recently we'll try to incorporate it for you
as soon as we can!
<HR>
<?php
$objTreeMenu->printMenu();
?>
<?php
};
?>
</BODY>
</HTML>
The menu is rendered exactly the same way that it was in the example at the start of the chapter.
Finally, don't forget to include the JavaScript file required by the HTML_TreeMenu component at the top of your HTML—it's a prerequisite.
