<?php
// run.php
// script to extract album art from amazon product database
// http://magnetikonline.com/fetchalbumart/
require('config.php');
$scanalbumfolder = new scanalbumfolder($argv);
$scanalbumfolder->execute();
class scanalbumfolder {
private $argv = array();
public function __construct(array $inputargv) {
$this->argv = $inputargv;
}
public function execute() {
if (sizeof($this->argv) != 2) {
// album path not given
die('Error: please specify an album path to scan');
}
if (!is_dir($sourcefolder = $this->argv[1])) {
// invalid album path
die('Error: \'' . $sourcefolder . '\' is not a valid path');
}
$sourcefolder = rtrim($sourcefolder,'\\/') . '\*';
$amazonfindalbumimage = new amazonfindalbumimage();
$extractartistalbum = new extractartistalbum();
while ($albumfolderlist = glob($sourcefolder,GLOB_ONLYDIR)) {
foreach ($albumfolderlist as $albumfolder) {
// check for mp3's in found folder
if (!glob($albumfolder . '\\*.mp3')) continue;
// generate save image path and check if image already exists, skip to next album if so
$saveimagepath = $albumfolder . '\\' . TARGETIMAGE_FILENAME;
if (is_file($saveimagepath)) continue;
// skip if can't extract artist & album data
if (!($artistalbumlist = $extractartistalbum->extract($albumfolder))) continue;
$amazonfindalbumimage->execute(
$artistalbumlist[0],
$artistalbumlist[1],
$saveimagepath
);
}
// try one folder deeper
$sourcefolder .= '\*';
}
// all done
$amazonfindalbumimage->finish();
}
}
class amazonfindalbumimage {
const amazon_requesthost = 'ecs.amazonaws.com';
const amazon_requestpath = '/onca/xml';
private $logfp;
private $logerrorfp;
private $amazonresultpathlist = array();
private $amazonresultlist = array();
private $amazonresultcurrent = array();
public function __construct() {
$this->logfp = fopen(LOG_FILE,'a');
$this->logerrorfp = fopen(LOGERROR_FILE,'a');
}
public function finish() {
$this->writelog('Finished');
fclose($this->logfp);
fclose($this->logerrorfp);
}
public function execute($inputartistname,$inputalbumname,$inputtargetfile) {
$artistname = $this->removenoisewords($inputartistname);
$albumname = $this->removenoisewords($inputalbumname);
// make request to Amazon products API
$this->writeline('Searching for: [' . $artistname . ' / ' . $albumname . ']');
$responsexml = $this->fetchurl(
$this->buildamazonrequesturl(array(
'Keywords' => $artistname . ' ' . $albumname,
'Operation' => 'ItemSearch',
'ResponseGroup' => 'ItemAttributes,Images',
'SearchIndex' => 'Music',
'salesrank' => 'Bestselling'
)),
TRUE
);
// if $responsexml is null then error in fetching product details URL from amazon API
if (is_null($responsexml)) return FALSE;
// work over XML response and extract required data
$this->amazonresultlist = array();
$xmlparser = xml_parser_create();
xml_set_element_handler($xmlparser,array($this,'xmlparserstartelement'),array($this,'xmlparserendelement'));
xml_set_character_data_handler($xmlparser,array($this,'xmlparserdata'));
while ($responsexml) {
if (!xml_parse($xmlparser,array_shift($responsexml),!$responsexml)) {
die(
sprintf('XML error: %s at line %d' . self::crlf,
xml_error_string(xml_get_error_code($xmlparser)),
xml_get_current_line_number($xmlparser))
);
}
}
xml_parser_free($xmlparser);
// add final item to list
$this->addamazoncurrentitemtolist();
if (!$this->amazonresultlist) {
// no results/valid results - exit
$this->writeerrorlog('Unable to find album results for: [' . $artistname . ' / ' . $albumname . ']');
return FALSE;
}
// find the best item from our results
$lowestdistance = 9999;
$bestitemkey = 0;
foreach ($this->amazonresultlist as $key => $resultitem) {
$curdistance =
levenshtein($artistname,$this->removenoisewords($resultitem['artist'])) +
levenshtein($albumname,$this->removenoisewords($resultitem['album']));
if ($curdistance < $lowestdistance) {
// found a better item
$lowestdistance = $curdistance;
$bestitemkey = $key;
}
}
// save the best result item found
$resultitem = $this->amazonresultlist[$bestitemkey];
// download album image to temp location
$albumimagedata = $this->fetchurl($resultitem['url']);
// if $albumimagedata is null then error in fetching the album image from amazon
if (is_null($albumimagedata)) return FALSE;
file_put_contents(TEMP_IMAGE_PATH,$albumimagedata);
list($imagewidth,$imageheight) = getimagesize(TEMP_IMAGE_PATH);
// work out which axis to resize on
$resizewidth = ceil($imagewidth * (TARGETIMAGE_HEIGHT / $imageheight));
if ($resizewidth >= TARGETIMAGE_WIDTH) {
// resize source amazon image by width
$exec = $this->buildimagemagikexec($resizewidth,$inputtargetfile);
} else {
// resize source amazon image by height
$exec = $this->buildimagemagikexec(
'x' . (ceil($imageheight * (TARGETIMAGE_WIDTH / $imagewidth))),
$inputtargetfile
);
}
// execute image resize, putting image into its target location
exec($exec);
$this->writelog('Found album result for: [' . $artistname . ' / ' . $albumname . ']');
$this->writelog('Image resize cmd: ' . $exec);
$this->writelog('Saved image to: ' . $inputtargetfile);
// success!
return TRUE;
}
private function removenoisewords($inputstring) {
$text = $inputstring;
// remove some common noise words/symbols that will get in the way
$text = str_replace(
array(
' a ',
' and ',
' the ',
'&',
'-',
':',
),
' ',
' ' . $text . ' '
);
// remove single quotes
$text = str_replace('\'','',$text);
// remove all redundant spaces from the string
$text = preg_replace('/ +/',' ',$text);
// trim result, lowercase and return
return strtolower(trim($text));
}
private function fetchurl($inputurl,$inputchunked = FALSE) {
$fp = fopen($inputurl,'r');
if ($fp === FALSE) {
// error fetching URL, most likely invalid characters in request URI
$this->writeerrorlog('Unable to open URL: [' . $inputurl . ']');
return NULL;
}
$response = array();
while (!feof($fp)) $response[] .= fread($fp,1024);
fclose($fp);
return ($inputchunked) ? $response : implode('',$response);
}
function buildamazonrequesturl(array $inputparameterlist,$inputawsversion = '2009-06-01') {
$query = array(
'Service' => 'AWSECommerceService',
'AWSAccessKeyId' => AMAZON_ACCESSKEY,
'Timestamp' => date('Y-m-d\TH:i:s\Z'),
'Version' => $inputawsversion,
);
$querylist = array_merge($query,$inputparameterlist);
// do a case-insensitive, natural order sort on the array keys
ksort($querylist);
// create the signable string
$templist = array();
foreach ($querylist as $k => $v) {
$templist[] = str_replace('%7E','~',rawurlencode($k) . '=' .rawurlencode($v));
}
// hash the AWS secret key and generate a signature for the request
$generatedhash = hash_hmac(
'sha256',
"GET\n" . self::amazon_requesthost . "\n" . self::amazon_requestpath . "\n" . implode('&',$templist),
AMAZON_SECRETKEY
);
$signature = '';
$generatedhashlength = strlen($generatedhash);
for ($i = 0;$i < $generatedhashlength;$i += 2) {
$signature .= chr(hexdec(substr($generatedhash,$i,2)));
}
$querylist['Signature'] = base64_encode($signature);
ksort($querylist);
$templist = array();
foreach ($querylist as $k => $v) {
$templist[] = rawurlencode($k) . '=' . rawurlencode($v);
}
// return final request URL
return 'http://' . self::amazon_requesthost . self::amazon_requestpath . '?' . implode('&',$templist);
}
private function xmlparserstartelement($inputparser,$inputname,$inputattributelist) {
array_push($this->amazonresultpathlist,$inputname);
}
private function xmlparserendelement($inputparser,$inputname) {
array_pop($this->amazonresultpathlist);
}
private function xmlparserdata($inputparser,$inputdata) {
$xmlpath = implode('/',$this->amazonresultpathlist);
if ($xmlpath == 'ITEMSEARCHRESPONSE/ITEMS/ITEM/ASIN') {
// found start of a new item
// validate item, if all required data found add to list
$this->addamazoncurrentitemtolist();
$this->amazonresultcurrent = array();
return;
}
if ($xmlpath == 'ITEMSEARCHRESPONSE/ITEMS/ITEM/LARGEIMAGE/URL') {
// save item image url
$this->amazonresultcurrent['url'] = $inputdata;
return;
}
if ($xmlpath == 'ITEMSEARCHRESPONSE/ITEMS/ITEM/LARGEIMAGE/WIDTH') {
// save item image width
$this->amazonresultcurrent['width'] = intval($inputdata);
return;
}
if ($xmlpath == 'ITEMSEARCHRESPONSE/ITEMS/ITEM/LARGEIMAGE/HEIGHT') {
// save item image height
$this->amazonresultcurrent['height'] = intval($inputdata);
return;
}
if ($xmlpath == 'ITEMSEARCHRESPONSE/ITEMS/ITEM/ITEMATTRIBUTES/ARTIST') {
// save item album artist
$this->amazonresultcurrent['artist'] = trim($inputdata);
return;
}
if ($xmlpath == 'ITEMSEARCHRESPONSE/ITEMS/ITEM/ITEMATTRIBUTES/BINDING') {
// ensure item is an Audio CD
if ($inputdata == 'Audio CD') $this->amazonresultcurrent['isaudiocd'] = TRUE;
return;
}
if ($xmlpath == 'ITEMSEARCHRESPONSE/ITEMS/ITEM/ITEMATTRIBUTES/TITLE') {
// save item album title
$this->amazonresultcurrent['album'] = trim($inputdata);
return;
}
}
private function addamazoncurrentitemtolist() {
// does all data exist for item?
foreach (array('isaudiocd','url','width','height','artist','album') as $item) {
if (!isset($this->amazonresultcurrent[$item])) return;
}
// all data exists check image width/height are large enough
if (
($this->amazonresultcurrent['width'] < TARGETIMAGE_WIDTH) ||
($this->amazonresultcurrent['height'] < TARGETIMAGE_HEIGHT)
) return;
// save to $this->amazonresultlist
$this->amazonresultlist[] = $this->amazonresultcurrent;
}
private function buildimagemagikexec($inputresizeparameter,$inputtargetfile) {
return
IMAGEMAGICK_CONVERT . ' ' . TEMP_IMAGE_PATH . ' ' .
'-resize ' . $inputresizeparameter . ' ' .
'-gravity center ' .
'-crop ' . TARGETIMAGE_WIDTH . 'x' . TARGETIMAGE_HEIGHT . '+0+0 ' .
'-quality ' . IMAGEMAGICK_SAVE_QUALITY . ' ' .
'"' . $inputtargetfile . '"';
}
private function writeline($inputmessage) {
echo($inputmessage . "\r\n");
}
private function writelog($inputmessage) {
$line = '[' . date('Y-m-d H:i:s') . '] ' . $inputmessage . "\r\n";
fwrite($this->logfp,$line);
echo($line);
}
private function writeerrorlog($inputmessage) {
$line = '[' . date('Y-m-d H:i:s') . '] ' . $inputmessage . "\r\n";
fwrite($this->logerrorfp,$line);
echo('Error: ' . $line);
}
}
?>