This took me some time to figure out, and based on the number of posts in online forums, I can tell I am not th eonly person interested in integrating my Winamp playlists with a website.
In case you haven’t noticed, I have added a block on the right-hand side of this website entitled “Now Playing…”. The contents of this block reflects what is currently on my Winamp media player. There was a trick to getting that to work properly, but the other trick which has little to do with Winamp, is the display of the CD cover art.
There are several steps to this project:
Step 1: Data has to be sent from Winamp to the website. In order to do this, I used a plugin called “DoSomething” http://www.oddsock.org/tools/dosomething, which has the capability to export ID3 metadata to files, which can then be sent to the website via FTP. Once you have the plugin downloaded and installed, you need to configure it.
The first part of configuring the plugin is to create a template that the plugin can write data to. I chose to use an XML format for my template, which looks like this:
%%CURRENTARTIST%%
%%CURRENTALBUM%%
%%ARTIST2%%
%%ALBUM2%%
%%ARTIST3%%
%%ALBUM3%%
%%LASTUPDATED%%
I saved this template as C:\temp\winamp.template.txt. In the DoSomething configuration dialog box, select “Generate HTML Playlist” from the Actions pulldown box. In the “Template In” field, enter the path to your template file. In the “Template Out” field, I entered C:\temp\winamp.xml. Press the Add button to add this to the list of actions.
You’ll probably want to enable Error Messages, as well as ID3 Info Gathering.
Next, select “FTP A File” from the Actions pulldown. Configure your ftp host, username, and password. Specify the local file as the path to your template out file (in my case, C:\temp\winamp.xml), and specify the path and file name of the remote file on your FTP server.
Now, everytime the song changes, this file will get updated and sent to your website via FTP.
Step 2:Now you have the data on your website, you have to do something with it. My solution was to use PHP5 to parse the XML. With PHP5, parsing XML is very very easy. I placed the following code in a file called “nowPlaying.php”:
/**
* Parse XML playlist
*
* This function parses an XML playlist file that was generated by Winamp, and
* uploaded to the webserver via FTP
*
* @access public
* @param string $file The path to the xml file to parse
* @return mixed Associative array containing the playlist data
*/
function parsePlaylist($file)
{
$playlist = array();
$dom = new DomDocument();
$dom->load($file);
// Loop through all the nodes
foreach ($dom->documentElement->childNodes as $entries)
{
// if node is an element
if ($entries->nodeType == 1)
{
// Loop through the inner set of nodes
foreach ($entries->childNodes as $item)
{
// Populate the array
$playlist[$entries->nodeName][$item->nodeName] = $item->textContent;
}
}
}
return $playlist;
} // End Function
// Call the parsePlaylist function, which returns an array
$arrPlaylist = parsePlaylist(”../winamp/winamp.xml”);
$title = $arrPlaylist[’current’][’title’];
$artist = $arrPlaylist[’current’][’artist’];
$album = $arrPlaylist[’current’][’album’];
// Force the page to refresh every 60 seconds
print ‘‘;
if($title != “”)
{
print ‘
Title:
‘.
‘
’.$title.
‘
‘;
}
if($artist != “”)
{
print ‘
Artist:
‘.
‘
‘.$artist.’
‘;
}
if($album != “”)
{
print ‘
From the album:
‘.
‘
’.$album.’
‘;
}
It’s fairly ugly to have all the css code within the print statements - these should be placed with an external css file as classes, and referenced as such within the
tags.
Step 3: Now that we have the playlist displayed on a webpage, we have to get the CD cover art. This was fairly tricky to do, and took some trial and error, but it works fairly reliably. The cover art is retrieved dynamically from Amazon.com using their search API. I borrowed some code written by Calin Uioreanu at php9.com, which did a very good job of performing a search and returning a bunch of results. However, I did not want multiple results; I only wanted the CD image.
There are three components to this approach:
- amazon_layout.php
- amazon_config.php
- amazon_class.php
The amazon_layout.php is what displays the actual image. The code for it is as follows:
‘;
}
echo
‘
‘,
‘
‘
;
?>
Again, this code was modified from Colin’s original to use only the first image in the array of data, as opposed to displaying all the images returned from the Amazon.com search.
The next file, amazon_conf.php contains configuration code for accessing the Amazon API:
‘baby (Baby)’,
‘books’ => ‘books (Books)’,
‘classical’ => ‘classical (Classical Music)’,
‘dvd’ => ‘dvd (DVD)’,
‘electronics’ => ‘electronics (Electronics)’,
‘garden’ => ‘garden (Outdoor Living)’,
‘kitchen’ => ‘kitchen (Kitchen & Housewares)’,
‘magazines’ => ‘magazines (Magazines)’,
‘music’ => ‘music (Popular Music)’,
‘pc-hardware’ => ‘pc-hardware (Computers)’,
‘photo’ => ‘photo (Camera & Photo)’,
’software’ => ’software (Software)’,
‘toys’ => ‘toys (Toys & Games)’,
‘universal’ => ‘universal (Tools & Hardware)’,
‘vhs’ => ‘vhs (Video)’,
‘videogames’ => ‘videogames (Computer & Video Games)’
);
// sort by salesRank by default
if (!$sCurrentModeSortType = $_GET[’SortBy’])
{
$sCurrentModeSortType = ‘+salesrank’;
}
// Sort Types
$arModeSortType = array (
‘baby’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
),
‘books’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+reviewrank’ => ‘Average Customer Review’,
‘+pricerank’ => ‘Price (Low to High)’,
‘+inverse-pricerank’ => ‘Price (High to Low)’,
‘+daterank’ => ‘Publication Date’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
‘-titlerank’ => ‘Alphabetical (Z-A)’,
),
‘classical’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
),
‘dvd’ => array(
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical’,
),
‘electronics’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical’,
‘+reviewrank’ => ‘Review’,
),
‘garden’ => array(
‘+psrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
‘-titlerank’ => ‘Alphabetical (Z-A)’,
‘+manufactrank’ => ‘Manufacturer (A-Z)’,
‘-manufactrank’ => ‘Manufacturer (Z-A)’,
‘+price’ => ‘Price (Low to High)’,
‘-price’ => ‘Price (High to Low)’,
),
‘kitchen’ => array(
‘+psrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
‘-titlerank’ => ‘Alphabetical (Z-A)’,
‘+manufactrank’ => ‘Manufacturer (A-Z)’,
‘-manufactrank’ => ‘Manufacturer (Z-A)’,
‘+price’ => ‘Price (Low to High)’,
‘-price’ => ‘Price (High to Low)’,
),
‘magazines’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
),
‘music’ => array(
‘+psrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+artistrank’ => ‘Artist Name’,
‘+orig-rel-date’ => ‘Original Release Date’,
‘+titlerank’ => ‘Alphabetical’,
),
‘pc-hardware’ => array(
‘+psrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
‘-titlerank’ => ‘Alphabetical (Z-A)’,
),
‘photo’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
‘-titlerank’ => ‘Alphabetical (Z-A)’,
),
’software’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical’,
‘+price’ => ‘Price (Low to High)’,
‘+price’ => ‘Price (High to Low)’,
),
‘toys’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
),
‘universal’ => array(
‘+psrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical (A-Z)’,
‘-titlerank’ => ‘Alphabetical (Z-A)’,
‘+manufactrank’ => ‘Manufacturer (A-Z)’,
‘-manufactrank’ => ‘Manufacturer (Z-A)’,
‘+price’ => ‘Price (Low to High)’,
‘-price’ => ‘Price (High to Low)’,
),
‘vhs’ => array(
‘+psrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical’,
),
‘videogames’ => array(
‘+pmrank’ => ‘Featured Items’,
‘+salesrank’ => ‘Bestselling’,
‘+titlerank’ => ‘Alphabetical’,
‘+price’ => ‘Price (Low to High)’,
‘-price’ => ‘Price (High to Low)’,
),
);
$sUrl = ‘http://xml.amazon.com/onca/xml3′;
$sUrl .= ‘?t=’. ASSOCIATE_ID;
$sUrl .= ‘&dev-t=’. DEVELOPER_TOKEN;
$sUrl .= ‘&mode=’ . $sCurrentMode;
$sUrl .= ‘&type=lite&page=1′;
$sUrl .= ‘&f=xml’;
$sUrl .= ‘&KeywordSearch=’;
// The searchFor variable is set as a global variable in the calling php script
$sUrl .= urlencode($GLOBALS[’searchFor’]);
$sUrl .= ‘&sort=’. $sCurrentModeSortType;
?>
At this point, you can see that the config file sets a bunch of variables that the Amazon class needs to use. A very important piece of this is what you wish to search for. This data comes from your calling script; in my case, it’s defined in nowPlaying.php, which is listed in step 1. You will need to add the following code to your calling script to set the global variables containing the search criteria:
global $searchFor;
$searchFor = $arrPlaylist['current']['album'].'+'.$arrPlaylist['current']['artist'];
Add this code somewhere after you call the parsePlaylist() function. By combining the name of the album with the name of the artist, we increase the chances that we’ll get accurate matches from Amazon.
Lastly, we have the actual class which is the heart and soul of this entire project:
parser = xml_parser_create();
} // end func
/**
* boolean parse(void)
* parses a XML file
*/
function parse()
{
if (!$this->testLock())
{
// prevent html caching
$_GLOBALS[’bFeedError’] = true;
return false;
}
// set the handlers
xml_set_object($this->parser, $this);
xml_set_element_handler($this->parser, ’startHandler’, ‘endHandler’);
xml_set_character_data_handler($this->parser, ‘cdataHandler’);
if (!is_resource($this->fp))
{
$this->createLock();
return false;
}
/**
* Read the XML file and store the chunks into an array. This is done because
* the Amazon search will return many results, and we only want the first result
* which will be the most relevant (and most likely, the correct one)
*/
while ($data = fread($this->fp, 2048))
{
$chunks[] = $data;
}
// Close the xml file
fclose($this->fp);
// Parse the XML contained within the first chunk of data
$err = xml_parse($this->parser, $chunks[0]);
if (! $err)
{
return $err;
}
// free the parser resource
xml_parser_free($this->parser);
return true;
} // end func
/**
* void startHandler (obj , str , arr)
* Event handler called by the expat library when an element’s begin tag is encountered.
*/
function startHandler($parser, $sTag, $arAttr)
{
// Start with empty sData string.
$this->sData = ”;
// Put each attribute into the Data array.
foreach ($arAttr as $Key=> $Val)
{
$this->arAtribute[”$sTag:$Key”] = trim($Val);
}
} // end func
/**
* void cdataHandler (obj, str)
* Event handler called by the expat library when Character Data are encountered.
*/
function cdataHandler($parser, $sTag)
{
$this->sData .= $sTag;
}// end func
/**
* void endHandler (obj, str)
* Event handler called by the expat library when an element’s end tag is encountered.
*/
function endHandler($parser, $sTag)
{
static
$MODE,
$ASIN,
$PRODUCTNAME,
$CATALOG,
$AUTHORS,
$ARTISTS,
$STARRING,
$DIRECTORS,
$THEATRICALRELEASEDATE,
$RELEASEDATE,
$MANUFACTURER,
$IMAGEURLSMALL,
$IMAGEURLMEDIUM,
$IMAGEURLLARGE,
$ARRIMAGES,
$LISTPRICE,
$OURPRICE,
$USEDPRICE,
$REFURBISHEDPRICE,
$THIRDPARTYNEWPRICE,
$SALESRANK,
$LISTS,
$TRACKS,
$BROWSELIST,
$MEDIA,
$NUMMEDIA,
$ISBN,
$FEATURES,
$MPAARATING,
$PLATFORM,
$AVAILABILITY,
$UPC,
$ACCESSORIES,
$PRODUCTDESCRIPTION,
$REVIEWS,
$THIRDPARTYPRODUCTINFO,
$AVGCUSTOMERRATING,
$CUSTOMERREVIEW,
$THIRDPARTYPRODUCTDETAILS,
$SIMILARPRODUCTS,
$TOTALRESULTS
;
// put the $this->sData into a string.
$sData = $this->sData;
switch (strtoupper ($sTag))
{
case ‘ACTOR’:
// build the list
$STARRING[] = $sData;
break;
case ‘ARTIST’:
// build the list
$ARTISTS[] = $sData;
break;
case ‘DIRECTOR’:
// build the list
$DIRECTORS[] = $sData;
break;
case ‘AUTHOR’:
// build the Authors list
$AUTHORS .= ($AUTHORS?’, ‘:”) . $sData;
break;
case ‘LISTID’:
// build the list
$LISTS[] = $sData;
break;
case ‘TRACK’:
// build the list
$TRACKS[] = $sData;
break;
case ‘BROWSENAME’:
// build the list
$BROWSELIST[] = $sData;
break;
case ‘FEATURE’:
// build the list
$FEATURES[] = $sData;
break;
case ‘ACCESSORY’:
// build the list
$ACCESSORIES[] = $sData;
break;
case ‘PRODUCT’:
// build the list
$SIMILARPRODUCTS[] = $sData;
break;
case ‘AVGCUSTOMERRATING’:
// build the list
$AVGCUSTOMERRATING = $sData;
break;
case ‘CUSTOMERREVIEW’:
// build the list
$REVIEWS[] = $CUSTOMERREVIEW;
break;
case ‘THIRDPARTYPRODUCTDETAILS’:
// build the list
$THIRDPARTYPRODUCTINFO[] = $THIRDPARTYPRODUCTDETAILS;
break;
case ‘SELLERID’:
case ‘SELLERNICKNAME’:
case ‘EXCHANGEID’:
case ‘OFFERINGPRICE’:
case ‘CONDITION’:
case ‘CONDITIONTYPE’:
case ‘EXCHANGEAVAILABILITY’:
case ‘SELLERCOUNTRY’:
case ‘SELLERSTATE’:
case ‘SELLERRATING’:
// build the list
$THIRDPARTYPRODUCTDETAILS[$sTag] = $sData;
break;
case ‘RATING’:
// build the list
$CUSTOMERREVIEW[’Rating’] = $sData;
break;
case ‘SUMMARY’:
// build the list
$CUSTOMERREVIEW[’Summary’] = $sData;
break;
case ‘COMMENT’:
// build the list
$CUSTOMERREVIEW[’Comment’] = $sData;
break;
case ‘ASIN’:
case ‘MODE’:
case ‘PRODUCTNAME’:
case ‘CATALOG’:
case ‘THEATRICALRELEASEDATE’:
case ‘MPAARATING’:
case ‘RELEASEDATE’:
case ‘MANUFACTURER’:
case ‘IMAGEURLSMALL’:
case ‘IMAGEURLMEDIUM’:
$ARRIMAGES[][] = $sData;
break;
case ‘IMAGEURLLARGE’:
case ‘LISTPRICE’:
case ‘OURPRICE’:
case ‘USEDPRICE’:
case ‘REFURBISHEDPRICE’:
case ‘THIRDPARTYNEWPRICE’:
case ‘SALESRANK’:
case ‘MEDIA’:
case ‘PRODUCTDESCRIPTION’:
case ‘NUMMEDIA’:
case ‘ISBN’:
case ‘PLATFORM’:
case ‘AVAILABILITY’:
case ‘UPC’:
$$sTag = $sData;
break;
case ‘ERRORMSG’:
// if this is a global error, avoid caching the module
if (!$this->iNumResults)
{
global $bParseError;
$bParseError = true;
}
break;
case ‘DETAILS’:
// offer some details
$sBookUrl = PRODUCT_DETAIL_URL . $ASIN;
//Details finished
require($this->sTemplate);
// empty the product related information
$STARRING= array ();
$ARTISTS= array ();
$DIRECTORS= array ();
$AUTHORS = ”;
$REVIEWS = array ();
$THIRDPARTYPRODUCTINFO = array();
$BROWSELIST= array ();
$FEATURES= array ();
$ACCESSORIES = array ();
$AVGCUSTOMERRATING = array ();
$CUSTOMERREVIEW = array ();
$SIMILARPRODUCTS = array ();
$ASIN=”;
$PRODUCTNAME=”;
$CATALOG=”;
$AUTHORS=”;
$THEATRICALRELEASEDATE=”;
$RELEASEDATE=”;
$MANUFACTURER=”;
$IMAGEURLSMALL=”;
$IMAGEURLMEDIUM=”;
$IMAGEURLLARGE=”;
$LISTPRICE=”;
$OURPRICE=”;
$USEDPRICE=”;
$REFURBISHEDPRICE=”;
$THIRDPARTYNEWPRICE=”;
$SALESRANK=”;
$MEDIA=”;
$PRODUCTDESCRIPTION = ”;
$NUMMEDIA=”;
$ISBN=”;
$MPAARATING=”;
$PLATFORM=”;
$AVAILABILITY=”;
$UPC=”;
// increase global counter
$this->iNumResults++;
break;
case ‘TOTALRESULTS’:
$this->arAtribute[’TotalResults’] = $sData;
break;
case ‘PRODUCTINFO’:
break;
}
}
/**
* bool setInputUrl(str, int)
* tries to open a connection to the given url with the given timeout
*/
function setInputUrl($sUrl, $iTimeout)
{
if (!$this->testLock())
{
// prevent html caching
$_GLOBALS[’bFeedError’] = true;
return false;
}
$arUrl = parse_url($sUrl);
$sHost = $arUrl[’host’];
if (!@$iPort = $arUrl[’port’])
{
$iPort = 80; // default HTTP port
}
$sFullPath = $arUrl[’path’] . ‘?’ . $arUrl[’query’];
$fp = fsockopen($sHost, $iPort, $errno, $errstr, $iTimeout);
if(!is_resource($fp))
{
// return an error
echo ‘@’;
$this->createLock();
return false;
}
else
{
fputs($fp,”GET $sFullPath HTTP/1.0rnHost: ” . $sHost. “rnrn”);
// win32
if (function_exists(’socket_set_timeout’))
{
@socket_set_timeout($fp, $iTimeout);
}
// in blocking mode it will wait for data to become available on the socket
socket_set_blocking($fp, true);
// get the first line and determine the answer-code
$sLine = fgets($fp , 1024);
$iCode = preg_replace(”/.*(ddd).*/i” , “\1″ , $sLine);
// an error occurred if code is not between 200 and 399
$error = null;
if ($iCode != 200)
{
// prevent html caching
$_GLOBALS[’bFeedError’] = true;
error_log (
“n”. time() .’:’. $sUrl .’:’. $sLine .’:’. $_SERVER[’REQUEST_URI’],
3,
‘htmlcache/search/return_code_failures.log’
);
fclose($fp);
$this->createLock();
return false;
}
$sHeader = ”;
// no error - now determine start of data (skipping header)
while (!feof($fp))
{
$sLine = fgets($fp , 1024);
if (strlen($sLine) < 3)
{
break;
}
}
$this->fp = $fp;
return true;
}
} // end func
/**
* createLock()
* lock
*/
function createLock()
{
if (!$handle = fopen(AWS_LOCK_FILE, ‘w’))
{
echo ‘cannot open lock’;
return false;
}
fclose($handle);
return true;
} // end func
/**
* testLock()
* test lock for a given time, eventually release it
*/
function testLock()
{
if (!file_exists(AWS_LOCK_FILE))
{
return true;
}
if ((time() - filemtime(AWS_LOCK_FILE)) < AWS_LOCK_TIME)
{
return false;
}
// release lock
if (!$handle = unlink(AWS_LOCK_FILE))
{
return false;
}
return true;
} // end func
} // end class definition
?>
Step 4: Now that you have all of the code, you have to use it. I added the following function to my calling script (nowPlaying.php):
/**
* Get the image
*
* This function uses the Amazon class to search for a CD for the purpose of displaying
* the cover art on a webpage
*
* @access public
* @param string $album The name of the album
* @return null
*/
function getCDImage($album)
{
// configuration variables
require_once(’./amazon/amazon_config.php’);
// webservice class definition
require_once(’./amazon/amazon_class.php’);
flush();
if($album != “”)
{
$oAmazon = new Amazon_WebService();
if (!$oAmazon->setInputUrl($sUrl, 20))
{
die (’cannot open input file. exiting..’ . ‘@‘);
}
// pass the output display template
$oAmazon->sTemplate = ‘./amazon/amazon_layout.php’;
if (!$oAmazon->parse())
{
die (’XMLParse failed’);
}
}
else
{
print ”
No CD artwork available for this album
“;
}
}
Step 5: The final step is to embed this script into a webpage. The script prints a meta refresh tag which causes the page to reload every 60 seconds. I did not want the entire page to reload, so I display the script within an iframe:
Now, only this small rectangular block of content will refresh. I know that there seems to be a lot of steps involved with this integration, and perhaps the amazon stuff can all be merged into a single class (which it probably should…), but the results are worth the effort, in my opinion!
Feel free to contact me with problems!