* Based on TMXInfoFetcher & http://united.tm-exchange.com/main.aspx?action=threadshow&id=619302 * * v1.7: Added Countable interface to searcher class * v1.6: Fixed an error checking bug * v1.5: Improved error reporting via $error * v1.4: Added User-Agent to the GET request * v1.3: Renamed $worldrec into $custimg to correct its meaning; fixed * $replayid check * v1.2: Fixed TMXInfo processing false $track * v1.1: Optimized get_file URL parsing * v1.0: Initial release */ class TMXInfoSearcher implements Iterator,Countable { public $error; protected $tracks = array(); private $section; private $prefix; /** * Searches TMX for tracks matching name, author and/or environment; * or search TMX for the 10 most recent tracks * * @param String $game * TMX section for 'TMO', 'TMS', 'TMN', 'TMU', 'TMNF' * @param String $name * The track name to search for (partial, case-insensitive match) * @param String $author * The track author to search for (partial, case-insensitive match) * @param String $env * The environment to search for (exact case-insensitive match * from: Desert, Snow, Rally, Bay, Coast, Island, Stadium); * ignored when searching TMN or TMNF * @param Boolean $recent * If true, ignore search parameters and just return 10 newest tracks * (max. one per author) * @return TMXInfoSearcher * If ->valid() is false, no matching track was found; * otherwise, an iterator of TMXInfo objects for a 'foreach' loop. * Returns at most 500 tracks ($maxpage * 20) for TMNF/TMU(F), * and at most 20 tracks for the other TMX sections. */ public function __construct($game, $name, $author, $env, $recent) { $this->section = $game; switch ($game) { case 'TMO': $this->prefix = 'original'; break; case 'TMS': $this->prefix = 'sunrise'; break; case 'TMN': $this->prefix = 'nations'; $env = ''; // ignore possible environment break; case 'TMU': $this->prefix = 'united'; break; case 'TMNF': $this->prefix = 'tmnforever'; $env = ''; // ignore possible environment break; default: $this->prefix = ''; $this->error = 'Unknown TMX section: ' . $game; return; } $this->error = ''; if ($recent) { $this->tracks = $this->getRecent(); } else { $this->tracks = $this->getList($name, $author, $env); } } // __construct // define standard Iterator functions public function rewind() { reset($this->tracks); } public function current() { return new TMXInfo($this->section, $this->prefix, current($this->tracks)); } public function next() { return new TMXInfo($this->section, $this->prefix, next($this->tracks)); } public function key() { return key($this->tracks); } public function valid() { return (current($this->tracks) !== false); } // define standard Countable function public function count() { return count($this->tracks); } private function getRecent() { // get 10 most recent tracks $url = 'http://' . $this->prefix . '.tm-exchange.com/apiget.aspx?action=apirecent'; $file = $this->get_file($url); if ($file === false) { $this->error = 'Connection or response error on ' . $url; return array(); } else if ($file === -1) { $this->error = 'Timed out while reading data from ' . $url; return array(); } else if ($file == '') { $this->error = 'No data returned from ' . $url; return array(); } // check for API error message if (strpos($file, chr(27)) !== false) { $this->error = 'Cannot decode recent track info from ' . $url; return array(); } // return list of tracks as array of strings return explode("\r\n", $file); } // getRecent private function getList($name, $author, $env) { // older TMX sections don't support multi-page results :( $maxpage = 1; if ($this->prefix == 'tmnforever' || $this->prefix == 'united') $maxpage = 25; // max. 500 tracks // compile search URL $url = 'http://' . $this->prefix . '.tm-exchange.com/apiget.aspx?action=apisearch'; if ($name != '') $url .= '&track=' . $name; if ($author != '') $url .= '&author=' . $author; if ($env != '') $url .= '&env=' . $env; $url .= '&page='; $tracks = ''; $page = 0; $done = false; // get results 20 tracks at a time while ($page < $maxpage && !$done) { $file = $this->get_file($url . $page); if ($file === false) { $this->error = 'Connection or response error on ' . $url; return array(); } else if ($file === -1) { $this->error = 'Timed out while reading data from ' . $url; return array(); } else if ($file == '') { if ($tracks == '') { $this->error = 'No data returned from ' . $url; return array(); } else { break; } } // check for API error message if (strpos($file, chr(27)) !== false) { $this->error = 'Cannot decode searched track info from ' . $url; return array(); } // check for results if ($file != '') { // no line break before first page $tracks .= ($page++ > 0 ? "\r\n" : '') . $file; } else { $done = true; } } // return list of tracks as array of strings if ($tracks != '') return explode("\r\n", $tracks); else return array(); } // getList // Simple HTTP Get function with timeout // ok: return string || error: return false || timeout: return -1 private function get_file($url) { $url = parse_url($url); $port = isset($url['port']) ? $url['port'] : 80; $query = isset($url['query']) ? "?" . $url['query'] : ""; $fp = @fsockopen($url['host'], $port, $errno, $errstr, 4); if (!$fp) return false; fwrite($fp, 'GET ' . $url['path'] . $query . " HTTP/1.0\r\n" . 'Host: ' . $url['host'] . "\r\n" . 'User-Agent: TMXInfoSearcher (' . PHP_OS . ")\r\n\r\n"); stream_set_timeout($fp, 2); $res = ''; $info['timed_out'] = false; while (!feof($fp) && !$info['timed_out']) { $res .= fread($fp, 512); $info = stream_get_meta_data($fp); } fclose($fp); if ($info['timed_out']) { return -1; } else { if (substr($res, 9, 3) != '200') return false; $page = explode("\r\n\r\n", $res, 2); return trim($page[1]); } } // get_file } // class TMXInfoSearcher class TMXInfo { public $section, $prefix, $id, $name, $userid, $author, $type, $envir, $mood, $style, $routes, $length, $diffic, $lbrating, $awards, $comments, $custimg, $game, $uploaded, $updated, $pageurl, $replayid, $replayurl, $imageurl, $thumburl, $dloadurl; /** * Returns track object with a hell of a lot of data from TMX track string * * @param String $section * TMX section * @param String $prefix * TMX URL prefix * @param String $track * The TMX track string from TMXInfoSearcher * @return TMXInfo */ public function TMXInfo($section, $prefix, $track) { $this->section = $section; $this->prefix = $prefix; if ($track) { // separate columns on Tabs $fields = explode(chr(9), $track); $this->id = $fields[0]; $this->name = $fields[1]; $this->userid = $fields[2]; $this->author = $fields[3]; $this->type = $fields[4]; $this->envir = $fields[5]; $this->mood = $fields[6]; $this->style = $fields[7]; $this->routes = $fields[8]; $this->length = $fields[9]; $this->diffic = $fields[10]; $this->lbrating = ($fields[11] > 0 ? $fields[11] : 'Classic!'); $this->awards = $fields[12]; $this->comments = $fields[13]; $this->custimg = (strtolower($fields[14]) == 'true'); $this->game = $fields[15]; $this->replayid = $fields[16]; // unknown = $fields[17-21]; $this->uploaded = $fields[22]; $this->updated = $fields[23]; $this->pageurl = 'http://' . $prefix . '.tm-exchange.com/main.aspx?action=trackshow&id=' . $this->id; $this->imageurl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=trackscreen&id=' . $this->id; $this->thumburl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=trackscreensmall&id=' . $this->id; $this->dloadurl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=trackgbx&id=' . $this->id; if ($this->replayid > 0) { $this->replayurl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=recordgbx&id=' . $this->replayid; } else { $this->replayurl = ''; } } } // TMXInfo } // class TMXInfo ?>