| : | Javascript | : | PHP Index | : | MySQL | : |
| : | Source | : | : | Explanation | : | : | Example | : | : | Todo | : | : | Feedback | : |
A collection of classes which can retrieve information from internet radio streams such as shoutcast, icecast and steamcast
Added: 2010-07-15 18:40:14
Last update: 2010-04-23 09:18:40
<?
/***
* Copyright (c) 2005-2010, Excudo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Excudo nor the names of its contributors may be used
* to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Abstract.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Exception
*/
require_once "Exception.php";
/**
* Contains blueprint for some necessary functions as well as most of the functionality needed
* to parse the online information of an internet radiostation
*/
abstract class InternetRadio_Abstract
{
/**
* This value represents the case in which we want any possible exception to be thrown.
* @see $exceptionReporting
*/
const EXCEPTION_THROW = 1;
/**
* This value represents the case in which we want any possible exception to caught, but shown
* @see $exceptionReporting
*/
const EXCEPTION_SHOW = 2;
/**
* This value represents the case in which we want any possible exception to caught and hidden
* @see $exceptionReporting
*/
const EXCEPTION_HIDE = 3;
/**
* This setting determines what will be done when a exception occurs
* This only applies to exceptions that are thrown during the retrieval of information. Other
* exceptions, for example during the parsing of that information, will always be thrown.
*
* @see getExceptionReporting()
* @see setExceptionReporting()
*
* @var integer
*/
protected $exceptionReporting = self::EXCEPTION_HIDE;
/**
* The domain where the radiostation can be found
*
* @var String
*/
public $domain;
/**
* The port at which it can be found
*
* @var String
*/
public $port;
/**
* The path where we can find the output.
* This is the path where the server/stream information can be found
*
* @var String
*/
public $path;
/**
* This array will be filled with the applicable fields (which are different for every server-type)
* It can only contain key-value pairs of which the key also exist in the $defaultFields array
* of the implementing class
*
* @see setFields()
*
* @var array
*/
public $fields = array();
/**
* Array which is going to store different kinds of content (depending on which type of content
* is loaded). In order to display information about the radio-station, different kinds of
* content may be loaded from different paths on the server. Whenever data has been loaded, it
* will be cached in this array.
*
* @see setContentArr()
*
* @var Array
*/
protected $contentArr;
/**
* Different kinds of output-templates can be set in order to display the information with a
* certain markup. These templates can be passed as an argument whenever output is required,
* but you can set a default one as well, which is stored in this variable.
*
* @see setOutputTemplate()
*
* @var InternetRadio_Output_Interface
*/
protected $outputTemplate;
/**
* Flag to toggle the creation of hyperlinks in the output of the server-information.
* By default this is not something we want
*
* @see setCreateHyperlinks()
*
* @var boolean
*/
protected $createHyperlinks = False;
/**
* By setting this property you can indicate what the character encoding is, that
* the server uses in the public html pages with information.
* Note: an attempt will be made to auto-detect the character encoding, but this
* doesn't always work. This setting will always overrule that attempt though.
*
* @see setInputEncoding()
*
* @var String
*/
protected $inputEncoding;
/**
* By setting this property you can decide how the output should be encoded. For
* example: if the stream-information is encoded 'iso-8859-1' and the website
* on which you want to display it is 'utf-8', then you should set this property
* to 'utf-8'
*
* @see setOutputEncoding()
*
* @var String
*/
protected $outputEncoding;
/**
* An attempt will be made to auto-detect the encoding of the stream. (Currently, this
* only works for shoutcast server). When the attempt is succesfull, it will be
* saved in this property.
* Unless the $inputEncoding is set manually, this value will be treated as the
* inputEncoding
*
* @see getStreamEncoding()
*
* @var String
*/
protected $streamEncoding;
/**
* Abstract functions
*/
/**
* Implementation of this function should parse the html with server/stream info
* and fill the internal $fields array with it.
*
* @return void
*/
abstract protected function parseFields();
/**
* Implementation of this function should return what type of InternetRadiostation
* the class can handle
*
* @return String
*/
abstract public function getServerType();
/**
* Implementation of this function should assign an array to internal $contentArr
* which contains the different kinds of content we can retrieve. These types
* should be the keys in the array and their values should be null.
* When that particular type of content has been loaded, null will be overwritten
* with the retrieved content.
*
* example: $this->contentArr = array("stream" => null, "history" => null);
*
* @return void
*/
abstract protected function setContentArr();
/**
*
*
* @return void
*/
abstract protected function setPageMap();
/**
* Every extended class should define this method in such a way that by default it set the
* $streamChunks array for the different kinds of information that can be retrieved.
* The array consists of the index under which the contents of a page are cached (and loaded)
* and the value represents how many chunks of data should be loaded by the fopen() command.
* This is because sometimes the data is embedded in a(n endless) stream in which case you
* want to limit it, to prevent it loads forever.
*
* @return void
*/
abstract protected function setStreamChunks();
/**
* Public funtions
*/
/**
* Constructor.
*
* @return InternetRadio_Abstract
*/
public function __construct($url)
{
// parsing url and setting appro
$parsed_url = parse_url($url);
$this->domain = isset($parsed_url['host']) ? $parsed_url['host'] : "";
$this->port = !isset($parsed_url['port']) || empty($parsed_url['port']) ? "80" : $parsed_url['port'];
$this->path = empty($parsed_url['path']) ? "/" : $parsed_url['path'];
if (empty($this->domain))
{
$this->domain = $this->path;
$this->path = "";
}
// setting default fields
$this->setFields();
// setting default page map
$this->setPageMap();
// setting default stream-chunks
$this->setStreamChunks();
// sets the indexes for all the different kinds of content
$this->setContentArr();
if (!is_array($this->contentArr))
throw new InternetRadio_Exception("contentArr is not an array. Please implement setContentArr() properly.");
elseif (count($this->contentArr) == 0)
throw new InternetRadio_Exception("contentArr is empty. Please implement setContentArr() properly.");
else
{
foreach ($this->contentArr AS $key => $value)
{
if (!is_null($value))
throw new InternetRadio_Exception("Every value in the contentArr must have a default value of 'null'");
}
}
// setting default output template
require_once dirname(__FILE__)."/Output/Html/Table.php";
$this->setOutputTemplate(new InternetRadio_Output_Html_Table());
}
/**
* Retrieves the information about the server/stream and returns it using
* the choosen (or default) template
*
* @param String $page The page where the information about
* the stream can be found. If null, the
* default will be used. See
* @param InternetRadio_Output_Interface $template The template that has to be used when
* upon returning the data
*
* @return mixed The output as rendered by the template object. In most cases
* this will be a string, but it could just as easily be another
* type (for example, InternerRadio_Output_Php returns an array)
*/
public function getServerInfo($page = null, InternetRadio_Output_Interface $template = null)
{
try {
$this->parseFields($page);
} catch (InternetRadio_Exception $e) {
$this->error = $e->getMessage();
switch ($this->exceptionReporting)
{
case self::EXCEPTION_THROW :
throw $e;
break;
case self::EXCEPTION_SHOW :
$this->fields = array("error" => $e->getMessage());
break;
case self::EXCEPTION_HIDE :
default :
$this->fields = array("error" => "failed to load stream");
break;
}
}
return $this->getInfo($this->fields, $template);
}
/**
* Same as getServerInfo($page, InternetRadio_Output_Html_Table()), but without the
* need to pass the template object as an argument
*
* @param String $page
*
* @see getServerInfo()
*
* @return mixed
*/
public function getServerInfoAsHtml($page = null)
{
require_once dirname(__FILE__)."/Output/Html/Table.php";
return $this->getServerInfo($page, new InternetRadio_Output_Html_Table());
}
/**
* Same as getServerInfo($page, InternetRadio_Output_Php()), but without the
* need to pass the template object as an argument
*
* @param String $page
*
* @see getServerInfo()
*
* @return mixed
*/
public function getServerInfoAsPhp($page = null)
{
require_once dirname(__FILE__)."/Output/Php.php";
return $this->getServerInfo($page, new InternetRadio_Output_Php());
}
/**
* Same as getServerInfo($page, InternetRadio_Output_Xml()), but without the
* need to pass the template object as an argument
*
* @param String $page
*
* @see getServerInfo()
*
* @return mixed
*/
public function getServerInfoAsXml($page = null)
{
require_once dirname(__FILE__)."/Output/Xml.php";
return $this->getServerInfo($page, new InternetRadio_Output_Xml());
}
/**
* By passing an array with field-names, this function can exercise control over which server-information
* will be displayed when getServerInfo() is called.
* When called without arguments (or boolean false as argument), all available fields will be shown
*
* @param array $array Array of fields that we want to display when the server-info is requested
* These fields have to exist in the $defaultFields property
*
* @return void
*/
public function setFields($array=null)
{
if (is_null($array))
{
$this->fields = $this->defaultFields;
}
else
{
$tmpArr = array();
// before setting the array, we check if it contains invalid fields
foreach ($array AS $key => $value)
{
if (array_key_exists($key, $this->defaultFields))
{
$tmpArr[$key] = "n/a";
}
elseif (array_key_exists($value, $this->defaultFields))
{
$tmpArr[$value] = "n/a";
}
else
{
throw new InternetRadio_Exception($key."/".$value." is not a valid field (".implode(", ", array_keys($this->defaultFields)).")");
}
}
$this->fields = $tmpArr;
}
}
/**
* Influences how the urls in the server information are displayed.
*
* @param boolean $bool If true, the returned server-information will show urls as hyperlinks
*
* @see parseFields()
* @see getServerInfo()
*
* @return void
*/
public function setCreateHyperlinks($bool)
{
$this->createHyperlinks = (bool) $bool;
}
/**
* Getter for $outputTemplate
*
* @return InternetRadio_Output_Interface
*/
public function getOutputTemplate()
{
return $this->outputTemplate;
}
/**
* Setter for $outputTemplate
*
* @param InternetRadio_Output_Interface $template
*
* @return void
*/
public function setOutputTemplate(InternetRadio_Output_Interface $template)
{
$this->outputTemplate = $template;
}
/**
* Getter for $exceptionReporting
*
* @see self::EXCEPTION_THROW, self::EXCEPTION_SHOW, self::EXCEPTION_HIDE
*
* @return integer
*/
public function getExceptionReporting()
{
return $this->exceptionReporting;
}
/**
* Setter for $exceptionReporting
*
* @see self::EXCEPTION_THROW, self::EXCEPTION_SHOW, self::EXCEPTION_HIDE
*
* @return void
*/
public function setExceptionReporting($int)
{
$this->exceptionReporting = (int) $int;
}
/**
* Setter for the outputEncoding
*
* @see $outputEncoding
*
* @return void
*/
public function setOutputEncoding($charset)
{
$this->outputEncoding = $charset;
}
/**
* Setter for the inputEncoding
*
* @see $inputEncoding
*
* @return void
*/
public function setInputEncoding($charset)
{
$this->inputEncoding = $charset;
}
/**
* Getter for the streamEncoding
*
* @see $streamEncoding
*
* @return void
*/
public function getStreamEncoding()
{
return $this->streamEncoding;
}
/**
* internal methods
*/
/**
* Setter for the streamEncoding
* This setter is not public because only the inputEncoding may be manipulated
* when working with an object of this class.
*
* @see $streamEncoding
* @see $inputEncoding
*
* @return void
*/
protected function setStreamEncoding($charset)
{
$this->streamEncoding = $charset;
}
/**
* Wrapper method for getContents, which fills the first argument correctly
*
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @see getContents()
*
* @return String
*/
protected function getStreamContents($page = null)
{
return $this->getContents("stream", $page);
}
/**
* Loads the contents of the stream (/server) information of a radiostationg.
*
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @throws InternetRadio_Exception
*
* @see loadContents()
*
* @return void
*/
protected function loadStreamContents($page = null)
{
$this->loadContents("stream", $page);
}
/**
* This function checks if loadContents() has been called before and either returns
* the cached information if it has, or otherwise calls loadContents and then
* returns the info
*
* @param String $index This indicates what kind of content we're downloading.
* By naming it, we are able to cache it. (usually a server
* has more than 1 page with information)
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @see loadContents()
*
* @return String
*/
protected function getContents($index, $page = null)
{
if (is_null($this->contentArr[$index]))
{
$this->loadContents($index, $page);
}
return $this->contentArr[$index];
}
/**
* This method contains all the logic to download the information from the public
* pages of the internet-radiostation, which contain all the data we're interested
* in showing.
*
* Two other settings are important for correctly loading the content of a page:
* 1. The $streamChunks array. Every extended class should define this array for the
* different kinds of information that can be retrieved. The default is 20 and this will be
* if nothing else has been defined. This is because the server information is usually
* embedded in the radio-stream who's content is unlimited. By setting the streamChunks
* to -1, all the content of a certain page will be loaded. So, if the index (see index parameter)
* of a page is 'history' then you should set $streamChunks['history'] = -1 in the class.
* 2. The $pageMap array. This array can form a mapping for the type of content to the default page.
* For example, the history of a shoutcast-server is usually found at /played.html. So in the Shoutcast
* implementation of this class $pageMap['history'] = '/played.html'. This takes away the necessity
* to always pass the $page variable as an argument, while keeping the flexibility for instances
* where
*
* @param String $index This indicates what kind of content we're downloading.
* By naming it, we are able to cache it. (usually a server
* has more than 1 page with information)
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @see setPageMap()
* @see setStreamChunks()
*
* @throws InternetRadio_Exception
*
* @return void
*/
protected function loadContents($index, $page = null)
{
if (!array_key_exists($index, $this->contentArr))
{
throw new InternetRadio_Exception($index." is not a valid content-index. should be: ".implode(", ", array_keys($this->contentArr)));
}
if (is_null($page))
{
if (!isset($this->pageMap[$index]))
throw new InternetRadio_Exception("There is no default page defined for ".$index);
else
$page = $this->pageMap[$index];
}
$this->contentArr[$index] = "";
$domain = (substr($this->domain, 0, 7) == "http://") ? substr($this->domain, 7) : $this->domain;
if (@$fp = fsockopen($domain, $this->port, $this->errno, $this->errstr, 2))
{
fputs($fp, "GET ".$page." HTTP/1.1\r\n".
"User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)\r\n".
"Accept: */*\r\n".
"Host: ".$domain."\r\n\r\n");
$c = 0;
$cMax = isset($this->streamChunks[$index]) ? $this->streamChunks[$index] : 20;
while (!feof($fp) && ($cMax == -1 || $c <= $cMax))
{
$this->contentArr[$index] .= fgets($fp, 4096);
$c++;
}
fclose ($fp);
// this doesn't work for all server-types
preg_match("/(meta http-equiv=\"Content-Type\" content=\"(.*); charset=(.*)\")/iU", $this->contentArr[$index], $matches);
if (isset($matches[3]))
{
$this->setStreamEncoding($matches[3]);
}
$this->encodeContent($this->contentArr[$index]);
}
else
{
$errArr = error_get_last();
throw new InternetRadio_Exception("couldn't open stream @ ".$domain.":".$this->port."; ERROR: ".$errArr['message'].", type[".$errArr['type']."] in ".$errArr['file']." on line ".$errArr['line']);
}
}
/**
* Returns any kind of data-array using a template's render function.
* When no template is passed, if will use the default template (@see constructor)
*
* @param array $data An array containing data with information about the radio-station.
* @param InternetRadio_Output_Interface $template The template that has to be used when upon returning the data
*
* @return mixed The output as rendered by the template object. In most cases
* this will be a string, but it could just as easily be another
* type (for example, InternerRadio_Output_Php returns an array)
*/
protected function getInfo(array $data, InternetRadio_Output_Interface $template = null)
{
if (!is_null($template))
{
$oldTemplate = $this->getOutputTemplate();
$this->setOutputTemplate($template);
}
else
{
$template = $this->getOutputTemplate();
}
$return = $template->render($data);
if (isset($oldTemplate))
{
$this->setOutputTemplate($oldTemplate);
}
return $return;
}
/**
* This method encodes the passed string, based on the settings of $inputEncoding and $outputEncoding
*
* @param String $content The string which we want to encode
*
* @return void (The argument is be passed by reference)
*/
protected function encodeContent(&$content)
{
// we only continue if the outputEncoding has been set
if (!is_null($this->outputEncoding))
{
if (is_null($this->inputEncoding))
{
$this->inputEncoding = $this->streamEncoding;
}
if (!function_exists("iconv"))
{
if (empty($this->inputEncoding))
$content = mb_convert_encoding($content, $this->outputEncoding);
else
$content = mb_convert_encoding($content, $this->outputEncoding, $this->inputEncoding);
}
else
{
if (empty($this->inputEncoding))
$content = iconv(null, $this->outputEncoding, $content);
else
$content = iconv($this->inputEncoding, $this->outputEncoding, $content);
}
}
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Exception.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* Exception class for exceptions that occur within the InternetRadio classes
*/
class InternetRadio_Exception extends Exception
{
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Icecast.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Abstract
*/
require_once "Abstract.php";
/**
* Class that can parse and display the information of Icecast Servers
*/
class InternetRadio_Icecast extends InternetRadio_Abstract
{
const SERVER_TYPE = "Icecast";
/**
* All the possible fields with information about the stream/server that can be loaded
* By default, this array is assigned to the $fields (see parent class) array. But, the
* $fields array can also be set with setFields() in order to display a particular set
* of fields
*
* @var array
*
* @see $fields
* @see setFields()
*/
protected $defaultFields = array(
"Server Type" => "n/a",
"Stream Status" => "n/a",
"Listener Peak" => "n/a",
"Stream Title" => "n/a",
"Content Type" => "n/a",
"Mount Started" => "n/a",
"Stream Genre" => "n/a",
"Stream Description" => "n/a",
"Stream URL" => "n/a",
"Current Song" => "n/a",
"Current Listeners" => "n/a",
"Bitrate" => "n/a",
"Audio Info" => "n/a",
);
/**
* The default streamChunks which are loaded when setStreamChunks() is called without
* argument (which is done in the constructor of the Abstract parent classs)
*
* @var array
*/
private $defaultStreamChunks = array(
"stream" => 20,
"status" => -1
);
/**
* By default it will be assigned $defaultStreamChunks as value.
*
* @see setStreamChunks()
*
* @var array
*/
protected $streamChunks;
/**
* The default pageMaps which is loaded when setPageMap() is called without
* argument (which is done in the constructor of the Abstract parent classs)
*
* @var array
*/
private $defaultPageMap = array(
"stream" => "/",
"status" => "/status.xsl"
);
/**
* By default it will be assigned $defaultPageMap as value.
*
* @see setPageMap()
*
* @var array
*/
protected $pageMap;
/**
* When true the 'Audio Info' data will be parsed as well and saved
* in seperate bits of information: bitrate, channels & samplerate.
* Note: bitrate is already available from another field
*
* @var boolean
*/
protected $parseAudioInfo = True;
/**
* Implementation of abstract parent methods
*/
/**
* Returns the type of InternetRadiostation this class can handle
*
* @return String
*/
public function getServerType()
{
return self::SERVER_TYPE;
}
/**
* When called without argument it sets the default pageMap.
* The page map links
*
* @see parent::setPageMap()
*
* @param array $data Array containing the mapping
*
* @return void
*/
protected function setPageMap(array $data = null)
{
if (is_null($data))
$data = $this->defaultPageMap;
$this->pageMap = $data;
}
/**
* When called without argument it sets the default streamChunks.
*
* @see parent::setStreamChunks()
*
* @param array $data Array containing the mapping
*
* @return void
*/
protected function setStreamChunks($data = null)
{
if (is_null($data))
$data = $this->defaultStreamChunks;
$this->streamChunks = $data;
}
/**
* Retrieves the page with the information about the server/stream, parses it and puts the
* parsed data into the appropriate fields
*
* @see getStreamContents()
* @see loadStatus()
*/
protected function parseFields($page = null)
{
$contents = $this->getStreamContents($page);
if (False !== $contents)
{
$dataStr = str_replace("\r", "\n", str_replace("\r\n", "\n", $contents));
$lines = explode("\n", $dataStr);
foreach ($lines AS $line)
{
if ($dp = strpos($line, ":"))
{
$key = substr($line, 0, $dp);
$value = trim(substr($line, ($dp+1)));
if (preg_match("/genre/i", $key) && isset($this->fields['Stream Genre']))
$this->fields['Stream Genre'] = $value;
if (preg_match("/^server$/i", $key) && isset($this->fields['Server Type']))
$this->fields['Server Type'] = $value;
elseif (preg_match("/name/i", $key) && isset($this->fields['Stream Title']))
$this->fields['Stream Title'] = $value;
elseif (preg_match("/server/i", $key) && isset($this->fields['Stream Type']))
$this->fields['Stream Type'] = $value;
elseif (preg_match("/description/i", $key) && isset($this->fields['Stream Description']))
$this->fields['Stream Description'] = $value;
elseif (preg_match("/content-type/i", $key) && isset($this->fields['Content Type']))
$this->fields['Content Type'] = $value;
elseif (preg_match("/icy-br/i", $key))
{
if (isset($this->fields['Stream Genre']))
$this->fields['Stream Status'] = "Stream is up at ".$value."kbps";
if (isset($this->fields['Bitrate']))
$this->fields['Bitrate'] = $value;
}
elseif (preg_match("/url/i", $key) && isset($this->fields['Stream URL']))
{
if ($this->createHyperlinks)
{
$this->fields['Stream URL'] = "<a href=\"".$value."\">".$value."</a>";
}
else
{
$this->fields['Stream URL'] = $value;
}
}
elseif (preg_match("/ice-audio-info/i", $key) && isset($this->fields['Audio Info']))
{
if ($this->getParseAudioInfo())
{
$dataString = trim(str_replace("ice-", " ", $value));
$dataArr = explode("; ", $dataString);
foreach ($dataArr AS $data)
{
list($k, $v) = explode("=", $data);
if ($k == "samplerate")
$this->fields['Samplerate'] = $v;
elseif ($k == "channels")
$this->fields['Channels'] = $v;
}
unset($this->fields['Audio Info']);
}
else
{
$this->fields['Audio Info'] = str_replace("ice-", " ", $value);
}
}
}
}
if (!$this->loadStatus())
{
trigger_error("couldn't find the current stream on the icecast-status-page", E_USER_NOTICE);
}
return True;
}
else
{
return False;
}
}
/**
* This function will be called from the parent's constructor
* By setting the contentArr, it serves two purposes:
* 1. The keys of the array can later be used in validations that check if the
* index is an existing one (so, an existing key of this array)
* 2. It initializes the array with null values. This way the caching-functions
* will be able to tell if content has not been loaded or if it has been loaded
* with empty data
*
* @see parent::contentArr
* @see parent::__construct()
*
* @return void
*/
protected function setContentArr()
{
$this->contentArr = array(
"stream" => null,
"status" => null,
"history" => null,
);
}
/**
* Other methods which are specific for this internet-radiostation
*/
/**
* Getter for $parseAudioInfo
*
* @see $parseAudioInfo
*
* @return boolean
*/
public function getParseAudioInfo()
{
return $this->parseAudioInfo;
}
/**
* Setter for $parseAudioInfo
*
* @param boolean $bool
*
* @see $parseAudioInfo
*
* @return void
*/
public function setParseAudioInfo($bool)
{
$this->parseAudioInfo = (bool) $bool;
}
/**
* Loads and parses the contents of the status-page
*
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
* @param String $streamTitle The title of the stream we're interested in
* If not passed, we will retrieve it either from the cache
* or load it from the server-information page
*
* @return boolean True if status could be loaded properly
*/
protected function loadStatus($page = null, $streamTitle = null)
{
// if the streamtitle is not given, we retrieve it from the server-information
if (is_null($streamTitle))
{
// if the field is not yet or empty, we load the data first
if (!isset($this->fields['Stream Title']) || empty($this->fields['Stream Title']))
{
$this->parseFields();
if (!isset($this->fields['Stream Title']))
{
// without streamtitle, we cannot load the status
throw new InternetRadio_Exception("Stream Title couldn't be found in contents");
}
}
$streamTitle = $this->fields['Stream Title'];
}
// retrieving the contents of the status page
$contents = $this->getStatusContents($page);
if (False !== strpos($contents, "<table"))
{
// the status page contains lots of html-tables with information about the various
// available streams on this server
$tables = explode("<table", $contents);
foreach ($tables AS $table)
{
// check if the current table is the stream we're interested in
if (preg_match("/(<td(.*)>".$streamTitle."<\/td>)/", $table))
{
// this is the one, parsing it's data now
$rows = explode("<tr>", $table);
foreach ($rows AS $row)
{
if (preg_match_all("/<td.*>(.*)<\/td>/siU", $row, $matches))
{
$type = trim(str_replace(":", "", $matches[1][0]));
$value = $matches[1][1];
if ($type == "Current Song" && isset($this->fields['Current Song']))
$this->fields['Current Song'] = $value;
elseif ($type == "Current Listeners" && isset($this->fields['Current Listeners']))
$this->fields['Current Listeners'] = $value;
elseif ($type == "Peak Listeners" && isset($this->fields['Listener Peak']))
$this->fields['Listener Peak'] = $value;
elseif ($type == "Mount started" && isset($this->fields['Mount Started']))
$this->fields['Mount Started'] = $value;
}
}
return True;
}
}
}
else
{
throw new Exception($page." is not a valid status page or unreachable");
}
return False;
}
/**
* Wrapper for parent::getContents() with the first parameter filled correctly
*
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @see getContents()
*
* @return String
*/
protected function getStatusContents($page = null)
{
return $this->getContents("status", $page);
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Html/DefinitionList.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Output_Tag
*/
require_once dirname(__FILE__)."/../Tag.php";
/**
* This template class generates an definition list (<dl></dl>)
*/
class InternetRadio_Output_Html_DefinitionList extends InternetRadio_Output_Tag
{
/**
* Template-string which can be handled by setHeader() and which is
* used as the default
* @see setHeader()
*/
const DEFAULT_HEADER = "<dl class=\"internetradioList\">";
/**
* Template-string which can be handled by setFooter() and which is
* used as the default
* @see setFooter()
*/
const DEFAULT_FOOTER = "</dl>";
/**
* Template-string which can be handled by setRow() and which is
* used as the default
* @see setRow()
*/
const DEFAULT_ROW = "<dt>%s</dt><dd>%s</dd>";
/**
* Constructor.
* Sets the header, footer and row templates
*
* @param string $header Optional. If not given, the default will be used
* @param string $footer Optional. If not given, the default will be used
* @param string $row Optional. If not given, the default will be used
*
* @see setHeader()
* @see setFooter()
* @see setRow()
*/
public function __construct($header = null, $footer = null, $row = null)
{
$this->setHeader(is_null($header) ? self::DEFAULT_HEADER : $header);
$this->setFooter(is_null($footer) ? self::DEFAULT_FOOTER : $footer);
$this->setRow(is_null($row) ? self::DEFAULT_ROW : $row);
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Html/OrderedList.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Output_Tag
*/
require_once dirname(__FILE__)."/../Tag.php";
/**
* This template class generates an ordered list (<ol></ol>)
*/
class InternetRadio_Output_Html_OrderedList extends InternetRadio_Output_Tag
{
/**
* Template-string which can be handled by setHeader() and which is
* used as the default
* @see setHeader()
*/
const DEFAULT_HEADER = "<ol class=\"internetradioList\">";
/**
* Template-string which can be handled by setFooter() and which is
* used as the default
* @see setFooter()
*/
const DEFAULT_FOOTER = "</ol>";
/**
* Template-string which can be handled by setRow() and which is
* used as the default
* @see setRow()
*/
const DEFAULT_ROW = "<li>%s: %s</li>";
/**
* Constructor.
* Sets the header, footer and row templates
*
* @param string $header Optional. If not given, the default will be used
* @param string $footer Optional. If not given, the default will be used
* @param string $row Optional. If not given, the default will be used
*
* @see setHeader()
* @see setFooter()
* @see setRow()
*/
public function __construct($header = null, $footer = null, $row = null)
{
$this->setHeader(is_null($header) ? self::DEFAULT_HEADER : $header);
$this->setFooter(is_null($footer) ? self::DEFAULT_FOOTER : $footer);
$this->setRow(is_null($row) ? self::DEFAULT_ROW : $row);
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Html/Table.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Output_Tag
*/
require_once dirname(__FILE__)."/../Tag.php";
/**
* This template class generates a html-table
*/
class InternetRadio_Output_Html_Table extends InternetRadio_Output_Tag
{
/**
* Template-string which can be handled by setHeader() and which is
* used as the default
* @see setHeader()
*/
const DEFAULT_HEADER = "<table class=\"internetradioTable\"><tbody>";
/**
* Template-string which can be handled by setFooter() and which is
* used as the default
* @see setFooter()
*/
const DEFAULT_FOOTER = "</tbody></table>";
/**
* Template-string which can be handled by setRow() and which is
* used as the default
* @see setRow()
*/
const DEFAULT_ROW = "<tr><td>%s</td><td>%s</td></tr>";
/**
* Constructor.
* Sets the header, footer and row templates
*
* @param string $header Optional. If not given, the default will be used
* @param string $footer Optional. If not given, the default will be used
* @param string $row Optional. If not given, the default will be used
*
* @see setHeader()
* @see setFooter()
* @see setRow()
*/
public function __construct($header = null, $footer = null, $row = null)
{
$this->setHeader(is_null($header) ? self::DEFAULT_HEADER : $header);
$this->setFooter(is_null($footer) ? self::DEFAULT_FOOTER : $footer);
$this->setRow(is_null($row) ? self::DEFAULT_ROW : $row);
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Html/UnorderedList.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Output_Tag
*/
require_once dirname(__FILE__)."/../Tag.php";
/**
* This template class generates an unordered list (<ul></ul>)
*/
class InternetRadio_Output_Html_UnorderedList extends InternetRadio_Output_Tag
{
/**
* Template-string which can be handled by setHeader() and which is
* used as the default
* @see setHeader()
*/
const DEFAULT_HEADER = "<ul class=\"internetradioList\">";
/**
* Template-string which can be handled by setFooter() and which is
* used as the default
* @see setFooter()
*/
const DEFAULT_FOOTER = "</ul>";
/**
* Template-string which can be handled by setRow() and which is
* used as the default
* @see setRow()
*/
const DEFAULT_ROW = "<li>%s: %s</li>";
/**
* Constructor.
* Sets the header, footer and row templates
*
* @param string $header Optional. If not given, the default will be used
* @param string $footer Optional. If not given, the default will be used
* @param string $row Optional. If not given, the default will be used
*
* @see setHeader()
* @see setFooter()
* @see setRow()
*/
public function __construct($header = null, $footer = null, $row = null)
{
$this->setHeader(is_null($header) ? self::DEFAULT_HEADER : $header);
$this->setFooter(is_null($footer) ? self::DEFAULT_FOOTER : $footer);
$this->setRow(is_null($row) ? self::DEFAULT_ROW : $row);
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Interface.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* All output-templates must implement this interface to ensure that the render-method is available.
* This way, any InternetRadio_Abstract class can set an object of this type as template and call
* the render-method with the data as argument to generate the output
*/
interface InternetRadio_Output_Interface
{
/**
* Implemenation of this method must parse the passed data and convert it to the desired output
* and then return that output
*
* @return String
*/
public function render(array $data);
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Php.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Output_Interface
*/
require_once dirname(__FILE__)."/Interface.php";
/**
* This template class does nothing more than returning the passed data-array
* It exists because the only way to force alternate output is to use a template
* class of type InternetRadio_Output
*/
class InternetRadio_Output_Php implements InternetRadio_Output_Interface
{
/**
* @param array $data Array which we don't convert, but return immediately
*
* @return array
*/
public function render(array $data)
{
return $data;
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Tag.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Output_Interface
*/
require_once dirname(__FILE__)."/Interface.php";
/**
* This class serves as the base for all other InternetRadio_Output classes which generate output with tags
* For example, output in html, xml, etc.
*/
class InternetRadio_Output_Tag implements InternetRadio_Output_Interface
{
/**
* Top-part of the output, which is not variable
*
* @see getHeader()
* @see setHeader()
*
* @var String
*/
protected $_header;
/**
* Bottom-part of the output, which is not variable
*
* @see getFooter()
* @see setFooter()
*
* @var String
*/
protected $_footer;
/**
* Body-part of the output, which Is variable and occurs 1 + n times
*
* @see getRow()
* @see setRow()
*
* @var String
*/
protected $_row;
/**
* This indicates that the data (to be rendered) is passed as key-value pairs
* (in which the key-part has significance too and needs to be shown)
*
* @var boolean
*/
protected $_keyValue;
/**
* A positive value indicates that we need to skip a few lines before starting the output
*
* @see setOffset()
*
* @var integer
*/
protected $_offset;
/**
* A positive value indicates we need to limit the amount of rows in the output
* -1 indicates there is no limit (and so does null)
*
* @see setLimit()
*
* @var integer
*/
protected $_limit;
/**
*
*
* @return String
*/
public function render(array $data)
{
// initiate variable that will hold the variable-data
$body = "";
// initiate variable that will count the amount of rows we're parsing
$count = 0;
// initiate variable that will count the amount of rows we're adding as variable data
$added = 0;
// calculating the real limit we're gonna use
if (!is_null($this->_limit) && $this->_limit < 0)
{
// setting the limit to 'limitless'
$limit = count($data) - (int) $this->_offset + $this->_limit;
}
else
{
$limit = $this->_limit;
}
$this->sanitizeData($data);
// looping through the data-array
foreach ($data AS $key => $value)
{
// check if we are within the boundaries defined by the offset & limit
if ( (is_null($this->_offset) || $count >= $this->_offset) && (is_null($limit) || $added < $limit))
{
// if true, we want to display both key and value
if ($this->_keyValue)
{
$body .= sprintf($this->_row, $key, $value);
}
else
{
$body .= sprintf($this->_row, $value);
}
// row has been added, we increment
$added++;
}
else
{
echo "<!-- ".$count." is outside boundaries; offset[".$this->_offset."], limit[".$limit."] //-->\n";
}
// row has been parsed, we increment
$count++;
}
// returning the rendered output
return $this->getHeader().$body.$this->getFooter();
}
protected function sanitizeData(&$data)
{
$newData = array();
foreach ($data AS $key => $value)
{
// check if the current value is an array itself
// and in that case we extract it's key and value
if (is_array($value))
{
list($key, $value) = array_values($value);
}
$newData[$key] = $value;
}
$data = $newData;
}
/**
* @see $_limit
* @return void
*/
public function setLimit($limit)
{
$this->_limit = (int) $limit;
}
/**
* @see $_offset
* @return void
*/
public function setOffset($offset)
{
$this->_offset = (int) $offset;
}
/**
* @see $_header
* @return String
*/
public function getHeader()
{
return $this->_header;
}
/**
* @see $_header
* @return void
*/
public function setHeader($str)
{
$this->_header = $str;
}
/**
* @see $_footer
* @return String
*/
public function getFooter()
{
return $this->_footer;
}
/**
* @see $_footer
* @return void
*/
public function setFooter($str)
{
$this->_footer = $str;
}
/**
* @see $_row
* @return String
*/
public function getRow()
{
return $this->_row;
}
/**
* @see $_row
* @return void
*/
public function setRow($str)
{
if (False === strpos($str, "%s"))
{
throw new Exception("There are no variables in the row of your table. Please use %s as variable");
}
$this->_keyValue = preg_match("/%s.*%s/s", $str);
$this->_row = $str;
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Output/Xml.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Output_Tag
*/
require_once dirname(__FILE__)."/Tag.php";
/**
* This template class generates xml-output
*/
class InternetRadio_Output_Xml extends InternetRadio_Output_Tag
{
/**
* Template-string which can be handled by setHeader() and which is
* used as the default
* @see setHeader()
*/
const DEFAULT_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<data>\n";
/**
* Template-string which can be handled by setFooter() and which is
* used as the default
* @see setFooter()
*/
const DEFAULT_FOOTER = "</data>";
/**
* Template-string which can be handled by setRow() and which is
* used as the default
* @see setRow()
*/
const DEFAULT_ROW = "\t<item>\n\t\t<name>%s</name>\n\t\t<value>%s</value>\n\t</item>\n";
/**
* Constructor.
* Sets the header, footer and row templates
*
* @param string $header Optional. If not given, the default will be used
* @param string $footer Optional. If not given, the default will be used
* @param string $row Optional. If not given, the default will be used
*
* @see setHeader()
* @see setFooter()
* @see setRow()
*/
public function __construct($header = null, $footer = null, $row = null)
{
$this->setHeader(is_null($header) ? self::DEFAULT_HEADER : $header);
$this->setFooter(is_null($footer) ? self::DEFAULT_FOOTER : $footer);
$this->setRow(is_null($row) ? self::DEFAULT_ROW : $row);
}
/**
* Overloads the parent function so it can create valid xml-values
*
* @param array $data Array which we want to convert into xml
*
* @see parent::render()
*
* @return String (xml)
*/
public function render(array $data)
{
$this->sanitizeData($data);
$newData = array();
foreach ($data AS $key => $value)
{
$newData[htmlspecialchars(utf8_encode($key))] = htmlspecialchars(utf8_encode($value));
}
return parent::render($newData);
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Shoutcast.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Abstract
*/
require_once "Abstract.php";
/**
* Class that can parse and display the information of Shoutcast Servers
*/
class InternetRadio_Shoutcast extends InternetRadio_Abstract
{
/**
* Used by getServerType() to show what kind of internet-radiostations this class
* can handle
*/
const SERVER_TYPE = "Shoutcast";
/**
* The following constants all serve the function which parses the public information
* pages. This way, it is easy to change in case the format of these pages change in
* the future
*/
/**
* Determines from which point we start searching for the fields with server information
* @see parseFields();
*/
const OFFSET = "Current Stream Information";
/**
* This is how the html-table with the track-history starts
* @see parseHistory();
* @see TABLE_END
*/
const TABLE_START = "<table border=0 cellpadding=2 cellspacing=2>";
/**
* End this is how it ends
* @see parseHistory();
* @see TABLE_START
*/
const TABLE_END = "</table>";
/**
* All the possible fields with information about the stream/server that can be loaded
* By default, this array is assigned to the $fields (see parent class) array. But, the
* $fields array can also be set with setFields() in order to display a particular set
* of fields
*
* @var array
*
* @see $fields
* @see setFields()
*/
protected $defaultFields = array(
"Server Status" => "n/a",
"Stream Status" => "n/a",
"Listener Peak" => "n/a",
"Average Listen Time" => "n/a",
"Stream Title" => "n/a",
"Content Type" => "n/a",
"Stream Genre" => "n/a",
"Stream URL" => "n/a",
"Current Song" => "n/a"
);
/**
* The default streamChunks which are loaded when setStreamChunks() is called without
* argument (which is done in the constructor of the Abstract parent classs)
*
* @var array
*/
private $defaultStreamChunks = array(
"history" => -1,
);
/**
* By default it will be assigned $defaultStreamChunks as value.
*
* @see setStreamChunks()
*
* @var array
*/
protected $streamChunks;
/**
* The default pageMaps which is loaded when setPageMap() is called without
* argument (which is done in the constructor of the Abstract parent classs)
*
* @var array
*/
private $defaultPageMap = array(
"stream" => "/",
"history" => "/played.html",
);
/**
* By default it will be assigned $defaultPageMap as value.
*
* @see setPageMap()
*
* @var array
*/
protected $pageMap;
/**
* Implementation of abstract parent methods
*/
/**
* Returns the type of InternetRadiostation this class can handle
*
* @return String
*/
public function getServerType()
{
return self::SERVER_TYPE;
}
/**
* When called without argument it sets the default pageMap.
* The page map links
*
* @see parent::setPageMap()
*
* @param array $data Array containing the mapping
*
* @return void
*/
public function setPageMap($data = null)
{
if (is_null($data))
$data = $this->defaultPageMap;
$this->pageMap = $data;
}
/**
* When called without argument it sets the default streamChunks.
*
* @see parent::setStreamChunks()
*
* @param array $data Array containing the mapping
*
* @return void
*/
protected function setStreamChunks($data = null)
{
if (is_null($data))
$data = $this->defaultStreamChunks;
$this->streamChunks = $data;
}
/**
* Retrieves the page with the information about the server/stream, parses it and puts the
* parsed data into the appropriate fields
*
* @see getStreamContents()
* @see loadStatus()
*
* @return void
*/
public function parseFields($page = null)
{
if ($contents = $this->getStreamContents($page))
{
// parsing the contents
$very_first_pos = stripos($contents, self::OFFSET);
foreach ($this->fields AS $item => $value)
{
$first_pos = stripos($contents, $item, $very_first_pos);
$line_start = strpos($contents, "<td>", $first_pos);
$line_end = strpos($contents, "</td>", $line_start) + 4;
$difference = $line_end - $line_start;
$line = substr($contents, $line_start, $difference);
$this->fields[$item] = strip_tags($line);
if ($this->createHyperlinks && strtolower(substr($item, -3)) == "url")
{
$this->fields[$item] = "<a href=\"".$this->fields[$item]."\">".$this->fields[$item]."</a>";
}
}
}
}
/**
* This function will be called from the parent's constructor
* By setting the contentArr, it serves two purposes:
* 1. The keys of the array can later be used in validations that check if the
* index is an existing one (so, an existing key of this array)
* 2. It initializes the array with null values. This way the caching-functions
* will be able to tell if content has not been loaded or if it has been loaded
* with empty data
*
* @see parent::contentArr
* @see parent::__construct()
*
* @return void
*/
protected function setContentArr()
{
$this->contentArr = array(
"stream" => null,
"history" => null,
);
}
/**
* overloaded methods
*/
/***
* Overloads the parents function in order to cut of part of the output which is irrelevant
*
* @var String $page Optional. The page where we can find the content.
* If none is given, the $pageMap variable will be used
*
* @see parent::loadStreamContents()
*
* @return void
*/
protected function loadContents($index, $page = null)
{
parent::loadContents($index, $page);
if ($index == "stream")
{
if (!empty($this->contentArr['stream']))
{
preg_match("/(Content-Type:)(.*)/i", $this->contentArr['stream'], $matches);
if (count($matches) > 0)
{
$contentType = trim($matches[2]);
if ($contentType != "text/html")
{
throw new Exception("This is not a valid shoutcast-stream");
}
}
}
}
}
/**
* Other methods which are specific for this internet-radiostation
*/
/**
* Retrieves the history of the played tracks and returns it using the choosen
* (or default) template
*
* @param unknown_type $page The page where the information about
* the stream can be found. If null, the
* default will be used. See
* @param InternetRadio_Output_Interface $template The template that has to be used when
* upon returning the data
*
* @return mixed The output as rendered by the template object. In most cases
* this will be a string, but it could just as easily be another
* type (for example, InternerRadio_Output_Php returns an array)
*/
public function getHistoryInfo($page = null, InternetRadio_Output_Interface $template = null)
{
if (empty($this->tracks))
{
try {
$this->parseHistory($page);
} catch (InternetRadio_Exception $e) {
$this->error = $e->getMessage();
switch ($this->exceptionReporting)
{
case self::EXCEPTION_THROW :
throw $e;
break;
case self::EXCEPTION_SHOW :
$this->tracks = array(array("error", $e->getMessage()));
break;
case self::EXCEPTION_HIDE :
default :
$this->tracks = array(array("error", "failed to load stream"));
break;
}
}
}
return $this->getInfo($this->tracks, $template);
}
/**
* Same as getHistoryInfo($page, InternetRadio_Output_Html_Table()), but without the
* need to pass the template object as an argument
*
* @param String $page
*
* @see getHistoryInfo()
*
* @return mixed
*/
public function getHistoryInfoAsHtml($page = null)
{
require_once dirname(__FILE__)."/Output/Html/Table.php";
return $this->getHistoryInfo($page, new InternetRadio_Output_Html_Table());
}
/**
* Same as getHistoryInfo($page, InternetRadio_Output_Php()), but without the
* need to pass the template object as an argument
*
* @param String $page
*
* @see getHistoryInfo()
*
* @return mixed
*/
public function getHistoryInfoAsPhp($page = null)
{
require_once dirname(__FILE__)."/Output/Php.php";
return $this->getHistoryInfo($page, new InternetRadio_Output_Php());
}
/**
* Same as getHistoryInfo($page, InternetRadio_Output_Xml()), but without the
* need to pass the template object as an argument
*
* @param String $page
*
* @see getHistoryInfo()
*
* @return mixed
*/
public function getHistoryInfoAsXml($page = null)
{
require_once dirname(__FILE__)."/Output/Xml.php";
return $this->getHistoryInfo($page, new InternetRadio_Output_Xml());
}
/**
* Retrieves the page with the information about the history of the played tracks,
* parses it and craetes an array of tracks out of the parsed data
*
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @return void
*/
protected function parseHistory($page = null)
{
$html = $this->getHistoryContents($page);
$fromPos = stripos($html, self::TABLE_START);
if (false == $fromPos)
{
// fix for newer shoutcastservers, which quote the values of the html-attributes
$tableStart = preg_replace("/([0-2])/", "\"\\1\"", self::TABLE_START);
$fromPos = stripos($html, $tableStart);
}
$toPos = stripos($html, self::TABLE_END, $fromPos);
$tableData = substr($html, $fromPos, ($toPos-$fromPos));
$lines = explode("</tr><tr>", $tableData);
$tracks = array();
$c = 0;
foreach ($lines AS $line)
{
$info = explode ("</td><td>", $line);
$time = trim(strip_tags($info[0]));
if (substr($time, 0, 9) != "Copyright" && !preg_match("/Tag Loomis, Tom Pepper and Justin Frankel/i", $info[1]))
{
$this->tracks[$c]['time'] = $time;
$this->tracks[$c++]['track'] = trim(strip_tags($info[1]));
}
}
if (count($this->tracks) > 0)
{
unset($this->tracks[0]);
if (isset($this->tracks[1]))
$this->tracks[1]['track'] = str_replace("Current Song", "", $this->tracks[1]['track']);
}
}
/**
* Loads the contents of the information about the history of the played tracks
*
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @throws InternetRadio_Exception
*
* @see loadContents()
*
* @return void
*/
protected function loadHistoryContents($page = null)
{
$this->loadContents("history", $page);
}
/**
* Wrapper for parent::getContents() with the first parameter filled correctly
*
* @param String $page The location of the page with the content. This is the
* [page] part in http://[domain]:[port]/[page]
*
* @see getContents()
*
* @return String
*/
protected function getHistoryContents($page = null)
{
return $this->getContents("history", $page);
}
}
/*-*-*-*-*-*-*-*-*-*-*- file : InternetRadio/Steamcast.php -*-*-*-*-*-*-*-*-*-*-*/
/**
* @see InternetRadio_Abstract
*/
require_once "Abstract.php";
/**
* Class that can parse and display the information of Steamcast Servers
*/
class InternetRadio_Steamcast extends InternetRadio_Abstract
{
/**
* Used by getServerType() to show what kind of internet-radiostations this class
* can handle
*/
const SERVER_TYPE = "Steamcast";
/**
* The following constants all serve the function which parses the public information
* pages. This way, it is easy to change in case the format of these pages change in
* the future
*/
/**
* Determines from which point we start searching for the fields with server information
* @see parseFields();
*/
const OFFSET = "about.html";
/**
* This is how the html-table with the track-history starts
* @see parseHistory();
* @see TABLE_END
*/
const TABLE_START = "<table align=center>";
/**
* End this is how it ends
* @see parseHistory();
* @see TABLE_START
*/
const TABLE_END = "</table>";
/**
* All the possible fields with information about the stream/server that can be loaded
* By default, this array is assigned to the $fields (see parent class) array. But, the
* $fields array can also be set with setFields() in order to display a particular set
* of fields
*
* @var array
*
* @see $fields
* @see setFields()
*/
protected $defaultFields = array(
"Stream Status" => "n/a",
"Stream Name" => "n/a",
"Stream Url" => "n/a",
"Genre" => "n/a",
"Content Type" => "n/a",
"Stream Bitrate" => "n/a",
"Listeners" => "n/a",
"Average Listening Time" => "n/a",
"Overall Listeners" => "n/a",
"Peak Listeners" => "n/a",
"Tune-ins" => "n/a",
"5min Tune-ins" => "n/a",
"Now Playing" => "n/a",
"Active Url" => "n/a",
);
/**
* The default streamChunks which are loaded when setStreamChunks() is called without
* argument (which is done in the constructor of the Abstract parent classs)
*
* @var array
*/
private $defaultStreamChunks = array(
"history" => -1,
"status" => -1
);
/**
* By default it will be assigned $defaultStreamChunks as value.
*
* @see setStreamChunks()
*
* @var array
*/
protected $streamChunks;
/**
* The default pageMaps which is loaded when setPageMap() is called without
* argument (which is done in the constructor of the Abstract parent classs)
*
* @var array
*/
private $defaultPageMap = array(
"stream" => "/live.mp3",
"history" => "/played.html",
"status" => "/",
);
/**
* By default it will be assigned $defaultPageMap as value.
*
* @see setPageMap()
*
* @var array
*/
protected $pageMap;
/**
* Implementation of abstract parent methods
*/
/**
* Returns the type of InternetRadiostation this class can handle
*
* @return String
*/
public function getServerType()
{
return self::SERVER_TYPE;
}
/**
* When called without argument it sets the default pageMap.
* The page map links
*
* @see parent::setPageMap()
*
* @param array $data Array containing the mapping
*
* @return void
*/
protected function setPageMap($data = null)
{
if (is_null($data))
$data = $this->defaultPageMap;
$this->pageMap = $data;
}
/**
* When called without argument it sets the default streamChunks.
*
* @see parent::setStreamChunks()
*
* @param array $data Array containing the mapping
*
* @return void
*/
protected function setStreamChunks($data = null)
{
if (is_null($data))
$data = $this->defaultStreamChunks;
$this->streamChunks = $data;
}
/**
* Retrieves the page with the information about the server/stream, parses it and puts the
* parsed data into the appropriate fields
*
* @see getStreamContents()
* @see loadStatus()
*/
public function parseFields($page = null)
{
if ($contents = $this->getStreamContents($page))
{
$very_first_pos = stripos($contents, self::OFFSET);
foreach ($this->defaultFields AS $item => $value)
{
$first_pos = stripos($contents, $item, $very_first_pos);
$line_start = strpos($contents, "</td>", $first_pos);
$line_end = strpos($contents, "</tr>", $line_start) + 5;
$difference = $line_end - $line_start;
$line = substr($contents, $line_start, $difference);
$this->fields[$item] = strip_tags($line);
if ($this->createHyperlinks && strtolower(substr($item, -3)) == "url")
{
$this->fields[$item] = "<a href=\"".$this->fields[$item]."\">".$this->fields[$item]."</a>";
}
}
$this->loadStatus();
}
}
/**
* This function will be called from the parent's constructor
* By setting the contentArr, it serves two purposes:
* 1. The keys of the array can later be used in validations that check if the
* index is an existing one (so, an existing key of this array)
* 2. It initializes the array with null values. This way the caching-functions
* will be able to tell if content has not been loaded or if it has been loaded
* with empty data
*
* @see parent::contentArr
* @see parent::__construct()
*
* @return void
*/
protected function setContentArr()
{
$this->contentArr = array(
"stream" => null,
"status" => null,
"history" => null,
);
}
/**
* overloaded methods
*/
/***
* Overloads the parents function in order to cut of part of the output which is irrelevant
*
* @var String $page Optional. The page where we can find the content.
* If none is given, the $pageMap variable will be used
*
* @see parent::loadStreamContents()
*
* @return void
*/
protected function loadContents($index, $page = null)
{
parent::loadContents($index, $page);
if (!empty($this->contentArr['stream']))
{
preg_match("/(Content-Type:)(.*)/i", $this->contentArr['stream'], $matches);
if (count($matches) > 0)
{
$contentType = trim($matches[2]);
if ($contentType != "text/html")
{
throw new Exception("This is not a valid shoutcast-stream");
}
}
}
}
/**
* Other methods which are specific for this internet-radiostation
*/
/**
* Retrieves the history of the played tracks and returns it using the choosen
* (or default) template
*
* @param unknown_type $page The page where the information about
* the stream can be found. If null, the
* default will be used. See
* @param InternetRadio_Output_Interface $template The template that has to be used when
* upon returning the data
*
* @return mixed The output as rendered by the template object. In most cases
* this will be a string, but it could just as easily be another
* type (for example, InternerRadio_Output_Php returns an array)
*/
public function getHistoryInfo($page = "/played.html", InternetRadio_Output_Interface $template = null)
{
if (empty($this->tracks))
{
try {
$this->parseHistory($page);
} catch (InternetRadio_Exception $e) {
$this->error = $e->getMessage();
switch ($this->exceptionReporting)
{
case self::EXCEPTION_THROW :
throw $e;
break;
case self::EXCEPTION_SHOW :
$this->tracks = array(array("error", $e->getMessage()));
break;
case self::EXCEPTION_HIDE :
default :
$this->tracks = array(array("error", "failed to load stream"));
break;
}
}
}
return $this->getInfo($this->tracks, $template);
}