3     Pitchfork Music Player Daemon Client
 
   4     Copyright (C) 2007  Roger Bystrøm
 
   6     This program is free software; you can redistribute it and/or modify
 
   7     it under the terms of the GNU General Public License as published by
 
   8     the Free Software Foundation; version 2 of the License.
 
  10     This program is distributed in the hope that it will be useful,
 
  11     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  13     GNU General Public License for more details.
 
  15     You should have received a copy of the GNU General Public License along
 
  16     with this program; if not, write to the Free Software Foundation, Inc.,
 
  17     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
  21         /* how much time must pass before we try searching for cover art again */
 
  22         $COVER_SEARCH_AGAIN = 86400;
 
  24         include("aws_signed_request.php");
 
  25         require_once("../inc/base.php");
 
  26         require_once("metadata_cover.php");
 
  28         function amazonlink($params)
 
  30                 $params["SubscriptionId"] = "15BH771NY941TX2NKC02";
 
  31                 if (strlen(get_config("aws_secret")) == 0)
 
  33                 $res = aws_signed_request("com", $params, get_config("aws_keyid"), get_config("aws_secret"));
 
  38         /* metadata should not require locking of session */
 
  39         session_write_close();
 
  41         $missing_metadata_dir = false;
 
  43         if(!file_exists($metadata_dir)) {
 
  44                 if(!mkdir($metadata_dir, 0755)) {
 
  45                         $missing_metadata_dir = true;
 
  48         if(!is_writeable($metadata_dir)) {
 
  49                 $missing_metadata_dir = true;
 
  52         if($missing_metadata_dir) {
 
  53                 $xml = array_to_xml(array("result" => "nocachedir"));
 
  58         function escape_name($name) {
 
  59                 return str_replace(DIRECTORY_SEPARATOR, "_", $name);
 
  62         function get_cover_base_name($artist, $album) {
 
  64                 return $cover_dir . escape_name($artist) . " - " . escape_name($album);
 
  67         function get_album_info_name($artist, $album) {
 
  68                 return get_cover_base_name($artist, $album) . ".txt";
 
  70         function get_lyric_filename($artist, $title) {
 
  72                 return $cover_dir . escape_name($artist) . " - ". escape_name($title) . ".lyric";
 
  75         function find_lyrics($arr) {
 
  76                 foreach($arr as $val) {
 
  79                         if(isset($val['name'])&&$val['name']=="RETURN") {
 
  80                                 return $val['children'];
 
  82                         else if(is_array($val)) {
 
  83                                 $ret = find_lyrics($val);
 
  91         function fp_get_contents($fp) {
 
  94                 while($tmp = fgets($fp)) 
 
 102         /* Queries amazon with the specified url, strict serach first and then a more careless one, 
 
 103          * will urlencode artist and albumname 
 
 104          * returns xml document or false upon failure */
 
 105         function amazon_album_query($params, $album) {
 
 106                 $stype = array("Title", "Keywords");
 
 107                 $artist = urlencode($artist);
 
 108                 $album = urlencode($album);
 
 109                 foreach($stype as $st) {
 
 112                         $params[$st] = $album;
 
 113                         $xml = amazonlink($params);
 
 114                         if($xml&&isset($xml->Items[0])&&isset($xml->Items[0]->Item[0]))
 
 121         /* returns file pointer or false */
 
 122         function get_album_lock($artist, $album) {
 
 123                 $file_name = get_album_info_name($artist, $album);
 
 124                 $exists = file_exists($file_name);
 
 128                         $fp = @fopen($file_name, "r+");
 
 129                 else $fp = @fopen($file_name, "w+");
 
 130                 if($fp && flock($fp, LOCK_EX))
 
 133                 trigger_error("Can't lock album-file: $file_name", E_USER_WARNING);
 
 137         /* waits for appropriate amazon time, have to be called before making any amazon requests
 
 138            returns true if ok to continue otherwise false */
 
 139         function amazon_wait() {
 
 140                 global $metadata_dir;
 
 142                 /* rationale behind this: 
 
 143                  * amazon requires that we don't make more than one request pr. second pr. ip */
 
 145                 $file_name = $metadata_dir . "amazon_time";
 
 146                 if(file_exists($file_name)) 
 
 147                         $fp = @fopen($file_name, "r+");
 
 148                 else $fp = @fopen($file_name, "w+");
 
 151                         trigger_error("Can't open amazon_time", E_USER_WARNING);
 
 154                 if(!flock($fp, LOCK_EX)) {
 
 156                         trigger_error("Can't lock amazon_time", E_USER_WARNING);
 
 160                 $last = fp_get_contents($fp);
 
 163                         if(is_numeric($last)) {
 
 164                                 $stime = current_time_millis() - $last;
 
 166                         $stime = abs($stime);
 
 168                                 usleep($stime*1000); // micro seconds
 
 171                 if(@fwrite($fp, current_time_millis())===false) {
 
 173                         trigger_error("Can't write to amazon_time", E_USER_WARNING);
 
 182         /* returns artist and album info and get's album lock or dies */
 
 183         /* return value: array($fp, $artist, $album) */
 
 184         function init_album_artist_or_die() {
 
 186                 header("Content-Type: text/xml; charset=UTF-8");
 
 190                 if(isset($_GET['artist'])&&isset($_GET['album']) &&
 
 191                    strlen(trim($_GET['artist']))>0&&strlen(trim($_GET['album']))>0) {
 
 192                         $album = trim($_GET['album']);
 
 193                         $artist = trim($_GET['artist']);
 
 196                         $xml = array_to_xml(array("result" => "missingparam"));
 
 201                 $fp = get_album_lock($artist, $album);
 
 204                         $xml = array_to_xml(array("result" => "failed"));
 
 208                 return array($fp, $artist, $album);
 
 211         /* returns array(artist, album, filename) or false */
 
 212         function get_current_info() {
 
 214                         $pl = get_playback();
 
 216                                 $info = $pl->getCurrentSong();
 
 217                                 if(isset($info['Artist'])&&isset($info['Title'])) {
 
 218                                         $artist = trim($info['Artist']);
 
 219                                         $title = trim($info['Title']);
 
 220                                         $file_name = $info['file'];
 
 221                                         return array($artist, $title, $file_name);
 
 226                 catch(PEARException $e) {
 
 232         function get_cover() {
 
 233                 global $COVER_SEARCH_AGAIN, $cover_providers;
 
 235                 list($fp, $artist, $album) = init_album_artist_or_die();
 
 237                 $xml = fp_get_contents($fp);
 
 239                         $xml = @simplexml_load_string($xml);
 
 242                                 if(isset($xml->notfound)&&is_numeric((string)$xml->notfound[0])) {
 
 243                                         $time = @intval((string)$xml->notfound[0]);
 
 244                                         if($time+$COVER_SEARCH_AGAIN<time())
 
 247                                 else if(!isset($xml->image[0])&&!isset($xml->thumbnail[0])) {
 
 252                                         $xml->addChild("cached", "true");
 
 262                 foreach($cover_providers as $cp) {
 
 263                         $res = $cp($artist, $album);
 
 264                         if($res&&is_array($res))
 
 269                         if($res&&is_array($res)) {
 
 270                                 foreach($res as $key => $val) {
 
 271                                         if(!isset($xml->$key))
 
 272                                                 $xml->$key = (string)$val;
 
 276                                 $xml->notfound = time();
 
 280                         if($res&&is_array($res)) {
 
 281                                 $res['time'] = time();
 
 282                                 $xml = array_to_xml($res);
 
 285                                 $xml = array("notfound" => time());
 
 286                                 $xml = array_to_xml($xml);
 
 290                 @fwrite($fp, $xml->asXML());
 
 297         function get_review() {
 
 298                 global $COVER_SEARCH_AGAIN;
 
 300                 list($fp, $artist, $album) = init_album_artist_or_die();
 
 302                 $xml = fp_get_contents($fp);
 
 313                         $xml = @simplexml_load_string($xml);
 
 315                                 if(isset($xml->rnotfound)&&is_numeric((string)$xml->rnotfound[0])) {
 
 316                                         $time = @intval((string)$xml->rnotfound[0]);
 
 317                                         if($time+$COVER_SEARCH_AGAIN>time())
 
 323                 if(!$xml||(!(isset($xml->review[0])||isset($xml->desc[0]))&&!$no_search)) {
 
 326                                 echo array_to_xml(array("result" => "failed"))->asXML();
 
 330                         if($xml&&isset($xml->asin[0])) {
 
 331                                 $res = amazonlink(array("Operation"=>"ItemLookup", "IdType"=>"ASIN", "ItemId"=>"urlencode($xml->asin[0])"));
 
 335                                 $res = @amazon_album_query(array("Operation"=>"ItemSearch", "SearchIndex"=>"Music", "Artist"=>"$artist"), $album);
 
 338                                 if($res&&isset($res->Items[0])&&isset($res->Items[0]->Item[0])) {
 
 339                                         $p = $res->Items[0]->Item[0];
 
 340                                         $asin = (string) $p->ASIN;
 
 341                                         if(isset($p->EditorialReviews[0])) {
 
 342                                                 $p = $p->EditorialReviews[0];
 
 343                                                 foreach($p->EditorialReview as $er) {
 
 344                                                         if(!$desc&&"Album Description" == (string)$er->Source) {
 
 345                                                                 $desc = (string) $er->Content;
 
 348                                                                 $review_src = (string) $er->Source;
 
 349                                                                 $review = (string) $er->Content;
 
 353                                         /* set info in xml-file... */
 
 356                                                         $xml->review_src = htmlspecialchars($review_src); 
 
 357                                                         $xml->review = htmlspecialchars($review);
 
 360                                                         $xml->desc = htmlspecialchars($desc);
 
 362                                                 if(!isset($xml->asin[0])) {
 
 363                                                         $xml->addChild("asin", $asin);
 
 366                                                 if(!$review&&!$desc) {
 
 375                                                 $xml['asin'] = $asin;
 
 377                                                         $xml['desc'] = $desc;
 
 379                                                         $xml['review_src'] = $review_src;
 
 380                                                         $xml['review'] = $review;
 
 384                                                 $xml = array_to_xml($xml);
 
 397                         $xml->addChild("cached", "true");
 
 402                                 if(isset($xml->rnotfound)) {
 
 403                                         $xml->rnotfound = time();
 
 406                                         $xml->addChild("rnotfound", time());
 
 408                                 @fwrite($fp, $xml->asXML());
 
 411                                 @fwrite($fp, $xml->asXML());
 
 415                         $xml = array_to_xml(array("rnotfound" => time()));
 
 416                         @fwrite($fp, $xml->asXML());
 
 423         /* artist, title and song file name in system */
 
 424         function _get_lyric_lyricwiki($artist, $title, $file_name) {
 
 425                 $file = get_lyric_filename($artist, $title);
 
 426                 $fp = fsockopen("lyricwiki.org", 80);
 
 428                         $xml = array_to_xml(array("result"=>"connectionfailed"));
 
 429                         return $xml->asXML();
 
 432                 $out = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n";
 
 433                 $out .= "<SOAP-ENV:Envelope SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" ";
 
 434                 $out .= "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" ";
 
 435                 $out .= "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" ";
 
 436                 $out .= "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
 
 437                 $out .= "xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" ";
 
 438                 $out .= "xmlns:tns=\"urn:LyricWiki\">";
 
 439                 $out .= "<SOAP-ENV:Body><tns:getSong xmlns:tns=\"urn:LyricWiki\">";
 
 440                 $out .= "<artist xsi:type=\"xsd:string\">";
 
 441                 $out .= htmlspecialchars($artist);
 
 443                 $out .= "<song xsi:type=\"xsd:string\">";
 
 444                 $out .= htmlspecialchars($title);
 
 446                 $out .= "</tns:getSong></SOAP-ENV:Body></SOAP-ENV:Envelope>\r\n";
 
 448                 $head = "POST /server.php HTTP/1.1\r\n";
 
 449                 $head .= "Host: lyrics.wikia.com\r\n";
 
 450                 $head .= "SOAPAction: urn:LyricWiki#getSong\r\n";
 
 451                 $head .= "Content-Type: text/xml; charset=UTF-8\r\n";
 
 452                 $head .= "User-Agent: RemissPitchfork/0.1\r\n";
 
 453                 $head .= "Content-Length: " . str_byte_count($out) . "\r\n";
 
 454                 $head .= "Connection: Close\r\n\r\n";
 
 456                 fwrite($fp, $head . $out);
 
 458                 $responseHeader = "";
 
 459                 /* assume everything is dandy */
 
 461                         $responseHeader.= fread($fp, 1);
 
 463                 while (!preg_match('/\\r\\n\\r\\n$/', $responseHeader));
 
 467                         $ret .= fgets($fp, 128);
 
 470                 /* stupid hack to get around wrong xml declearation */
 
 472                 if(strpos($ret, "<". $qmark . "xml version=\"1.0\" encoding=\"ISO-8859-1\"".$qmark.">")===0)
 
 473                         $ret = str_replace("<". $qmark . "xml version=\"1.0\" encoding=\"ISO-8859-1\"".$qmark.">",
 
 474                                         "<". $qmark . "xml version=\"1.0\" encoding=\"UTF-8\"".$qmark.">",
 
 479                 $parser = new xml2array();
 
 480                 $parser->parse($ret);
 
 481                 $data = find_lyrics($parser->arrOutput);
 
 482                 // check that data is ok and lyrics exist
 
 483                 if($data&&isset($data[2]['tagData'])) {
 
 485                         foreach($data as $d) {
 
 486                                 if($d['name']=="ARTIST") 
 
 487                                         $res['artist'] = $d['tagData'];
 
 488                                 else if($d['name']=="SONG") 
 
 489                                         $res['title'] = $d['tagData'];
 
 490                                 else if($d['name']=="LYRICS")
 
 491                                         $res['lyric'] = $d['tagData'];
 
 492                                 else if($d['name']=="URL")
 
 493                                         $res['url'] = $d['tagData'];
 
 495                         $res['from'] = "lyricwiki.org";
 
 496                         $res['time'] = time();
 
 497                         /* this caching thing will have to be extracted if we 
 
 498                          * put in another lyrics source */
 
 499                         if(trim($res['lyric'])&&trim($res['lyric'])!="Not found") {
 
 500                                 $xml = array_to_xml(array("result" => $res));
 
 501                                 $xml->addChild("file", htmlspecialchars($file_name));
 
 502                                 $res = $xml->asXML();
 
 503                                 @file_put_contents($file, $res);
 
 506                                 $out = array("result" => "notfound");
 
 507                                 if(isset($res['url']))
 
 508                                         $out['url'] = $res['url'];
 
 509                                 $res = array_to_xml($out);
 
 510                                 $res = $res->asXML();
 
 517         /* $file: filename of cached version
 
 518          * $file_name: file name of song */
 
 519         function _get_lyric_cache($file, $file_name) {
 
 520                 $xml = @simplexml_load_file($file);
 
 523                         if(isset($xml->file)) {
 
 524                                 foreach($xml->file as $f) {
 
 525                                         if(((string)$f)==$file_name)
 
 530                                 $xml->addChild("file", htmlspecialchars($file_name));
 
 531                                 @file_put_contents($file, $xml->asXML());
 
 533                         $xml->addChild("cached", "true");
 
 534                         return $xml->asXML();
 
 539         function get_lyric($info = null) {
 
 540                 header("Content-Type: text/xml; charset=UTF-8");
 
 543                         $info = get_current_info();
 
 545                         $xml = array_to_xml(array("result"=>"failed"));
 
 551                 $file_name = $info[2];
 
 553                 $file = get_lyric_filename($artist, $title);
 
 554                 if(file_exists($file)&&!isset($_GET['force'])) {
 
 555                         $xml = _get_lyric_cache($file, $file_name);
 
 562                 $xml = _get_lyric_lyricwiki($artist, $title, $file_name);
 
 567                         echo array_to_xml(array("result" => "failed"))->asXML();
 
 575                 $b_name = basename(trim($_GET['pic']));
 
 576                 $name = $cover_dir . $b_name;
 
 577                 if(file_exists($name)&&is_readable($name)) {
 
 578                         if(function_exists("finfo_open")&&function_exists("finfo_file")) {
 
 579                                 $f = finfo_open(FILEINFO_MIME); 
 
 580                                 header("Content-Type: " . finfo_file($f, $name));
 
 582                         else if(function_exists("mime_content_type")) {
 
 583                                 header("Content-Type: " . mime_content_type($name));
 
 586                                 header("Content-Type: image/jpeg");
 
 588                         $c = "Content-Disposition: inline; filename=\"";
 
 589                         $c .= rawurlencode($b_name) . "\"";
 
 591                         echo @file_get_contents($name);
 
 596                         echo "File does not exist\n";
 
 597                         trigger_error("Did not find albumart althought it was requested", E_USER_WARNING);
 
 602         function get_recommendations_from_playlist() {
 
 603                 require_once("../player/openstrands.php");
 
 604                 $pl = get_playlist();
 
 605                 $list = $pl->getPlaylistInfo();
 
 607                 foreach($list as $song) {
 
 608                         if(isset($song['Artist'])&&$song['Artist'])
 
 609                                 $artist[$song['Artist']] = true;
 
 611                 $artist = array_keys(array_change_key_case($artist));
 
 614                 header("Content-Type: text/xml; charset=UTF-8");
 
 616                 $ret = strands_get_recommendations($artist);
 
 618                 if(!$ret || ! count($ret)) {
 
 619                         $res['result'] = is_array($ret)?"notfound":"failed";
 
 620                         echo array_to_xml($res)->asXML();
 
 623                 $db = get_database();
 
 624                 foreach($ret as $a) {
 
 627                         $tmp['album'] = $db->getMetadata("Album", "Artist", $a);
 
 630                 $out = array("result" => $res);
 
 632                 echo array_to_xml($out)->asXML();
 
 635         function do_houseclean() {
 
 636                 /* this is a *very* inefficient method, but it's needed... */
 
 637                 //header("Content-Type: text/xml; charset=UTF-8");
 
 638                 header("Content-type: multipart/x-mixed-replace;boundary=--ThisRandomString");
 
 640                 global $metadata_dir;
 
 642                 echo "--ThisRandomString\n";
 
 643                 $out = "Content-type: text/html\n\n".
 
 644                         "<html><head><title>Housecleaning</title></head><body>\n".
 
 645                         "<p>Performing housecleaning, please wait...</p>\n";
 
 647                 echo "$out--ThisRandomString\n";
 
 650                 set_time_limit(300); // this might take a while, but 
 
 651                                     // shouldn't be more than 5 mins even on slow machines
 
 652                 $db = get_database();
 
 655                         $time = current_time_millis();
 
 656                         $list = $db->getAll();
 
 657                         if(!isset($list['file']))
 
 659                         $files = $list['file'];
 
 661                         $list = scandir($metadata_dir);
 
 662                         $total = count($list);
 
 668                         foreach($list as $f) {
 
 669                                 $r = strrpos($f, ".lyric");
 
 671                                 if($r!==false&&$r+6==strlen($f)) {
 
 672                                         $xml = @simplexml_load_file($metadata_dir . $f);
 
 674                                         if($fcount%100 == 0) {
 
 676                                                 echo "<p>Processed $fcount (".(int)($tcount*100/$total)."%)..</p>\n";
 
 677                                                 echo "--ThisRandomString\n";
 
 683                                                 foreach($xml->file as $v) {
 
 684                                                         $x_files[] = (string)$v;
 
 686                                                 $dis = array_intersect($x_files, $files);
 
 687                                                 if(count($dis)!=count($x_files)) {
 
 688                                                         $dom = @dom_import_simplexml($xml);
 
 694                                                         while($elem = $dom->getElementsByTagName("file")->item(0)) {
 
 695                                                                 $dom->removeChild($elem);
 
 698                                                         $xml = simplexml_import_dom($dom);
 
 699                                                         array_to_xml($dis, $xml, "file");
 
 700                                                         @$xml->asXML($metadata_dir . $f);
 
 709                         $result = array("time" => intval(current_time_millis() - $time), "fixed" => $fixed, "errors" => $errors);
 
 711                 catch(PEAR_Exception $e) {
 
 713                 echo "Content-type: text/html\n\n";
 
 715                 if(is_array($result)) {
 
 716                         echo "Result of cleaning:<br/>\n";
 
 717                         echo "$fcount files checked in " . $result['time'] . "ms of which $fcount_inv was invalid<br/>";
 
 718                         echo "Fixed: " . $result['fixed'] . "<br/>";
 
 719                         echo "Errors: " . $result['errors'] . "<br/>\n";
 
 722                 else if($result=="failed") {
 
 723                         echo "It appears housecleaning failed, check your MPD settings";
 
 726                         echo "hmm.. somethings wrong, try again";
 
 728                 echo "</p><p><a href='config.php'>Back to configuration</a></p></body></html>\n";
 
 729                 echo "\n--ThisRandomString\n";
 
 733         if(!isset($iamincluded)) {
 
 734                 if(isset($_GET['cover'])) get_cover();
 
 735                 else if(isset($_GET['review'])) get_review();
 
 736                 else if(isset($_GET['lyric'])) get_lyric();
 
 737                 else if(isset($_GET['pic'])) get_pic(); 
 
 738                 else if(isset($_GET['housecleaning'])) do_houseclean();
 
 739                 else if(isset($_GET['plrecommend'])) get_recommendations_from_playlist();
 
 741                         header("Content-Type: text/xml; charset=UTF-8");
 
 742                         $xml = array_to_xml(array("result"=>"what do you want?"));
 
 751    var $arrOutput = array();
 
 756    function parse($strInputXML) {
 
 758            $this->resParser = xml_parser_create("UTF-8");
 
 760            xml_set_object($this->resParser,$this);
 
 761            xml_set_element_handler($this->resParser, "tagOpen", "tagClosed");
 
 762            xml_parser_set_option($this->resParser, XML_OPTION_TARGET_ENCODING, "UTF-8");
 
 764            xml_set_character_data_handler($this->resParser, "tagData");
 
 766            $this->strXmlData = xml_parse($this->resParser,$strInputXML );
 
 767            if(!$this->strXmlData) {
 
 768                die(sprintf("XML error: %s at line %d",
 
 769            xml_error_string(xml_get_error_code($this->resParser)),
 
 770            xml_get_current_line_number($this->resParser)));
 
 773            xml_parser_free($this->resParser);
 
 775            return $this->arrOutput;
 
 777    function tagOpen($parser, $name, $attrs) {
 
 778        $tag=array("name"=>$name,"attrs"=>$attrs);
 
 779        array_push($this->arrOutput,$tag);
 
 782    function tagData($parser, $tagData) {
 
 783            if(isset($this->arrOutput[count($this->arrOutput)-1]['tagData'])) 
 
 784                $this->arrOutput[count($this->arrOutput)-1]['tagData'] .= $tagData;
 
 786                $this->arrOutput[count($this->arrOutput)-1]['tagData'] = $tagData;
 
 789    function tagClosed($parser, $name) {
 
 790        $this->arrOutput[count($this->arrOutput)-2]['children'][] = $this->arrOutput[count($this->arrOutput)-1];
 
 791        array_pop($this->arrOutput);