├── .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 |
2 | include("Gracenote.class.php");
3 | include('../config.php');
4 |
5 | //You can get these after completing registration at https://developer.gracenote.com
6 | if(!empty($config['gracenote']['userID'])){
7 | die('You already have an userID specified. No need for another.');
8 | }
9 |
10 | $api = new Gracenote\WebAPI\GracenoteWebAPI($config['gracenote']['clientID'], $config['gracenote']['clientTag'], $config['gracenote']['userID']);
11 | $userID = $api->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
--------------------------------------------------------------------------------