Soundcloud.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. <?php
  2. require_once 'Soundcloud/Exception.php';
  3. require_once 'Soundcloud/Version.php';
  4. /**
  5. * SoundCloud API wrapper with support for authentication using OAuth 2.
  6. *
  7. * @category Services
  8. * @package Services_Soundcloud
  9. * @author Anton Lindqvist <anton@qvister.se>
  10. * @copyright 2010 Anton Lindqvist <anton@qvister.se>
  11. * @license http://www.opensource.org/licenses/mit-license.php MIT
  12. * @link http://github.com/mptre/php-soundcloud
  13. */
  14. class Services_Soundcloud {
  15. /**
  16. * Custom cURL option.
  17. *
  18. * @access public
  19. *
  20. * @var integer
  21. */
  22. const CURLOPT_OAUTH_TOKEN = 173;
  23. /**
  24. * Access token returned by the service provider after a successful authentication.
  25. *
  26. * @access private
  27. *
  28. * @var string
  29. */
  30. private $_accessToken;
  31. /**
  32. * Version of the API to use.
  33. *
  34. * @access private
  35. *
  36. * @var integer
  37. */
  38. private static $_apiVersion = 1;
  39. /**
  40. * Supported audio MIME types.
  41. *
  42. * @access private
  43. *
  44. * @var array
  45. */
  46. private static $_audioMimeTypes = array(
  47. 'aac' => 'video/mp4',
  48. 'aiff' => 'audio/x-aiff',
  49. 'flac' => 'audio/flac',
  50. 'mp3' => 'audio/mpeg',
  51. 'ogg' => 'audio/ogg',
  52. 'wav' => 'audio/x-wav'
  53. );
  54. /**
  55. * OAuth client id.
  56. *
  57. * @access private
  58. *
  59. * @var string
  60. */
  61. private $_clientId;
  62. /**
  63. * OAuth client secret.
  64. *
  65. * @access private
  66. *
  67. * @var string
  68. */
  69. private $_clientSecret;
  70. /**
  71. * Development mode.
  72. *
  73. * @access private
  74. *
  75. * @var boolean
  76. */
  77. private $_development;
  78. /**
  79. * Available API domains.
  80. *
  81. * @access private
  82. *
  83. * @var array
  84. */
  85. private static $_domains = array(
  86. 'development' => 'sandbox-soundcloud.com',
  87. 'production' => 'soundcloud.com'
  88. );
  89. /**
  90. * HTTP response body from the last request.
  91. *
  92. * @access private
  93. *
  94. * @var string
  95. */
  96. private $_lastHttpResponseBody;
  97. /**
  98. * HTTP response code from the last request.
  99. *
  100. * @access private
  101. *
  102. * @var integer
  103. */
  104. private $_lastHttpResponseCode;
  105. /**
  106. * HTTP response headers from last request.
  107. *
  108. * @access private
  109. *
  110. * @var array
  111. */
  112. private $_lastHttpResponseHeaders;
  113. /**
  114. * OAuth paths.
  115. *
  116. * @access private
  117. *
  118. * @var array
  119. */
  120. private static $_paths = array(
  121. 'authorize' => 'connect',
  122. 'access_token' => 'oauth2/token',
  123. );
  124. /**
  125. * OAuth redirect uri.
  126. *
  127. * @access private
  128. *
  129. * @var string
  130. */
  131. private $_redirectUri;
  132. /**
  133. * API response format MIME type.
  134. *
  135. * @access private
  136. *
  137. * @var string
  138. */
  139. private $_requestFormat;
  140. /**
  141. * Available response formats.
  142. *
  143. * @access private
  144. *
  145. * @var array
  146. */
  147. private static $_responseFormats = array(
  148. '*' => '*/*',
  149. 'json' => 'application/json',
  150. 'xml' => 'application/xml'
  151. );
  152. /**
  153. * HTTP user agent.
  154. *
  155. * @access private
  156. *
  157. * @var string
  158. */
  159. private static $_userAgent = 'PHP-SoundCloud';
  160. /**
  161. * Class version.
  162. *
  163. * @var string
  164. */
  165. public $version;
  166. /**
  167. * Constructor.
  168. *
  169. * @param string $clientId OAuth client id
  170. * @param string $clientSecret OAuth client secret
  171. * @param string $redirectUri OAuth redirect uri
  172. * @param boolean $development Sandbox mode
  173. *
  174. * @throws Services_Soundcloud_Missing_Client_Id_Exception when missing client id
  175. * @return void
  176. */
  177. function __construct($clientId, $clientSecret, $redirectUri = null, $development = false) {
  178. if (empty($clientId)) {
  179. throw new Services_Soundcloud_Missing_Client_Id_Exception();
  180. }
  181. $this->_clientId = $clientId;
  182. $this->_clientSecret = $clientSecret;
  183. $this->_redirectUri = $redirectUri;
  184. $this->_development = $development;
  185. $this->_responseFormat = self::$_responseFormats['json'];
  186. $this->version = Services_Soundcloud_Version::get();
  187. }
  188. /**
  189. * Get authorization URL.
  190. *
  191. * @param array $params Optional query string parameters
  192. *
  193. * @return string
  194. * @see Soundcloud::_buildUrl()
  195. */
  196. function getAuthorizeUrl($params = array()) {
  197. $defaultParams = array(
  198. 'client_id' => $this->_clientId,
  199. 'redirect_uri' => $this->_redirectUri,
  200. 'response_type' => 'code'
  201. );
  202. $params = array_merge($defaultParams, $params);
  203. return $this->_buildUrl(self::$_paths['authorize'], $params, false);
  204. }
  205. /**
  206. * Get access token URL.
  207. *
  208. * @param array $params Optional query string parameters
  209. *
  210. * @return string
  211. * @see Soundcloud::_buildUrl()
  212. */
  213. function getAccessTokenUrl($params = array()) {
  214. return $this->_buildUrl(self::$_paths['access_token'], $params, false);
  215. }
  216. /**
  217. * Retrieve access token.
  218. *
  219. * @param string $code OAuth code returned from the service provider
  220. * @param array $postData Optional post data
  221. * @param array $curlOptions Optional cURL options
  222. *
  223. * @return mixed
  224. * @see Soundcloud::_getAccessToken()
  225. */
  226. function accessToken($code, $postData = array(), $curlOptions = array()) {
  227. $defaultPostData = array(
  228. 'code' => $code,
  229. 'client_id' => $this->_clientId,
  230. 'client_secret' => $this->_clientSecret,
  231. 'redirect_uri' => $this->_redirectUri,
  232. 'grant_type' => 'authorization_code'
  233. );
  234. $postData = array_merge($defaultPostData, $postData);
  235. return $this->_getAccessToken($postData, $curlOptions);
  236. }
  237. /**
  238. * Retrieve access token.
  239. *
  240. * @param string $username
  241. * @param string $password
  242. * @param array $postData Optional post data
  243. * @param array $curlOptions Optional cURL options
  244. *
  245. * @return mixed
  246. * @see Soundcloud::_getAccessToken()
  247. */
  248. function accessTokenResourceOwner($username, $password, $postData = array(), $curlOptions = array()) {
  249. $defaultPostData = array(
  250. 'client_id' => $this->_clientId,
  251. 'client_secret' => $this->_clientSecret,
  252. 'grant_type' => 'password',
  253. 'username' => $username,
  254. 'password' => $password
  255. );
  256. $postData = array_merge($defaultPostData, $postData);
  257. return $this->_getAccessToken($postData, $curlOptions);
  258. }
  259. /**
  260. * Refresh access token.
  261. *
  262. * @param string $refreshToken
  263. * @param array $postData Optional post data
  264. * @param array $curlOptions Optional cURL options
  265. *
  266. * @return mixed
  267. * @see Soundcloud::_getAccessToken()
  268. */
  269. function accessTokenRefresh($refreshToken, $postData = array(), $curlOptions = array()) {
  270. $defaultPostData = array(
  271. 'refresh_token' => $refreshToken,
  272. 'client_id' => $this->_clientId,
  273. 'client_secret' => $this->_clientSecret,
  274. 'redirect_uri' => $this->_redirectUri,
  275. 'grant_type' => 'refresh_token'
  276. );
  277. $postData = array_merge($defaultPostData, $postData);
  278. return $this->_getAccessToken($postData, $curlOptions);
  279. }
  280. /**
  281. * Get access token.
  282. *
  283. * @return mixed
  284. */
  285. function getAccessToken() {
  286. return $this->_accessToken;
  287. }
  288. /**
  289. * Get API version.
  290. *
  291. * @return integer
  292. */
  293. function getApiVersion() {
  294. return self::$_apiVersion;
  295. }
  296. /**
  297. * Get the corresponding MIME type for a given file extension.
  298. *
  299. * @param string $extension
  300. *
  301. * @return string
  302. * @throws Services_Soundcloud_Unsupported_Audio_Format_Exception if the format is unsupported
  303. */
  304. function getAudioMimeType($extension) {
  305. if (array_key_exists($extension, self::$_audioMimeTypes)) {
  306. return self::$_audioMimeTypes[$extension];
  307. } else {
  308. throw new Services_Soundcloud_Unsupported_Audio_Format_Exception();
  309. }
  310. }
  311. /**
  312. * Get development mode.
  313. *
  314. * @return boolean
  315. */
  316. function getDevelopment() {
  317. return $this->_development;
  318. }
  319. /**
  320. * Get HTTP response header.
  321. *
  322. * @param string $header Name of the header
  323. *
  324. * @return mixed
  325. */
  326. function getHttpHeader($header) {
  327. if (is_array($this->_lastHttpResponseHeaders)
  328. && array_key_exists($header, $this->_lastHttpResponseHeaders)
  329. ) {
  330. return $this->_lastHttpResponseHeaders[$header];
  331. } else {
  332. return false;
  333. }
  334. }
  335. /**
  336. * Get redirect uri.
  337. *
  338. * @return mixed
  339. */
  340. function getRedirectUri() {
  341. return $this->_redirectUri;
  342. }
  343. /**
  344. * Get response format.
  345. *
  346. * @return string
  347. */
  348. function getResponseFormat() {
  349. return $this->_responseFormat;
  350. }
  351. /**
  352. * Set access token.
  353. *
  354. * @param string $accessToken
  355. *
  356. * @return object
  357. */
  358. function setAccessToken($accessToken) {
  359. $this->_accessToken = $accessToken;
  360. return $this;
  361. }
  362. /**
  363. * Set redirect uri.
  364. *
  365. * @param string $redirectUri
  366. *
  367. * @return object
  368. */
  369. function setRedirectUri($redirectUri) {
  370. $this->_redirectUri = $redirectUri;
  371. return $this;
  372. }
  373. /**
  374. * Set response format.
  375. *
  376. * @param string $format Could either be xml or json
  377. *
  378. * @throws Services_Soundcloud_Unsupported_Response_Format_Exception if the given response format isn't supported
  379. * @return object
  380. */
  381. function setResponseFormat($format) {
  382. if (array_key_exists($format, self::$_responseFormats)) {
  383. $this->_responseFormat = self::$_responseFormats[$format];
  384. } else {
  385. throw new Services_Soundcloud_Unsupported_Response_Format_Exception();
  386. }
  387. return $this;
  388. }
  389. /**
  390. * Set development mode.
  391. *
  392. * @param boolean $development
  393. *
  394. * @return object
  395. */
  396. function setDevelopment($development) {
  397. $this->_development = $development;
  398. return $this;
  399. }
  400. /**
  401. * Send a GET HTTP request.
  402. *
  403. * @param string $path URI to request
  404. * @param array $params Optional query string parameters
  405. * @param array $curlOptions Optional cURL options
  406. *
  407. * @return mixed
  408. * @see Soundcloud::_request()
  409. */
  410. function get($path, $params = array(), $curlOptions = array()) {
  411. $url = $this->_buildUrl($path, $params);
  412. return $this->_request($url, $curlOptions);
  413. }
  414. /**
  415. * Send a POST HTTP request.
  416. *
  417. * @param string $path URI to request
  418. * @param array $postData Optional post data
  419. * @param array $curlOptions Optional cURL options
  420. *
  421. * @return mixed
  422. * @see Soundcloud::_request()
  423. */
  424. function post($path, $postData = array(), $curlOptions = array()) {
  425. $url = $this->_buildUrl($path);
  426. $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData);
  427. $options += $curlOptions;
  428. return $this->_request($url, $options);
  429. }
  430. /**
  431. * Send a PUT HTTP request.
  432. *
  433. * @param string $path URI to request
  434. * @param array $postData Optional post data
  435. * @param array $curlOptions Optional cURL options
  436. *
  437. * @return mixed
  438. * @see Soundcloud::_request()
  439. */
  440. function put($path, $postData, $curlOptions = array()) {
  441. $url = $this->_buildUrl($path);
  442. $options = array(
  443. CURLOPT_CUSTOMREQUEST => 'PUT',
  444. CURLOPT_POSTFIELDS => $postData
  445. );
  446. $options += $curlOptions;
  447. return $this->_request($url, $options);
  448. }
  449. /**
  450. * Send a DELETE HTTP request.
  451. *
  452. * @param string $path URI to request
  453. * @param array $params Optional query string parameters
  454. * @param array $curlOptions Optional cURL options
  455. *
  456. * @return mixed
  457. * @see Soundcloud::_request()
  458. */
  459. function delete($path, $params = array(), $curlOptions = array()) {
  460. $url = $this->_buildUrl($path, $params);
  461. $options = array(CURLOPT_CUSTOMREQUEST => 'DELETE');
  462. $options += $curlOptions;
  463. return $this->_request($url, $options);
  464. }
  465. /**
  466. * Download track.
  467. *
  468. * @param integer $trackId
  469. * @param array Optional query string parameters
  470. * @param array $curlOptions Optional cURL options
  471. *
  472. * @return mixed
  473. * @see Soundcloud::_request()
  474. */
  475. function download($trackId, $params = array(), $curlOptions = array()) {
  476. $lastResponseFormat = array_pop(
  477. preg_split('/\//', $this->getResponseFormat())
  478. );
  479. $defaultParams = array('oauth_token' => $this->getAccessToken());
  480. $defaultCurlOptions = array(
  481. CURLOPT_FOLLOWLOCATION => true,
  482. self::CURLOPT_OAUTH_TOKEN => false
  483. );
  484. $url = $this->_buildUrl(
  485. 'tracks/' . $trackId . '/download',
  486. array_merge($defaultParams, $params)
  487. );
  488. $options = $defaultCurlOptions + $curlOptions;
  489. $this->setResponseFormat('*');
  490. $response = $this->_request($url, $options);
  491. // rollback to the previously defined response format.
  492. $this->setResponseFormat($lastResponseFormat);
  493. return $response;
  494. }
  495. /**
  496. * Construct default HTTP headers including response format and authorization.
  497. *
  498. * @param boolean Include access token or not
  499. *
  500. * @return array $headers
  501. */
  502. protected function _buildDefaultHeaders($includeAccessToken = true) {
  503. $headers = array();
  504. if ($this->_responseFormat) {
  505. array_push($headers, 'Accept: ' . $this->_responseFormat);
  506. }
  507. if ($includeAccessToken && $this->_accessToken) {
  508. array_push($headers, 'Authorization: OAuth ' . $this->_accessToken);
  509. }
  510. return $headers;
  511. }
  512. /**
  513. * Construct a URL.
  514. *
  515. * @param string $path Relative or absolute URI
  516. * @param array $params Optional query string parameters
  517. * @param boolean $includeVersion Include API version
  518. *
  519. * @return string $url
  520. */
  521. protected function _buildUrl($path, $params = null, $includeVersion = true) {
  522. if (preg_match('/^https?\:\/\//', $path)) {
  523. $url = $path;
  524. } else {
  525. $url = 'https://';
  526. $url .= (!preg_match('/connect/', $path)) ? 'api.' : '';
  527. $url .= ($this->_development)
  528. ? self::$_domains['development']
  529. : self::$_domains['production'];
  530. $url .= '/';
  531. $url .= ($includeVersion) ? 'v' . self::$_apiVersion . '/' : '';
  532. $url .= $path;
  533. }
  534. $url .= (count($params)) ? '?' . http_build_query($params) : '';
  535. return $url;
  536. }
  537. /**
  538. * Retrieve access token.
  539. *
  540. * @param array $postData Post data
  541. * @param array $curlOptions Optional cURL options
  542. *
  543. * @return mixed
  544. */
  545. protected function _getAccessToken($postData, $curlOptions = array()) {
  546. $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData);
  547. $options += $curlOptions;
  548. $response = json_decode(
  549. $this->_request($this->getAccessTokenUrl(), $options),
  550. true
  551. );
  552. if (array_key_exists('access_token', $response)) {
  553. $this->_accessToken = $response['access_token'];
  554. return $response;
  555. } else {
  556. return false;
  557. }
  558. }
  559. /**
  560. * Get HTTP user agent.
  561. *
  562. * @access protected
  563. *
  564. * @return string
  565. */
  566. protected function _getUserAgent() {
  567. return self::$_userAgent . '/' . $this->version;
  568. }
  569. /**
  570. * Parse HTTP response headers.
  571. *
  572. * @param string $headers
  573. *
  574. * @return array
  575. */
  576. protected function _parseHttpHeaders($headers) {
  577. $headers = preg_split('/\n/', trim($headers));
  578. $parsedHeaders = array();
  579. foreach ($headers as $header) {
  580. if (!preg_match('/\:\s/', $header)) {
  581. continue;
  582. }
  583. list($key, $val) = preg_split('/\:\s/', $header, 2);
  584. $key = str_replace('-', '_', strtolower($key));
  585. $val = trim($val);
  586. $parsedHeaders[$key] = $val;
  587. }
  588. return $parsedHeaders;
  589. }
  590. /**
  591. * Validates HTTP response code.
  592. *
  593. * @access protected
  594. *
  595. * @return boolean
  596. */
  597. protected function _validResponseCode($code) {
  598. return (bool)preg_match('/^20[0-9]{1}$/', $code);
  599. }
  600. /**
  601. * Performs the actual HTTP request using curl. Can be overwritten by extending classes.
  602. *
  603. * @access protected
  604. *
  605. * @param string $url
  606. * @param array $curlOptions Optional cURL options
  607. *
  608. * @throws Services_Soundcloud_Invalid_Http_Response_Code_Exception if the response code isn't valid
  609. * @return mixed
  610. */
  611. protected function _request($url, $curlOptions = array()) {
  612. $ch = curl_init();
  613. $options = array(
  614. CURLOPT_URL => $url,
  615. CURLOPT_HEADER => true,
  616. CURLOPT_RETURNTRANSFER => true,
  617. CURLOPT_USERAGENT => $this->_getUserAgent()
  618. );
  619. $options += $curlOptions;
  620. if (array_key_exists(self::CURLOPT_OAUTH_TOKEN, $options)) {
  621. $includeAccessToken = $options[self::CURLOPT_OAUTH_TOKEN];
  622. unset($options[self::CURLOPT_OAUTH_TOKEN]);
  623. } else {
  624. $includeAccessToken = true;
  625. }
  626. if (array_key_exists(CURLOPT_HTTPHEADER, $options)) {
  627. $options[CURLOPT_HTTPHEADER] = array_merge(
  628. $this->_buildDefaultHeaders(),
  629. $curlOptions[CURLOPT_HTTPHEADER]
  630. );
  631. } else {
  632. $options[CURLOPT_HTTPHEADER] = $this->_buildDefaultHeaders($includeAccessToken);
  633. }
  634. curl_setopt_array($ch, $options);
  635. $data = curl_exec($ch);
  636. $info = curl_getinfo($ch);
  637. curl_close($ch);
  638. $this->_lastHttpResponseHeaders = $this->_parseHttpHeaders(
  639. substr($data, 0, $info['header_size'])
  640. );
  641. $this->_lastHttpResponseBody = substr($data, $info['header_size']);
  642. $this->_lastHttpResponseCode = $info['http_code'];
  643. if ($this->_validResponseCode($this->_lastHttpResponseCode)) {
  644. return $this->_lastHttpResponseBody;
  645. } else {
  646. throw new Services_Soundcloud_Invalid_Http_Response_Code_Exception(
  647. null,
  648. 0,
  649. $this->_lastHttpResponseBody,
  650. $this->_lastHttpResponseCode
  651. );
  652. }
  653. }
  654. }