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) {
106 $stype = array("Title", "Keywords");
107 $artist = urlencode($artist);
108 $album = urlencode($album);
109 foreach($stype as $st) {
112 $xml = amazonlink($params);
113 if($xml&&isset($xml->Items[0])&&isset($xml->Items[0]->Item[0]))
119 /* returns file pointer or false */
120 function get_album_lock($artist, $album) {
121 $file_name = get_album_info_name($artist, $album);
122 $exists = file_exists($file_name);
126 $fp = @fopen($file_name, "r+");
127 else $fp = @fopen($file_name, "w+");
128 if($fp && flock($fp, LOCK_EX))
131 trigger_error("Can't lock album-file: $file_name", E_USER_WARNING);
135 /* waits for appropriate amazon time, have to be called before making any amazon requests
136 returns true if ok to continue otherwise false */
137 function amazon_wait() {
138 global $metadata_dir;
140 /* rationale behind this:
141 * amazon requires that we don't make more than one request pr. second pr. ip */
143 $file_name = $metadata_dir . "amazon_time";
144 if(file_exists($file_name))
145 $fp = @fopen($file_name, "r+");
146 else $fp = @fopen($file_name, "w+");
149 trigger_error("Can't open amazon_time", E_USER_WARNING);
152 if(!flock($fp, LOCK_EX)) {
154 trigger_error("Can't lock amazon_time", E_USER_WARNING);
158 $last = fp_get_contents($fp);
161 if(is_numeric($last)) {
162 $stime = current_time_millis() - $last;
164 $stime = abs($stime);
166 usleep($stime*1000); // micro seconds
169 if(@fwrite($fp, current_time_millis())===false) {
171 trigger_error("Can't write to amazon_time", E_USER_WARNING);
180 /* returns artist and album info and get's album lock or dies */
181 /* return value: array($fp, $artist, $album) */
182 function init_album_artist_or_die() {
184 header("Content-Type: text/xml; charset=UTF-8");
188 if(isset($_GET['artist'])&&isset($_GET['album']) &&
189 strlen(trim($_GET['artist']))>0&&strlen(trim($_GET['album']))>0) {
190 $album = trim($_GET['album']);
191 $artist = trim($_GET['artist']);
194 $xml = array_to_xml(array("result" => "missingparam"));
199 $fp = get_album_lock($artist, $album);
202 $xml = array_to_xml(array("result" => "failed"));
206 return array($fp, $artist, $album);
209 /* returns array(artist, album, filename) or false */
210 function get_current_info() {
212 $pl = get_playback();
214 $info = $pl->getCurrentSong();
215 if(isset($info['Artist'])&&isset($info['Title'])) {
216 $artist = trim($info['Artist']);
217 $title = trim($info['Title']);
218 $file_name = $info['file'];
219 return array($artist, $title, $file_name);
224 catch(PEARException $e) {
230 function get_cover() {
231 global $COVER_SEARCH_AGAIN, $cover_providers;
233 list($fp, $artist, $album) = init_album_artist_or_die();
235 $xml = fp_get_contents($fp);
237 $xml = @simplexml_load_string($xml);
240 if(isset($xml->notfound)&&is_numeric((string)$xml->notfound[0])) {
241 $time = @intval((string)$xml->notfound[0]);
242 if($time+$COVER_SEARCH_AGAIN<time())
245 else if(!isset($xml->image[0])&&!isset($xml->thumbnail[0])) {
250 $xml->addChild("cached", "true");
260 foreach($cover_providers as $cp) {
261 $res = $cp($artist, $album);
262 if($res&&is_array($res))
267 if($res&&is_array($res)) {
268 foreach($res as $key => $val) {
269 if(!isset($xml->$key))
270 $xml->$key = (string)$val;
274 $xml->notfound = time();
278 if($res&&is_array($res)) {
279 $res['time'] = time();
280 $xml = array_to_xml($res);
283 $xml = array("notfound" => time());
284 $xml = array_to_xml($xml);
288 @fwrite($fp, $xml->asXML());
295 function get_review() {
296 global $COVER_SEARCH_AGAIN;
298 list($fp, $artist, $album) = init_album_artist_or_die();
300 $xml = fp_get_contents($fp);
311 $xml = @simplexml_load_string($xml);
313 if(isset($xml->rnotfound)&&is_numeric((string)$xml->rnotfound[0])) {
314 $time = @intval((string)$xml->rnotfound[0]);
315 if($time+$COVER_SEARCH_AGAIN>time())
321 if(!$xml||(!(isset($xml->review[0])||isset($xml->desc[0]))&&!$no_search)) {
324 echo array_to_xml(array("result" => "failed"))->asXML();
328 if($xml&&isset($xml->asin[0])) {
329 $res = amazonlink(array("Operation"=>"ItemLookup", "IdType"=>"ASIN", "ItemId"=>"urlencode($xml->asin[0])"));
333 $res = @amazon_album_query(array("Operation"=>"ItemSearch", "SearchIndex"=>"Music", "Artist"=>"$artist", "Album"=>"$album"));
336 if($res&&isset($res->Items[0])&&isset($res->Items[0]->Item[0])) {
337 $p = $res->Items[0]->Item[0];
338 $asin = (string) $p->ASIN;
339 if(isset($p->EditorialReviews[0])) {
340 $p = $p->EditorialReviews[0];
341 foreach($p->EditorialReview as $er) {
342 if(!$desc&&"Album Description" == (string)$er->Source) {
343 $desc = (string) $er->Content;
346 $review_src = (string) $er->Source;
347 $review = (string) $er->Content;
351 /* set info in xml-file... */
354 $xml->review_src = htmlspecialchars($review_src);
355 $xml->review = htmlspecialchars($review);
358 $xml->desc = htmlspecialchars($desc);
360 if(!isset($xml->asin[0])) {
361 $xml->addChild("asin", $asin);
364 if(!$review&&!$desc) {
373 $xml['asin'] = $asin;
375 $xml['desc'] = $desc;
377 $xml['review_src'] = $review_src;
378 $xml['review'] = $review;
382 $xml = array_to_xml($xml);
395 $xml->addChild("cached", "true");
400 if(isset($xml->rnotfound)) {
401 $xml->rnotfound = time();
404 $xml->addChild("rnotfound", time());
406 @fwrite($fp, $xml->asXML());
409 @fwrite($fp, $xml->asXML());
413 $xml = array_to_xml(array("rnotfound" => time()));
414 @fwrite($fp, $xml->asXML());
421 /* artist, title and song file name in system */
422 function _get_lyric_lyricwiki($artist, $title, $file_name) {
423 $file = get_lyric_filename($artist, $title);
424 $fp = fsockopen("lyricwiki.org", 80);
426 $xml = array_to_xml(array("result"=>"connectionfailed"));
427 return $xml->asXML();
430 $out = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n";
431 $out .= "<SOAP-ENV:Envelope SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" ";
432 $out .= "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" ";
433 $out .= "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" ";
434 $out .= "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
435 $out .= "xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\" ";
436 $out .= "xmlns:tns=\"urn:LyricWiki\">";
437 $out .= "<SOAP-ENV:Body><tns:getSong xmlns:tns=\"urn:LyricWiki\">";
438 $out .= "<artist xsi:type=\"xsd:string\">";
439 $out .= htmlspecialchars($artist);
441 $out .= "<song xsi:type=\"xsd:string\">";
442 $out .= htmlspecialchars($title);
444 $out .= "</tns:getSong></SOAP-ENV:Body></SOAP-ENV:Envelope>\r\n";
446 $head = "POST /server.php HTTP/1.1\r\n";
447 $head .= "Host: lyricwiki.org\r\n";
448 $head .= "SOAPAction: urn:LyricWiki#getSong\r\n";
449 $head .= "Content-Type: text/xml; charset=UTF-8\r\n";
450 $head .= "User-Agent: RemissPitchfork/0.1\r\n";
451 $head .= "Content-Length: " . str_byte_count($out) . "\r\n";
452 $head .= "Connection: Close\r\n\r\n";
454 fwrite($fp, $head . $out);
456 $responseHeader = "";
457 /* assume everything is dandy */
459 $responseHeader.= fread($fp, 1);
461 while (!preg_match('/\\r\\n\\r\\n$/', $responseHeader));
465 $ret .= fgets($fp, 128);
468 /* stupid hack to get around wrong xml declearation */
470 if(strpos($ret, "<". $qmark . "xml version=\"1.0\" encoding=\"ISO-8859-1\"".$qmark.">")===0)
471 $ret = str_replace("<". $qmark . "xml version=\"1.0\" encoding=\"ISO-8859-1\"".$qmark.">",
472 "<". $qmark . "xml version=\"1.0\" encoding=\"UTF-8\"".$qmark.">",
477 $parser = new xml2array();
478 $parser->parse($ret);
479 $data = find_lyrics($parser->arrOutput);
480 // check that data is ok and lyrics exist
481 if($data&&isset($data[2]['tagData'])) {
483 foreach($data as $d) {
484 if($d['name']=="ARTIST")
485 $res['artist'] = $d['tagData'];
486 else if($d['name']=="SONG")
487 $res['title'] = $d['tagData'];
488 else if($d['name']=="LYRICS")
489 $res['lyric'] = $d['tagData'];
490 else if($d['name']=="URL")
491 $res['url'] = $d['tagData'];
493 $res['from'] = "lyricwiki.org";
494 $res['time'] = time();
495 /* this caching thing will have to be extracted if we
496 * put in another lyrics source */
497 if(trim($res['lyric'])&&trim($res['lyric'])!="Not found") {
498 $xml = array_to_xml(array("result" => $res));
499 $xml->addChild("file", htmlspecialchars($file_name));
500 $res = $xml->asXML();
501 @file_put_contents($file, $res);
504 $out = array("result" => "notfound");
505 if(isset($res['url']))
506 $out['url'] = $res['url'];
507 $res = array_to_xml($out);
508 $res = $res->asXML();
515 /* $file: filename of cached version
516 * $file_name: file name of song */
517 function _get_lyric_cache($file, $file_name) {
518 $xml = @simplexml_load_file($file);
521 if(isset($xml->file)) {
522 foreach($xml->file as $f) {
523 if(((string)$f)==$file_name)
528 $xml->addChild("file", htmlspecialchars($file_name));
529 @file_put_contents($file, $xml->asXML());
531 $xml->addChild("cached", "true");
532 return $xml->asXML();
537 function get_lyric($info = null) {
538 header("Content-Type: text/xml; charset=UTF-8");
541 $info = get_current_info();
543 $xml = array_to_xml(array("result"=>"failed"));
549 $file_name = $info[2];
551 $file = get_lyric_filename($artist, $title);
552 if(file_exists($file)&&!isset($_GET['force'])) {
553 $xml = _get_lyric_cache($file, $file_name);
560 $xml = _get_lyric_lyricwiki($artist, $title, $file_name);
565 echo array_to_xml(array("result" => "failed"))->asXML();
573 $b_name = basename(trim($_GET['pic']));
574 $name = $cover_dir . $b_name;
575 if(file_exists($name)&&is_readable($name)) {
576 if(function_exists("finfo_open")&&function_exists("finfo_file")) {
577 $f = finfo_open(FILEINFO_MIME);
578 header("Content-Type: " . finfo_file($f, $name));
580 else if(function_exists("mime_content_type")) {
581 header("Content-Type: " . mime_content_type($name));
584 header("Content-Type: image/jpeg");
586 $c = "Content-Disposition: inline; filename=\"";
587 $c .= rawurlencode($b_name) . "\"";
589 echo @file_get_contents($name);
594 echo "File does not exist\n";
595 trigger_error("Did not find albumart althought it was requested", E_USER_WARNING);
600 function get_recommendations_from_playlist() {
601 require_once("../player/openstrands.php");
602 $pl = get_playlist();
603 $list = $pl->getPlaylistInfo();
605 foreach($list as $song) {
606 if(isset($song['Artist'])&&$song['Artist'])
607 $artist[$song['Artist']] = true;
609 $artist = array_keys(array_change_key_case($artist));
612 header("Content-Type: text/xml; charset=UTF-8");
614 $ret = strands_get_recommendations($artist);
616 if(!$ret || ! count($ret)) {
617 $res['result'] = is_array($ret)?"notfound":"failed";
618 echo array_to_xml($res)->asXML();
621 $db = get_database();
622 foreach($ret as $a) {
625 $tmp['album'] = $db->getMetadata("Album", "Artist", $a);
628 $out = array("result" => $res);
630 echo array_to_xml($out)->asXML();
633 function do_houseclean() {
634 /* this is a *very* inefficient method, but it's needed... */
635 //header("Content-Type: text/xml; charset=UTF-8");
636 header("Content-type: multipart/x-mixed-replace;boundary=--ThisRandomString");
638 global $metadata_dir;
640 echo "--ThisRandomString\n";
641 $out = "Content-type: text/html\n\n".
642 "<html><head><title>Housecleaning</title></head><body>\n".
643 "<p>Performing housecleaning, please wait...</p>\n";
645 echo "$out--ThisRandomString\n";
648 set_time_limit(300); // this might take a while, but
649 // shouldn't be more than 5 mins even on slow machines
650 $db = get_database();
653 $time = current_time_millis();
654 $list = $db->getAll();
655 if(!isset($list['file']))
657 $files = $list['file'];
659 $list = scandir($metadata_dir);
660 $total = count($list);
666 foreach($list as $f) {
667 $r = strrpos($f, ".lyric");
669 if($r!==false&&$r+6==strlen($f)) {
670 $xml = @simplexml_load_file($metadata_dir . $f);
672 if($fcount%100 == 0) {
674 echo "<p>Processed $fcount (".(int)($tcount*100/$total)."%)..</p>\n";
675 echo "--ThisRandomString\n";
681 foreach($xml->file as $v) {
682 $x_files[] = (string)$v;
684 $dis = array_intersect($x_files, $files);
685 if(count($dis)!=count($x_files)) {
686 $dom = @dom_import_simplexml($xml);
692 while($elem = $dom->getElementsByTagName("file")->item(0)) {
693 $dom->removeChild($elem);
696 $xml = simplexml_import_dom($dom);
697 array_to_xml($dis, $xml, "file");
698 @$xml->asXML($metadata_dir . $f);
707 $result = array("time" => intval(current_time_millis() - $time), "fixed" => $fixed, "errors" => $errors);
709 catch(PEAR_Exception $e) {
711 echo "Content-type: text/html\n\n";
713 if(is_array($result)) {
714 echo "Result of cleaning:<br/>\n";
715 echo "$fcount files checked in " . $result['time'] . "ms of which $fcount_inv was invalid<br/>";
716 echo "Fixed: " . $result['fixed'] . "<br/>";
717 echo "Errors: " . $result['errors'] . "<br/>\n";
720 else if($result=="failed") {
721 echo "It appears housecleaning failed, check your MPD settings";
724 echo "hmm.. somethings wrong, try again";
726 echo "</p><p><a href='config.php'>Back to configuration</a></p></body></html>\n";
727 echo "\n--ThisRandomString\n";
731 if(!isset($iamincluded)) {
732 if(isset($_GET['cover'])) get_cover();
733 else if(isset($_GET['review'])) get_review();
734 else if(isset($_GET['lyric'])) get_lyric();
735 else if(isset($_GET['pic'])) get_pic();
736 else if(isset($_GET['housecleaning'])) do_houseclean();
737 else if(isset($_GET['plrecommend'])) get_recommendations_from_playlist();
739 header("Content-Type: text/xml; charset=UTF-8");
740 $xml = array_to_xml(array("result"=>"what do you want?"));
749 var $arrOutput = array();
754 function parse($strInputXML) {
756 $this->resParser = xml_parser_create("UTF-8");
758 xml_set_object($this->resParser,$this);
759 xml_set_element_handler($this->resParser, "tagOpen", "tagClosed");
760 xml_parser_set_option($this->resParser, XML_OPTION_TARGET_ENCODING, "UTF-8");
762 xml_set_character_data_handler($this->resParser, "tagData");
764 $this->strXmlData = xml_parse($this->resParser,$strInputXML );
765 if(!$this->strXmlData) {
766 die(sprintf("XML error: %s at line %d",
767 xml_error_string(xml_get_error_code($this->resParser)),
768 xml_get_current_line_number($this->resParser)));
771 xml_parser_free($this->resParser);
773 return $this->arrOutput;
775 function tagOpen($parser, $name, $attrs) {
776 $tag=array("name"=>$name,"attrs"=>$attrs);
777 array_push($this->arrOutput,$tag);
780 function tagData($parser, $tagData) {
781 if(isset($this->arrOutput[count($this->arrOutput)-1]['tagData']))
782 $this->arrOutput[count($this->arrOutput)-1]['tagData'] .= $tagData;
784 $this->arrOutput[count($this->arrOutput)-1]['tagData'] = $tagData;
787 function tagClosed($parser, $name) {
788 $this->arrOutput[count($this->arrOutput)-2]['children'][] = $this->arrOutput[count($this->arrOutput)-1];
789 array_pop($this->arrOutput);