├── .htaccess ├── README.md ├── composer.json ├── config.php ├── gracenote-php ├── Gracenote.class.php ├── GracenoteError.class.php ├── HTTP.class.php └── register.php ├── icecast_api.php ├── index.php └── storage ├── albums └── 404.jpg ├── artists ├── 200fe6d767fdb847177be9a32f517e8e.jpg └── 8780f1b8319cd5a65180ce25a0fd4f73.jpg └── default └── 404.jpg /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Some hosts may require you to use the `RewriteBase` directive. 4 | # If you need to use the `RewriteBase` directive, it should be the 5 | # absolute physical path to the directory that contains this htaccess file. 6 | # 7 | # RewriteBase / 8 | 9 | RewriteCond %{REQUEST_FILENAME} !-f 10 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IceCast2 PHP API 2 | 3 | ## Warning: This project is EOL, outdated and has been abandoned. Use it at your own risk. 4 | 5 | ## Overview 6 | This is fully functional, easy-to-use, RESTful API interface for your icecast2-based radio station. 7 | It's based on a popular Slim Framework which makes it very flexible and reliable solution. 8 | 9 | Featues: 10 | * Easy to configure. 11 | * Integration with memcached for high performance. 12 | * Rapid deployment within a minute. 13 | * Multiple mountpoint support! 14 | * Easy to extend. 15 | * Different response types: JSON or XML. 16 | * Mount fallback support! 17 | * It is awesome! 18 | 19 | It allows you to: 20 | * Show number of listeners per mountpoint. 21 | * Show current track per mountpoint with timestamp. 22 | * Show last N tracks per mountpoiint alwo with their timestamps. 23 | * Show total number of current listeners online. 24 | * Show album art via GraceNote for current track. 25 | 26 | ## Install 27 | It's never been so easy if you're using composer. 28 | Just unpack it to your api's root directory, and install the dependencies: 29 | 30 | ```php composer.phar install``` 31 | 32 | ## Configuration 33 | After all dependencies are installed, it's the time to configure our new API. 34 | Well, it's pretty simple, just edit the $config array: 35 | ``` 36 | //IceCast API Config 37 | $config = array( 38 | 'icecast_server_hostname' => 'radio.example.com', //icecast2 server hostname or IP 39 | 'icecast_server_port' => 80, 40 | 'icecast_admin_username' => 'admin', //admin username 41 | 'icecast_admin_password' => 'hackme', //admin password 42 | 43 | 44 | //unused 45 | 'icecast_listener_auth_header_title' => 'icecast-auth-user', 46 | 'icecast_listener_auth_header_value' => '1', 47 | 'icecast_listener_auth_header_reject_reason' => 'Rejected', 48 | 49 | //If you have an event based mounts(e.g. for live broadcasting), 50 | //you should configure fallback map below according to your icecast2 config file. 51 | //Read the docs for more info. 52 | 'icecast_mount_fallback_map' => array('live' => 'nonstop', // from => to 53 | 'trance' => 'trance.nonstop', 54 | 'house' => 'house.nonstop'), 55 | 56 | 'playlist_logfile' => '/var/log/icecast2/playlist.log', // must be available for reading 57 | 58 | 'use_memcached' => false, // Enable memcached support: true | false 59 | 'use_db' => false, // Enable db support: true | false (unused atm) 60 | 61 | 'memcached' => array('host' => '127.0.0.1', 62 | 'port' => 11211, 63 | 'lifetime' => 5, // lifetime of the cache in seconds 64 | 'compressed' => 0), // compress data stored with memcached? 1 or 0. Requires zlib. 65 | 66 | 'db' => array('host' => '127.0.0.1', 67 | 'port' => 3306, 68 | 'user' => 'dbuser', 69 | 'password' => 'dbpassword'), 70 | 'max_amount_of_history' => '20', // max limit of requested items of playback history 71 | 'xmlrootnode' => 'response', // Root node name for the response using XML. 72 | 'album_art_folder' => getcwd().'/storage/albums/', // cache folder for albums art images. With trailing slash. Normally, u shouldn't change this. 73 | 'gracenote' => array('clientID' => '', 74 | 'clientTag' => '', 75 | 'userID' => '', 76 | ), 77 | 'default_storage_folder' => getcwd().'/storage/default/', // default static folder. Normally, u shouldn't change this. 78 | ); 79 | 80 | ``` 81 | ## Mount Fallback map 82 | I think every popular radiostation hosts a live broadcasts. But with all it's popularity, it comes with some problems, if you're using default Icecast2 fallback mechanic. 83 | When live source hits the air, listeners are being automatically moved to its mountpoint, leaving the old nonstop mount completely empty. 84 | In order to continue providing actual data to your API clients you need to detect when live broadcast is going up and alter your data "on-the-fly". 85 | 86 | To bring this thing to work you need to configure Mount Fallback map according to your station's archeticture. 87 | 88 | So, for example, if you have live(for DJs) mount called "live" with following configuration in icecast.xml: 89 | ``` 90 | 91 | /live 92 | MyRadio Main RJ Stream 93 | /myradio.nonstop 94 | 1 95 | 2048 96 | pwd 97 | 98 | ``` 99 | Just bring the `icecast_mount_fallback_map` to the following state: 100 | ``` 101 | 'icecast_mount_fallback_map' => array('live' => 'myradio.nonstop'), 102 | ``` 103 | That' all. Now your API service will provide data from the live mount when it's active or from the one it's associated with, if it's down. 104 | 105 | 106 | ## Getting an album and artist art from Gracenote for your current track 107 | Now, this API also allows you to show an album cover wherever you want. But, you have to do some steps in order to setup this feature. 108 | * First, go to https://developer.gracenote.com/ and make yourself an account if dont have one. 109 | * After creating your application, gracenote will provide you with clientTag and clientID. 110 | * Add them to your config file and launch yourapihost.com/gracenote-php/register.php . 111 | * If everything went OK, you should get your userID key. Save it to your config file. 112 | That's it. 113 | For example, you can try requesting youapihost.com/cover/Nickelback/Lullaby for album image, and youapihost.com/cover/Nickelback for artist image. 114 | 115 | Dont forget to make storage folders writable. 116 | Also note, that when album cover image is pulled from gracenote' api for the first time, API then saves it on the filesystem, making subsequent request on same image much faster. 117 | 118 | ## Performance 119 | Thanks to built-in memcached support your new api service has, quite literally, unrival performance. 120 | 121 | Here are some tests result: 122 | 123 | Query: `ab -n 10000 -c 100 http://api.example.com/radio/live/history/7/xml/ 124 | 125 | ### Memcached OFF 126 | ``` 127 | Server Software: nginx/0.7.67 128 | 129 | Document Path: /radio/live/history/7/xml/ 130 | Document Length: 697 bytes 131 | 132 | Concurrency Level: 100 133 | Time taken for tests: 24.540 seconds 134 | Complete requests: 10000 135 | Failed requests: 0 136 | Write errors: 0 137 | Total transferred: 25380000 bytes 138 | HTML transferred: 23410000 bytes 139 | Requests per second: 407.49 [#/sec] (mean) 140 | Time per request: 245.405 [ms] (mean) 141 | Time per request: 2.454 [ms] (mean, across all concurrent requests) 142 | Transfer rate: 1009.97 [Kbytes/sec] received 143 | ``` 144 | ### Memcached ON 145 | ``` 146 | Server Software: nginx/0.7.67 147 | 148 | Document Path: /radio/live/history/7/xml/ 149 | Document Length: 682 bytes 150 | 151 | 152 | Concurrency Level: 100 153 | Time taken for tests: 6.134 seconds 154 | Complete requests: 10000 155 | Failed requests: 0 156 | Write errors: 0 157 | Total transferred: 25380000 bytes 158 | HTML transferred: 23410000 bytes 159 | Requests per second: 1630.16 [#/sec] (mean) 160 | Time per request: 61.344 [ms] (mean) 161 | Time per request: 0.613 [ms] (mean, across all concurrent requests) 162 | Transfer rate: 4040.39 [Kbytes/sec] received 163 | ``` 164 | 165 | All tests were made on the following server: 166 | ``` 167 | CPU Intel Quad Xeon E3-1230 4 x 3.20 Ghz 168 | RAM 12 GB 169 | Web-server: nginx 0.7 with php5-fpm 170 | ``` 171 | 1630 RPS against 407. 172 | Not bad, huh? Whatcha think? 173 | 174 | ## Requirements 175 | To make everything run smoothly, you need to have the following software installed: 176 | 177 | * PHP 5.3 >= 178 | * cURL 179 | * libXML 180 | * (optional) memcached & memcache 181 | 182 | 183 | 184 | ## Demos 185 | So, the best demo is the working project, right? 186 | This API is fully integrated and succesfuly working at the most popular russian 187 | online gaming station called "Tort.FM". It was developed for that project, eventually. 188 | 189 | Here you go: 190 | ### Current listeners from tort.fm main mountpoint, xml response: 191 | 192 | ### Current track from tort.fm main mountpoint, json response: 193 | 194 | ### Last 7 tracks from our trance channel, xml response: 195 | 196 | ### Total listeners, xml response: 197 | 198 | ### Album art, jpg response: 199 | 200 | ### Artist art, jpg response: 201 | 202 | 203 | ## Extend 204 | If you want to add your custom functionality, just create additional methods in icecast_api.php file using this template: 205 | ``` 206 | private function YourCustomMethodAction(array $args){ 207 | return array('Hello' => 'Im a template for your custom methods.'); 208 | } 209 | ``` 210 | Note: There is a strict rules applied to the names of your methods. It has to have the following format: {methodname}Action. 211 | 212 | Then create new route block inside index.php file like this: 213 | ``` 214 | $app->get('/customMethod/:variable/:responseType(/)', function ($variable, $responseType) use ($icecastApi, $app) { 215 | 216 | $app->response()->header("Content-Type", "application/".$responseType); 217 | echo $icecastApi->Request('YourCustomMethod',array('your_var' => $variable))->Response($responseType); 218 | 219 | })->conditions(array("responseType" => "(json|xml)")); 220 | ``` 221 | This is it. You can find these templates within the files aswell. 222 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "2.*" 4 | } 5 | } -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 'radio.example.com', //icecast2 server hostname or IP 5 | 'icecast_server_port' => 80, 6 | 'icecast_admin_username' => 'admin', //admin username 7 | 'icecast_admin_password' => 'hackme', //admin password 8 | 9 | 10 | //unused 11 | 'icecast_listener_auth_header_title' => 'icecast-auth-user', 12 | 'icecast_listener_auth_header_value' => '1', 13 | 'icecast_listener_auth_header_reject_reason' => 'Rejected', 14 | 15 | //If you have an event based mounts(e.g. for live broadcasting), 16 | //you should configure fallback map below according to your icecast2 config file. 17 | //Read the docs for more info. 18 | 'icecast_mount_fallback_map' => array('live' => 'nonstop', // from => to 19 | 'trance' => 'trance.nonstop', 20 | 'house' => 'house.nonstop'), 21 | 22 | 'playlist_logfile' => '/var/log/icecast2/playlist.log', // must be available for reading 23 | 'fix_non_utf8_encoding' => true, //if set to true, the system will try to convert all non-UTF characters into proper string. 24 | 'mp3_title_charset' => 'cp1251', //If you have any songs metadata which contains non-UTF symbols(like cyrillic titles, etc) set this to your local charset value (cp1251 for cyrillic). 25 | 26 | 'use_memcached' => false, // Enable memcached support: true | false 27 | 'use_db' => false, // Enable db support: true | false (unused atm) 28 | 29 | 'memcached' => array('host' => '127.0.0.1', 30 | 'port' => 11211, 31 | 'lifetime' => 5, // lifetime of the cache in seconds 32 | 'compressed' => 0), // compress data stored with memcached? 1 or 0. Requires zlib. 33 | 34 | 'db' => array('host' => '127.0.0.1', 35 | 'port' => 3306, 36 | 'user' => 'dbuser', 37 | 'password' => 'dbpassword'), 38 | 'max_amount_of_history' => '20', // max limit of requested items of playback history 39 | 'xmlrootnode' => 'response', // Root node name for the response using XML. 40 | 'album_art_folder' => getcwd().'/storage/albums/', // cache folder for albums art images. With trailing slash. Normally, u shouldnt change this. 41 | 'artist_art_folder' => getcwd().'/storage/artists/', 42 | 'gracenote' => array('clientID' => '', 43 | 'clientTag' => '', 44 | 'userID' => '', 45 | ), 46 | 'default_storage_folder' => getcwd().'/storage/default/', 47 | ); 48 | 49 | ?> 50 | -------------------------------------------------------------------------------- /gracenote-php/Gracenote.class.php: -------------------------------------------------------------------------------- 1 | _clientID = $clientID; 33 | $this->_clientTag = $clientTag; 34 | $this->_userID = $userID; 35 | $this->_apiURL = str_replace("[[CLID]]", $this->_clientID, $this->_apiURL); 36 | } 37 | 38 | // Will register your clientID and Tag in order to get a userID. The userID should be stored 39 | // in a persistent form (filesystem, db, etc) otherwise you will hit your user limit. 40 | public function register($clientID = null) 41 | { 42 | // Use members from constructor if no input is specified. 43 | if ($clientID === null) { $clientID = $this->_clientID."-".$this->_clientTag; } 44 | 45 | // Make sure user doesn't try to register again if they already have a userID in the ctor. 46 | if ($this->_userID !== null && $this->_userID != '') 47 | { 48 | echo "Warning: You already have a userID, no need to register another. Using current ID.\n"; 49 | return $this->_userID; 50 | } 51 | 52 | // Do the register request 53 | $request = " 54 | 55 | ".$clientID." 56 | 57 | "; 58 | $http = new HTTP($this->_apiURL); 59 | $response = $http->post($request); 60 | $response = $this->_checkResponse($response); 61 | 62 | // Cache it locally then return to user. 63 | $this->_userID = (string)$response->RESPONSE->USER; 64 | return $this->_userID; 65 | } 66 | 67 | // Queries the Gracenote service for a track 68 | public function searchTrack($artistName, $albumTitle, $trackTitle, $matchMode = self::ALL_RESULTS) 69 | { 70 | // Sanity checks 71 | if ($this->_userID === null) { $this->register(); } 72 | 73 | $body = $this->_constructQueryBody($artistName, $albumTitle, $trackTitle, "", "ALBUM_SEARCH", $matchMode); 74 | $data = $this->_constructQueryRequest($body); 75 | return $this->_execute($data); 76 | } 77 | 78 | // Queries the Gracenote service for an artist. 79 | public function searchArtist($artistName, $matchMode = self::ALL_RESULTS) 80 | { 81 | return $this->searchTrack($artistName, "", "", $matchMode); 82 | } 83 | 84 | // Queries the Gracenote service for an album. 85 | public function searchAlbum($artistName, $albumTitle, $matchMode = self::ALL_RESULTS) 86 | { 87 | return $this->searchTrack($artistName, $albumTitle, "", $matchMode); 88 | } 89 | 90 | // This looks up an album directly using it's Gracenote identifier. Will return all the 91 | // additional GOET data. 92 | public function fetchAlbum($gn_id) 93 | { 94 | // Sanity checks 95 | if ($this->_userID === null) { $this->register(); } 96 | 97 | $body = $this->_constructQueryBody("", "", "", $gn_id, "ALBUM_FETCH"); 98 | $data = $this->_constructQueryRequest($body, "ALBUM_FETCH"); 99 | return $this->_execute($data); 100 | } 101 | 102 | // This retrieves ONLY the OET data from a fetch, and nothing else. Will return an array of that data. 103 | public function fetchOETData($gn_id) 104 | { 105 | // Sanity checks 106 | if ($this->_userID === null) { $this->register(); } 107 | 108 | $body = "".$gn_id." 109 | 113 | "; 117 | 118 | $data = $this->_constructQueryRequest($body, "ALBUM_FETCH"); 119 | $request = new HTTP($this->_apiURL); 120 | $response = $request->post($data); 121 | $xml = $this->_checkResponse($response); 122 | 123 | $output = array(); 124 | $output["artist_origin"] = ($xml->RESPONSE->ALBUM->ARTIST_ORIGIN) ? $this->_getOETElem($xml->RESPONSE->ALBUM->ARTIST_ORIGIN) : ""; 125 | $output["artist_era"] = ($xml->RESPONSE->ALBUM->ARTIST_ERA) ? $this->_getOETElem($xml->RESPONSE->ALBUM->ARTIST_ERA) : ""; 126 | $output["artist_type"] = ($xml->RESPONSE->ALBUM->ARTIST_TYPE) ? $this->_getOETElem($xml->RESPONSE->ALBUM->ARTIST_TYPE) : ""; 127 | return $output; 128 | } 129 | 130 | // Fetches album metadata based on a table of contents. 131 | public function albumToc($toc) 132 | { 133 | // Sanity checks 134 | if ($this->_userID === null) { $this->register(); } 135 | 136 | $body = "".$toc.""; 137 | 138 | $data = $this->_constructQueryRequest($body, "ALBUM_TOC"); 139 | return $this->_execute($data); 140 | } 141 | 142 | //////////////////////////////////////////////////////////////////////////////////////////////// 143 | 144 | // Simply executes the query to Gracenote WebAPI 145 | protected function _execute($data) 146 | { 147 | $request = new HTTP($this->_apiURL); 148 | $response = $request->post($data); 149 | return $this->_parseResponse($response); 150 | } 151 | 152 | // This will construct the gracenote query, adding in the authentication header, etc. 153 | protected function _constructQueryRequest($body, $command = "ALBUM_SEARCH") 154 | { 155 | return 156 | " 157 | 158 | ".$this->_clientID."-".$this->_clientTag." 159 | ".$this->_userID." 160 | 161 | 162 | ".$body." 163 | 164 | "; 165 | } 166 | 167 | // Constructs the main request body, including some default options for metadata, etc. 168 | protected function _constructQueryBody($artist, $album = "", $track = "", $gn_id = "", $command = "ALBUM_SEARCH", $matchMode = self::ALL_RESULTS) 169 | { 170 | $body = ""; 171 | 172 | // If a fetch scenario, user the Gracenote ID. 173 | if ($command == "ALBUM_FETCH") 174 | { 175 | $body .= "".$gn_id.""; 176 | } 177 | // Otherwise, just do a search. 178 | else 179 | { 180 | // Only get the single best match if that's what the user wants. 181 | if ($matchMode == self::BEST_MATCH_ONLY) { $body .= "SINGLE_BEST"; } 182 | 183 | // If a search scenario, then need the text input 184 | if ($artist != "") { $body .= "".$artist.""; } 185 | if ($track != "") { $body .= "".$track.""; } 186 | if ($album != "") { $body .= "".$album.""; } 187 | } 188 | 189 | // Include extended data. 190 | $body .= ""; 194 | 195 | // Include more detailed responses. 196 | $body .= ""; 200 | 201 | // Only want the thumbnail cover art for now (LARGE,XLARGE,SMALL,MEDIUM,THUMBNAIL) 202 | $body .= ""; 206 | 207 | return $body; 208 | } 209 | 210 | // Check the response for any Gracenote API errors. 211 | protected function _checkResponse($response = null) 212 | { 213 | // Response is in XML, so attempt to load into a SimpleXMLElement. 214 | $xml = null; 215 | try 216 | { 217 | $xml = new \SimpleXMLElement($response); 218 | } 219 | catch (Exception $e) 220 | { 221 | throw new GNException(GNError::UNABLE_TO_PARSE_RESPONSE); 222 | } 223 | 224 | // Get response status code. 225 | $status = (string) $xml->RESPONSE->attributes()->STATUS; 226 | 227 | // Check for any error codes and handle accordingly. 228 | switch ($status) 229 | { 230 | case "ERROR": throw new GNException(GNError::API_RESPONSE_ERROR, (string) $xml->MESSAGE); break; 231 | case "NO_MATCH": throw new GNException(GNError::API_NO_MATCH); break; 232 | default: 233 | if ($status != "OK") { throw new GNException(GNError::API_NON_OK_RESPONSE, $status); } 234 | } 235 | 236 | return $xml; 237 | } 238 | 239 | // This parses the API response into a PHP Array object. 240 | protected function _parseResponse($response) 241 | { 242 | // Parse the response from Gracenote, check for errors, etc. 243 | try 244 | { 245 | $xml = $this->_checkResponse($response); 246 | } 247 | catch (SAPIException $e) 248 | { 249 | // If it was a no match, just give empty array back 250 | if ($e->getCode() == SAPIError::GRACENOTE_NO_MATCH) 251 | { 252 | return array(); 253 | } 254 | 255 | // Otherwise, re-throw the exception 256 | throw $e; 257 | } 258 | 259 | // If we get to here, there were no errors, so continue to parse the response. 260 | $output = array(); 261 | foreach ($xml->RESPONSE->ALBUM as $a) 262 | { 263 | $obj = array(); 264 | 265 | // Album metadata 266 | $obj["album_gnid"] = (string)($a->GN_ID); 267 | $obj["album_artist_name"] = (string)($a->ARTIST); 268 | $obj["album_title"] = (string)($a->TITLE); 269 | $obj["album_year"] = (string)($a->DATE); 270 | $obj["genre"] = $this->_getOETElem($a->GENRE); 271 | $obj["album_art_url"] = (string)($this->_getAttribElem($a->URL, "TYPE", "COVERART")); 272 | 273 | // Artist metadata 274 | $obj["artist_image_url"] = (string)($this->_getAttribElem($a->URL, "TYPE", "ARTIST_IMAGE")); 275 | $obj["artist_bio_url"] = (string)($this->_getAttribElem($a->URL, "TYPE", "ARTIST_BIOGRAPHY")); 276 | $obj["review_url"] = (string)($this->_getAttribElem($a->URL, "TYPE", "REVIEW")); 277 | 278 | // If we have artist OET info, use it. 279 | if ($a->ARTIST_ORIGIN) 280 | { 281 | $obj["artist_era"] = $this->_getOETElem($a->ARTIST_ERA); 282 | $obj["artist_type"] = $this->_getOETElem($a->ARTIST_TYPE); 283 | $obj["artist_origin"] = $this->_getOETElem($a->ARTIST_ORIGIN); 284 | } 285 | // If not available, do a fetch to try and get it instead. 286 | else 287 | { 288 | $obj = array_merge($obj, $this->fetchOETData((string)($a->GN_ID))); 289 | } 290 | 291 | // Parse track metadata if there is any. 292 | foreach($a->TRACK as $t) 293 | { 294 | $track = array(); 295 | 296 | $track["track_number"] = (int)($t->TRACK_NUM); 297 | $track["track_gnid"] = (string)($t->GN_ID); 298 | $track["track_title"] = (string)($t->TITLE); 299 | $track["track_artist_name"] = (string)($t->ARTIST); 300 | 301 | // If no specific track artist, use the album one. 302 | if (!$t->ARTIST) { $track["track_artist_name"] = $obj["album_artist_name"]; } 303 | 304 | $track["mood"] = $this->_getOETElem($t->MOOD); 305 | $track["tempo"] = $this->_getOETElem($t->TEMPO); 306 | 307 | // If track level GOET data exists, overwrite metadata from album. 308 | if (isset($t->GENRE)) { $obj["genre"] = $this->_getOETElem($t->GENRE); } 309 | if (isset($t->ARTIST_ERA)) { $obj["artist_era"] = $this->_getOETElem($t->ARTIST_ERA); } 310 | if (isset($t->ARTIST_TYPE)) { $obj["artist_type"] = $this->_getOETElem($t->ARTIST_TYPE); } 311 | if (isset($t->ARTIST_ORIGIN)) { $obj["artist_origin"] = $this->_getOETElem($t->ARTIST_ORIGIN); } 312 | 313 | $obj["tracks"][] = $track; 314 | } 315 | 316 | $output[] = $obj; 317 | } 318 | return $output; 319 | } 320 | 321 | // A helper function to return the child node which has a certain attribute value. 322 | private function _getAttribElem($root, $attribute, $value) 323 | { 324 | foreach ($root as $r) 325 | { 326 | $attrib = $r->attributes(); 327 | if ($attrib[$attribute] == $value) { return $r; } 328 | } 329 | } 330 | 331 | // A helper function to parse OET data into an array 332 | private function _getOETElem($root) 333 | { 334 | $array = array(); 335 | foreach($root as $data) 336 | { 337 | $array[] = array("id" => (int)($data["ID"]), 338 | "text" => (string)($data)); 339 | } 340 | return $array; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /gracenote-php/GracenoteError.class.php: -------------------------------------------------------------------------------- 1 | _extInfo = $extInfo; 13 | //echo("exception: code=" . $code . ", message=" . GNError::getMessage($code) . ", ext=" . $extInfo . "\n"); 14 | } 15 | 16 | public function getExtraInfo() { return $this->_extInfo; } 17 | } 18 | 19 | // A simple class to encapsulate errors that can be returned by the API. 20 | class GNError 21 | { 22 | const UNABLE_TO_PARSE_RESPONSE = 1; // The response couldn't be parsed. Maybe an error, or maybe the API changed. 23 | 24 | const API_RESPONSE_ERROR = 1000; // There was a GN error code returned in the response. 25 | const API_NO_MATCH = 1001; // The API returned a NO_MATCH (i.e. there were no results). 26 | const API_NON_OK_RESPONSE = 1002; // There was some unanticipated non-"OK" response from the API. 27 | 28 | const HTTP_REQUEST_ERROR = 2000; // An uncaught exception was raised while doing a cURL request. 29 | const HTTP_REQUEST_TIMEOUT = 2001; // The external request timed out. 30 | const HTTP_RESPONSE_ERROR_CODE = 2002; // There was a HTTP400 error code returned. 31 | 32 | const INVALID_INPUT_SPECIFIED = 3000; // Some input the user gave wasn't valid. 33 | 34 | // The human readable error messages 35 | static $_MESSAGES = array 36 | ( 37 | // Generic Errors 38 | 1 => "Unable to parse response from Gracenote WebAPI." 39 | 40 | // Specific API Errors 41 | ,1000 => "The API returned an error code." 42 | ,1001 => "The API returned no results." 43 | ,1002 => "The API returned an unacceptable response." 44 | 45 | // HTTP Errors 46 | ,2000 => "There was an error while performing an external request." 47 | ,2001 => "Request to a Gracenote WebAPI timed out." 48 | ,2002 => "WebAPI response had a HTTP error code." 49 | 50 | // Input Errors 51 | ,3000 => "Invalid input." 52 | ); 53 | 54 | public static function getMessage($code) { return self::$_MESSAGES[$code]; } 55 | } 56 | -------------------------------------------------------------------------------- /gracenote-php/HTTP.class.php: -------------------------------------------------------------------------------- 1 | _url = $url; 26 | $this->_timeout = $timeout; 27 | 28 | // Prepare the cURL handle. 29 | $this->_ch = curl_init(); 30 | 31 | // Set connection options. 32 | curl_setopt($this->_ch, CURLOPT_URL, $this->_url); // API URL 33 | curl_setopt($this->_ch, CURLOPT_USERAGENT, "php-gracenote"); // Set our user agent 34 | curl_setopt($this->_ch, CURLOPT_FAILONERROR, true); // Fail on error response. 35 | @curl_setopt($this->_ch, CURLOPT_FOLLOWLOCATION, true); // Follow any redirects 36 | curl_setopt($this->_ch, CURLOPT_RETURNTRANSFER, true); // Put the response into a variable instead of printing. 37 | curl_setopt($this->_ch, /*CURLOPT_CONNECTTIMEOUT_MS */ $this->_timeout, 2500); // Don't want to hang around forever. 38 | } 39 | 40 | // Dtor 41 | public function __destruct() 42 | { 43 | if ($this->_ch != null) { curl_close($this->_ch); } 44 | } 45 | 46 | //////////////////////////////////////////////////////////////////////////////////////////////// 47 | 48 | // Prepare the cURL handle 49 | private function prepare() 50 | { 51 | // Set header data 52 | if ($this->_headers != null) 53 | { 54 | $hdrs = array(); 55 | foreach ($this->_headers as $header => $value) 56 | { 57 | // If specified properly (as string) use it. If name=>value, convert to name:value. 58 | $hdrs[] = ((strtolower(substr($value, 0, 1)) === "x") 59 | && (strpos($value, ":") !== false)) ? $value : $header.":".$value; 60 | } 61 | curl_setopt($this->_ch, CURLOPT_HTTPHEADER, $hdrs); 62 | } 63 | 64 | // Add POST data if it's a POST request 65 | if ($this->_type == HTTP::POST) 66 | { 67 | curl_setopt($this->_ch, CURLOPT_POST, true); 68 | curl_setopt($this->_ch, CURLOPT_POSTFIELDS, $this->_postData); 69 | } 70 | } 71 | 72 | //////////////////////////////////////////////////////////////////////////////////////////////// 73 | 74 | public function execute() 75 | { 76 | // Prepare the request 77 | $this->prepare(); 78 | 79 | // Now try to make the call. 80 | $response = null; 81 | try 82 | { 83 | if (GN_DEBUG) { echo("http: external request ".(($this->_type == HTTP::GET) ? "GET" : "POST")." url=" . $this->_url. ", timeout=" . $this->_timeout . "\n"); } 84 | 85 | // Execute the request 86 | $response = curl_exec($this->_ch); 87 | } 88 | catch (Exception $e) 89 | { 90 | throw new GNException(GNError::HTTP_REQUEST_ERROR); 91 | } 92 | 93 | // Validate the response, or throw the proper exceptionS. 94 | $this->validateResponse($response); 95 | 96 | return $response; 97 | } 98 | 99 | //////////////////////////////////////////////////////////////////////////////////////////////// 100 | 101 | // This validates a cURL response and throws an exception if it's invalid in any way. 102 | public function validateResponse($response, $errno = null) 103 | { 104 | $curl_error = ($errno === null) ? curl_errno($this->_ch) : $errno; 105 | if ($curl_error !== CURLE_OK) 106 | { 107 | switch ($curl_error) 108 | { 109 | case CURLE_HTTP_NOT_FOUND: throw new GNException(GNError::HTTP_RESPONSE_ERROR_CODE, $this->getResponseCode()); 110 | case CURLE_OPERATION_TIMEOUTED: throw new GNException(GNError::HTTP_REQUEST_TIMEOUT); 111 | } 112 | 113 | throw new GNException(GNError::HTTP_RESPONSE_ERROR, $curl_error); 114 | } 115 | } 116 | 117 | //////////////////////////////////////////////////////////////////////////////////////////////// 118 | 119 | public function getHandle() { return $this->_ch; } 120 | public function getResponseCode() { return curl_getinfo($this->_ch, CURLINFO_HTTP_CODE); } 121 | 122 | //////////////////////////////////////////////////////////////////////////////////////////////// 123 | 124 | public function setPOST() { $this->_type = HTTP::POST; } 125 | public function setGET() { $this->_type = HTTP::GET; } 126 | public function setPOSTData($data) { $this->_postData = $data; } 127 | public function setHeaders($headers) { $this->_headers = $headers; } 128 | public function addHeader($header) { $this->_headers[] = $header; } 129 | public function setCurlOpt($o, $v) { curl_setopt($this->_ch, $o, $v); } 130 | 131 | //////////////////////////////////////////////////////////////////////////////////////////////// 132 | 133 | // Wrappers 134 | public function get() 135 | { 136 | $this->setGET(); 137 | return $this->execute(); 138 | } 139 | 140 | public function post($data = null) 141 | { 142 | if ($data != null) { $this->_postData = $data; } 143 | $this->setPOST(); 144 | return $this->execute(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /gracenote-php/register.php: -------------------------------------------------------------------------------- 1 | register(); 12 | 13 | if(!empty($userID)){ 14 | echo "Your userID: ".$userID . '
'; 15 | echo "Save this value in the config file and remove this file."; 16 | } 17 | ?> -------------------------------------------------------------------------------- /icecast_api.php: -------------------------------------------------------------------------------- 1 | config = $config; 25 | if($this->config['use_memcached']) $this->MemcachedInit(); 26 | if($this->config['use_db']) $this->GetDB(); 27 | $this->icecast_xml = $this->Request('getDataFromIcecast')->result; 28 | if(empty($this->icecast_xml)) die('Failed to connect to Icecast Server.'); 29 | 30 | } 31 | //Preparing response according to its type. 32 | // @param (array) $type 33 | // @returns formatted xml/json or just "as is". 34 | public function Response($type = null){ 35 | switch($type){ 36 | case 'xml': 37 | return $this->array_to_xml($this->result); 38 | break; 39 | case 'json': 40 | return $this->array_to_json($this->result); 41 | break; 42 | default: 43 | return $this->result; 44 | break; 45 | } 46 | } 47 | 48 | 49 | //main factory 50 | public function Request($method, array $args = array(), $memcachedOverride = false){ 51 | 52 | if(!empty($args['mount']) && !empty($this->config['icecast_mount_fallback_map'][$args['mount']])){ 53 | $args['mount'] = (!in_array($args['mount'],$this->listMounts(true))) ? $this->config['icecast_mount_fallback_map'][$args['mount']] : $args['mount']; 54 | } 55 | 56 | $this->result = (($this->config['use_memcached'] === true) && ($memcachedOverride === false)) ? $this->WithMemcached($method , $args) : $this->{$method . 'Action'}($args); 57 | return $this; 58 | } 59 | 60 | public function listMounts($only_active = false){ 61 | 62 | $xml = simplexml_load_string($this->icecast_xml); 63 | if($only_active){ 64 | $xml = $xml->xpath("/icestats/source[source_ip != '']/@mount"); 65 | }else{ 66 | $xml = $xml->xpath("/icestats/source/@mount"); 67 | } 68 | // to array 69 | $json = json_encode($xml); 70 | $array = json_decode($json,TRUE); 71 | 72 | $list = array(); 73 | foreach($array as $key => $item){ 74 | $list[] = str_replace('/','',$item['@attributes']['mount']); 75 | } 76 | return $list; 77 | } 78 | 79 | 80 | //Memcached wrapper. 81 | private function WithMemcached($method, array $args){ 82 | 83 | $key = $_SERVER['SERVER_NAME'].'_'.$method; 84 | 85 | foreach($args as $k => $val){ 86 | $key .= '_'.$val; 87 | } 88 | $data = $this->_memcache->get($key); 89 | 90 | if(empty($data)){ 91 | $data = $this->{$method . 'Action'}($args); 92 | $this->_memcache->set($key,$data,$this->config['memcached']['compressed'],$this->config['memcached']['lifetime']); 93 | } 94 | 95 | return $data; 96 | } 97 | 98 | 99 | 100 | 101 | /* 102 | Getting number of listeners for given mountpoint 103 | 104 | @param array $args 105 | @return array 106 | */ 107 | 108 | private function GetListenersAction(array $args){ 109 | extract($args); // $mount 110 | 111 | $xml = simplexml_load_string($this->icecast_xml); 112 | $xml = $xml->xpath("/icestats/source[@mount='/".$mount."']/listeners"); 113 | 114 | //to array 115 | $json = json_encode($xml); 116 | $array = json_decode($json,TRUE); 117 | 118 | return array('listeners' => $array[0][0]); 119 | } 120 | 121 | /* 122 | Getting total number of listeners 123 | 124 | @param array $args - empty array 125 | @return array 126 | */ 127 | private function GetTotalListenersAction(array $args){ 128 | 129 | $xml = simplexml_load_string($this->icecast_xml); 130 | $xml = $xml->xpath("/icestats/listeners"); 131 | 132 | $json = json_encode($xml); 133 | $array = json_decode($json,TRUE); 134 | 135 | return array('totalListeners'=>$array[0][0]); 136 | } 137 | 138 | /* 139 | Getting the current track and its timestamp for given $mount 140 | 141 | @param array $args[$mount] 142 | @return array 143 | */ 144 | private function GetTrackAction(array $args){ 145 | extract($args); // $mount 146 | 147 | $data = $this->GetHistoryAction(array('mount'=> $mount,'amount'=>1)); //using already defined method. 148 | return $data; 149 | } 150 | 151 | /* 152 | Parsing the logfile, returning an array of with specified $amount of last tracks for given $mount 153 | 154 | @param array $args[string $mount, int $amount] 155 | @return array 156 | */ 157 | private function GetHistoryAction(array $args){ 158 | extract($args); // $mount, $amount 159 | $amount = ($amount > $this->config['max_amount_of_history']) ? $this->config['max_amount_of_history'] : $amount; // checking if number of requested songs is lower than max 160 | 161 | $grab_lines = intval($amount * pow(count($this->listMounts()),1.2)); // amount of lines to work with 162 | 163 | $last_lines = $this->GetLastLinesFromFile($this->config['playlist_logfile'], $grab_lines); //gettins required number of lines from the logfile 164 | $last_lines = explode("\n",$last_lines); 165 | array_pop($last_lines); // deleting last empty line 166 | $last_lines = array_reverse($last_lines); // desc. order 167 | 168 | $line_parsed = array(); 169 | $result_array = array(); 170 | $i = 0; 171 | 172 | foreach($last_lines as $key => $line){ 173 | $line_parsed = explode("|",$line); 174 | /* 175 | $line_parsed[0] - timestamp of the song, i.e. 14/Apr/2013:13:52:58 +0400 176 | $line_parsed[1] - mountpoint of the song, i.e. /trance 177 | $line_parsed[2] - icecast's ID of the song(or something like that), i.e. 36 178 | $line_parsed[3] - full title of the song, i.e. Pakito - You Wanna Rock 179 | */ 180 | if($line_parsed[1] == "/".$mount){ 181 | 182 | if(empty($line_parsed[3])) continue; //empty song title, skipping 183 | 184 | if($i < $amount){ 185 | 186 | if($this->config['fix_non_utf8_encoding'] == true){ 187 | $line_parsed[3] = mb_convert_encoding($line_parsed[3],'UTF-8',$this->config['mp3_title_charset']); 188 | } 189 | 190 | $song_parts = explode("-",htmlspecialchars($line_parsed[3])); // exploding to artist and title 191 | 192 | $result_array[$i]['track'] = htmlspecialchars($line_parsed[3]); 193 | 194 | $result_array[$i]['title'] = (!empty($song_parts[1])) ? trim($song_parts[1]) : 'Unknown Title'; //only title, i.e. You Wanna Rock 195 | $result_array[$i]['artist'] = (!empty($song_parts[0])) ? trim($song_parts[0]) : 'Unknown Artist'; //only artist, i.e. Pakito 196 | 197 | $result_array[$i]['timestamp'] = strtotime($line_parsed[0]); //unixtime 198 | $result_array[$i]['album_art_url'] = 'http://'.$_SERVER['SERVER_NAME'].'/cover/'.urlencode($result_array[$i]['artist']).'/'.urlencode($result_array[$i]['title']); 199 | $result_array[$i]['artist_image_url'] = 'http://'.$_SERVER['SERVER_NAME'].'/cover/'.urlencode($result_array[$i]['artist']); 200 | 201 | $i++; 202 | }else break; 203 | } 204 | } 205 | return $result_array; 206 | } 207 | 208 | 209 | private function GetAlbumArtAction(array $args){ 210 | require_once('gracenote-php/Gracenote.class.php'); 211 | 212 | if(!is_writable($this->config['album_art_folder'])){ 213 | die('album art folder is not writable.'); 214 | } 215 | 216 | $default_img = $this->config['default_storage_folder'] . '404.jpg'; 217 | $filename = $this->config['album_art_folder'] . md5($args['artist'] . $args['song']) . '.jpg'; 218 | 219 | if(file_exists($filename)){ //found in cache, returning. 220 | 221 | if(filesize($filename) == 0){ 222 | return $default_img; 223 | } 224 | 225 | return $filename; 226 | } 227 | 228 | 229 | if(!empty($this->config['gracenote']['userID'])){ 230 | $gracenote_api = new Gracenote\WebAPI\GracenoteWebAPI($this->config['gracenote']['clientID'], $this->config['gracenote']['clientTag'], $this->config['gracenote']['userID']); 231 | }else{ 232 | die('Get your Gracenote userID via register.php to continue.'); 233 | } 234 | $result = array(); 235 | 236 | //querying the GraceNote API 237 | try 238 | { 239 | $result = $gracenote_api->searchTrack($args['artist'], '',$args['song'], Gracenote\WebAPI\GracenoteWebAPI::BEST_MATCH_ONLY); 240 | } 241 | catch( Exception $e ) 242 | { 243 | return $default_img; // something went wrong, returning dummy picture 244 | } 245 | 246 | if(empty($result[0]['album_art_url'])){ 247 | 248 | touch($filename); 249 | return $default_img; // something went wrong, returning dummy picture 250 | } 251 | 252 | $album_art = file_get_contents($result[0]['album_art_url']); 253 | 254 | if(touch($filename) && file_put_contents($filename , $album_art)){ //attempt to create a cache image 255 | return $filename; // everything is ok 256 | }else{ 257 | return $default_img; // something went wrong, returning dummy picture; 258 | } 259 | 260 | return $default_img; 261 | } 262 | 263 | 264 | 265 | private function GetArtistArtAction(array $args){ 266 | require_once('gracenote-php/Gracenote.class.php'); 267 | 268 | if(!is_writable($this->config['artist_art_folder'])){ 269 | die('artist art folder is not writable.'); 270 | } 271 | 272 | $default_img = $this->config['default_storage_folder'] . '404.jpg'; 273 | $filename = $this->config['artist_art_folder'] . md5($args['artist']) . '.jpg'; 274 | 275 | if(file_exists($filename)){ //found in cache, returning. 276 | 277 | if(filesize($filename) == 0){ 278 | return $default_img; 279 | } 280 | 281 | return $filename; 282 | } 283 | 284 | 285 | 286 | if(!empty($this->config['gracenote']['userID'])){ 287 | $gracenote_api = new Gracenote\WebAPI\GracenoteWebAPI($this->config['gracenote']['clientID'], $this->config['gracenote']['clientTag'], $this->config['gracenote']['userID']); 288 | }else{ 289 | die('Get your Gracenote userID via register.php to continue.'); 290 | } 291 | $result = array(); 292 | 293 | //querying the GraceNote API 294 | try 295 | { 296 | $result = $gracenote_api->searchArtist($args['artist'], Gracenote\WebAPI\GracenoteWebAPI::BEST_MATCH_ONLY); 297 | } 298 | catch( Exception $e ) 299 | { 300 | return $default_img; // something went wrong, returning dummy picture 301 | } 302 | 303 | 304 | if(empty($result[0]['artist_image_url'])){ //empty response, probably wrong artist name. Caching. 305 | 306 | touch($filename); 307 | return $default_img; // something went wrong, returning dummy picture 308 | } 309 | 310 | $artist_art = file_get_contents($result[0]['artist_image_url']); 311 | 312 | if(touch($filename) && file_put_contents($filename , $artist_art)){ //attempt to create a cache image 313 | return $filename; // everything is ok 314 | }else{ 315 | return $default_img; // something went wrong, returning dummy picture; 316 | } 317 | 318 | return $default_img; 319 | } 320 | 321 | 322 | 323 | 324 | /* 325 | private function YourCustomMethodAction(array $args){ 326 | 327 | return array('Hello' => 'Im a template for your custom methods.'); 328 | } 329 | */ 330 | 331 | 332 | private function MemcachedInit(){ 333 | $this->_memcache = new Memcache(); 334 | $this->_memcache->connect($this->config['memcached']['host'], $this->config['memcached']['port']); 335 | } 336 | 337 | private function GetDB(){ 338 | //setup your db interface here... 339 | //$this->_db = your DB object. 340 | } 341 | 342 | 343 | 344 | private function getDataFromIcecastAction(){ 345 | 346 | $process = curl_init($this->config['icecast_server_hostname'].':'.$this->config['icecast_server_port'].'/admin/stats'); 347 | 348 | curl_setopt($process, CURLOPT_USERPWD, $this->config['icecast_admin_username'] . ":" . $this->config['icecast_admin_password']); 349 | curl_setopt($process, CURLOPT_TIMEOUT, 5); 350 | curl_setopt($process, CURLOPT_RETURNTRANSFER, TRUE); 351 | 352 | return curl_exec($process); 353 | } 354 | 355 | 356 | private function array_to_xml(array $array, $xml=null){ 357 | 358 | if ($xml == null) 359 | { 360 | $xml = simplexml_load_string("<". $this->config['xmlrootnode'] ."/>"); 361 | } 362 | 363 | foreach($array as $key => $value) 364 | { 365 | if (is_numeric($key)) 366 | { 367 | $key = 'item'; 368 | } 369 | 370 | $key = preg_replace('/[^a-z]/i', '', $key); 371 | if (is_array($value)) 372 | { 373 | $node = $xml->addChild($key); 374 | $this->array_to_xml($value, $node); 375 | } 376 | else 377 | { 378 | $value = mb_convert_encoding($value, 'UTF-8'); 379 | $xml->addChild($key,$value); 380 | } 381 | } 382 | return $xml->asXML(); 383 | } 384 | 385 | private function array_to_json(array $array){ 386 | return json_encode($array); 387 | } 388 | 389 | private function GetLastLinesFromFile($filename, $lines) 390 | { 391 | $offset = -1; 392 | $c = ''; 393 | $read = ''; 394 | $i = 0; 395 | $fp = @fopen($filename, "r"); 396 | while( $lines && fseek($fp, $offset, SEEK_END) >= 0 ) { 397 | $c = fgetc($fp); 398 | if($c == "\n" || $c == "\r"){ 399 | $lines--; 400 | } 401 | $read .= $c; 402 | $offset--; 403 | } 404 | fclose ($fp); 405 | return strrev(rtrim($read,"\n\r")); 406 | } 407 | 408 | 409 | 410 | } 411 | 412 | 413 | 414 | ?> 415 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | listMounts(true); 24 | if(empty($active_mounts)){ 25 | // build response 26 | $response = array( 27 | 'type' => '503', 28 | 'message' => 'Unavailable' 29 | ); 30 | 31 | // output response and exit. 32 | $app->halt(503, json_encode($response)); 33 | } 34 | 35 | 36 | //Number of listeners of specified mountpoint. 37 | //(string) :mount => one of the existing mounts 38 | //(string) :responseType => response type(json|xml) 39 | $app->get('/:mount/listeners/:responseType(/)', function ($mount,$responseType) use ($icecastApi, $app) { 40 | 41 | $app->response()->header("Content-Type", "application/".$responseType); //setting appropriate headers 42 | echo $icecastApi->Request('GetListeners',array('mount' => $mount))->Response($responseType); //returning response to the client 43 | 44 | })->conditions(array("mount" => "(". implode('|',$icecastApi->listMounts()) .")", "responseType" => "(json|xml)")); // Only existing mounts are allowed 45 | 46 | 47 | //Current track of specified mountpoint. 48 | //(string) :mount => one of the existing mounts 49 | //(string) :responseType => response type(json|xml) 50 | $app->get('/:mount/track/:responseType(/)', function ($mount,$responseType) use ($icecastApi, $app) { 51 | 52 | $app->response()->header("Content-Type", "application/".$responseType); //setting appropriate headers 53 | echo $icecastApi->Request('GetTrack',array('mount' => $mount))->Response($responseType); //returning response to the client 54 | 55 | })->conditions(array("mount" => "(". implode('|',$icecastApi->listMounts()) .")", "responseType" => "(json|xml)")); 56 | 57 | 58 | //Last tracks of specified mountpoint. 59 | //(string) :mount => one of the existing mounts 60 | //(int) :amount => amount of tracks to retrieve 61 | //(string) :responseType => response type(json|xml) 62 | $app->get('/:mount/history/:amount/:responseType(/)', function ($mount,$amount,$responseType) use ($icecastApi, $app) { 63 | 64 | $app->response()->header("Content-Type", "application/".$responseType); //setting appropriate headers 65 | echo $icecastApi->Request('GetHistory',array('mount' => $mount , 'amount' => $amount))->Response($responseType); //returning response to the client 66 | 67 | })->conditions(array("mount" => "(". implode('|',$icecastApi->listMounts()) .")", "amount" => "\d+" , "responseType" => "(json|xml)")); 68 | 69 | 70 | //Total listeners 71 | //(string) :responseType => response type(json|xml) 72 | $app->get('/totalListeners/:responseType(/)', function ($responseType) use ($icecastApi, $app) { 73 | 74 | $app->response()->header("Content-Type", "application/".$responseType); //setting appropriate headers 75 | echo $icecastApi->Request('GetTotalListeners',array())->Response($responseType); //returning response to the client 76 | 77 | })->conditions(array("responseType" => "(json|xml)")); 78 | 79 | 80 | 81 | 82 | $app->get('/cover/:artist/:song(/)', function ($artist, $song) use ($icecastApi, $app) { 83 | 84 | 85 | $img = $icecastApi->Request('GetAlbumArt',array('artist' => $artist, 'song' => $song), true)->Response(); 86 | $app->response()->header("Content-Type", "image/jpeg"); //setting appropriate headers 87 | $app->response()->header("Content-Length", filesize($img)); 88 | 89 | readfile($img); 90 | 91 | }); 92 | 93 | $app->get('/cover/:artist(/)', function ($artist) use ($icecastApi, $app) { 94 | 95 | 96 | $img = $icecastApi->Request('GetArtistArt',array('artist' => $artist), true)->Response(); 97 | $app->response()->header("Content-Type", "image/jpeg"); //setting appropriate headers 98 | $app->response()->header("Content-Length", filesize($img)); 99 | 100 | readfile($img); 101 | 102 | }); 103 | 104 | 105 | 106 | 107 | 108 | 109 | //Custom method template 110 | /* 111 | $app->get('/customMethod/:responseType(/)', function ($responseType) use ($icecastApi, $app) { 112 | 113 | $app->response()->header("Content-Type", "application/".$responseType); 114 | echo $icecastApi->Request('YourCustomMethod',array())->Response($responseType); 115 | 116 | })->conditions(array("responseType" => "(json|xml)")); 117 | */ 118 | 119 | 120 | $app->notFound(function () use ($app) { 121 | 122 | // build response 123 | $response = array( 124 | 'type' => '400', 125 | 'message' => 'Bad request' 126 | ); 127 | 128 | // output response and exit 129 | $app->halt(400, json_encode($response)); 130 | }); 131 | 132 | $app->run(); 133 | 134 | ?> -------------------------------------------------------------------------------- /storage/albums/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okwinza/icecast2-php-api/eb6e17cb58091d444cad4a51603c0dd5a6ea3ec8/storage/albums/404.jpg -------------------------------------------------------------------------------- /storage/artists/200fe6d767fdb847177be9a32f517e8e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okwinza/icecast2-php-api/eb6e17cb58091d444cad4a51603c0dd5a6ea3ec8/storage/artists/200fe6d767fdb847177be9a32f517e8e.jpg -------------------------------------------------------------------------------- /storage/artists/8780f1b8319cd5a65180ce25a0fd4f73.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okwinza/icecast2-php-api/eb6e17cb58091d444cad4a51603c0dd5a6ea3ec8/storage/artists/8780f1b8319cd5a65180ce25a0fd4f73.jpg -------------------------------------------------------------------------------- /storage/default/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okwinza/icecast2-php-api/eb6e17cb58091d444cad4a51603c0dd5a6ea3ec8/storage/default/404.jpg --------------------------------------------------------------------------------