Sonarr.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. <?php
  2. namespace Kryptonit3\Sonarr;
  3. use GuzzleHttp\Client;
  4. use Composer\Semver\Comparator;
  5. class Sonarr
  6. {
  7. protected $url;
  8. protected $apiKey;
  9. protected $httpAuthUsername;
  10. protected $httpAuthPassword;
  11. public function __construct($url, $apiKey, $type = 'sonarr', $httpAuthUsername = null, $httpAuthPassword = null)
  12. {
  13. $this->url = rtrim($url, '/\\'); // Example: http://127.0.0.1:8989 (no trailing forward-backward slashes)
  14. $this->apiKey = $apiKey;
  15. $this->type = strtolower($type);
  16. $this->httpAuthUsername = $httpAuthUsername;
  17. $this->httpAuthPassword = $httpAuthPassword;
  18. }
  19. /**
  20. * Gets upcoming episodes, if start/end are not supplied episodes airing today and tomorrow will be returned
  21. * When supplying start and/or end date you must supply date in format yyyy-mm-dd
  22. * Example: $sonarr->getCalendar('2015-01-25', '2016-01-15');
  23. * 'start' and 'end' not required. You may supply, one or both.
  24. *
  25. * @param string|null $start
  26. * @param string|null $end
  27. * @return array|object|string
  28. */
  29. public function getCalendar($start = null, $end = null, $sonarrUnmonitored = 'false')
  30. {
  31. $uriData = [];
  32. if ( $start ) {
  33. if ( $this->validateDate($start) ) {
  34. $uriData['start'] = $start;
  35. } else {
  36. return json_encode(array(
  37. 'error' => array(
  38. 'msg' => 'Start date string was not recognized as a valid DateTime. Format must be yyyy-mm-dd.',
  39. 'code' => 400,
  40. ),
  41. ));
  42. exit();
  43. }
  44. }
  45. if ( $end ) {
  46. if ( $this->validateDate($end) ) {
  47. $uriData['end'] = $end;
  48. } else {
  49. return json_encode(array(
  50. 'error' => array(
  51. 'msg' => 'End date string was not recognized as a valid DateTime. Format must be yyyy-mm-dd.',
  52. 'code' => 400,
  53. ),
  54. ));
  55. exit();
  56. }
  57. }
  58. if ( $sonarrUnmonitored == 'true' ) {
  59. $uriData['unmonitored'] = 'true';
  60. }
  61. if ( $this->type == 'lidarr' ) {
  62. $uriData['includeArtist'] = 'true';
  63. }
  64. $response = [
  65. 'uri' => 'calendar',
  66. 'type' => 'get',
  67. 'data' => $uriData
  68. ];
  69. return $this->processRequest($response);
  70. }
  71. /**
  72. * Queries the status of a previously started command, or all currently started commands.
  73. *
  74. * @param null $id Unique ID of the command
  75. * @return array|object|string
  76. */
  77. public function getCommand($id = null)
  78. {
  79. $uri = ($id) ? 'command/' . $id : 'command';
  80. $response = [
  81. 'uri' => $uri,
  82. 'type' => 'get',
  83. 'data' => []
  84. ];
  85. return $this->processRequest($response);
  86. }
  87. /**
  88. * Publish a new command for Sonarr to run.
  89. * These commands are executed asynchronously; use GET to retrieve the current status.
  90. *
  91. * Commands and their parameters can be found here:
  92. * https://github.com/Sonarr/Sonarr/wiki/Command#commands
  93. *
  94. * @param $name
  95. * @param array|null $params
  96. * @return string
  97. */
  98. public function postCommand($name, array $params = null)
  99. {
  100. $uri = 'command';
  101. $uriData = [
  102. 'name' => $name
  103. ];
  104. if (is_array($params)) {
  105. foreach($params as $key=>$value) {
  106. $uriData[$key] = $value;
  107. }
  108. }
  109. $response = [
  110. 'uri' => $uri,
  111. 'type' => 'post',
  112. 'data' => $uriData
  113. ];
  114. return $this->processRequest($response);
  115. }
  116. /**
  117. * Gets Diskspace
  118. *
  119. * @return array|object|string
  120. */
  121. public function getDiskspace()
  122. {
  123. $uri = 'diskspace';
  124. $response = [
  125. 'uri' => $uri,
  126. 'type' => 'get',
  127. 'data' => []
  128. ];
  129. return $this->processRequest($response);
  130. }
  131. /**
  132. * Returns all episodes for the given series
  133. *
  134. * @param $seriesId
  135. * @return array|object|string
  136. */
  137. public function getEpisodes($seriesId)
  138. {
  139. $uri = 'episode';
  140. $response = [
  141. 'uri' => $uri,
  142. 'type' => 'get',
  143. 'data' => [
  144. 'SeriesId' => $seriesId
  145. ]
  146. ];
  147. return $this->processRequest($response);
  148. }
  149. /**
  150. * Returns the episode with the matching id
  151. *
  152. * @param $id
  153. * @return string
  154. */
  155. public function getEpisode($id)
  156. {
  157. $uri = 'episode';
  158. $response = [
  159. 'uri' => $uri . '/' . $id,
  160. 'type' => 'get',
  161. 'data' => []
  162. ];
  163. return $this->processRequest($response);
  164. }
  165. /**
  166. * Update the given episodes, currently only monitored is changed, all other modifications are ignored.
  167. *
  168. * Required: All parameters; You should perform a getEpisode(id)
  169. * and submit the full body with the changes, as other values may be editable in the future.
  170. *
  171. * @param array $data
  172. * @return string
  173. */
  174. public function updateEpisode(array $data)
  175. {
  176. $uri = 'episode';
  177. $response = [
  178. 'uri' => $uri,
  179. 'type' => 'put',
  180. 'data' => $data
  181. ];
  182. return $this->processRequest($response);
  183. }
  184. /**
  185. * Returns all episode files for the given series
  186. *
  187. * @param $seriesId
  188. * @return array|object|string
  189. */
  190. public function getEpisodeFiles($seriesId)
  191. {
  192. $uri = 'episodefile';
  193. $response = [
  194. 'uri' => $uri,
  195. 'type' => 'get',
  196. 'data' => [
  197. 'SeriesId' => $seriesId
  198. ]
  199. ];
  200. return $this->processRequest($response);
  201. }
  202. /**
  203. * Returns the episode file with the matching id
  204. *
  205. * @param $id
  206. * @return string
  207. */
  208. public function getEpisodeFile($id)
  209. {
  210. $uri = 'episodefile';
  211. $response = [
  212. 'uri' => $uri . '/' . $id,
  213. 'type' => 'get',
  214. 'data' => []
  215. ];
  216. return $this->processRequest($response);
  217. }
  218. /**
  219. * Delete the given episode file
  220. *
  221. * @param $id
  222. * @return string
  223. */
  224. public function deleteEpisodeFile($id)
  225. {
  226. $uri = 'episodefile';
  227. $response = [
  228. 'uri' => $uri . '/' . $id,
  229. 'type' => 'delete',
  230. 'data' => []
  231. ];
  232. return $this->processRequest($response);
  233. }
  234. /**
  235. * Gets history (grabs/failures/completed).
  236. *
  237. * @param int $page Page Number
  238. * @param int $pageSize Results per Page
  239. * @param string $sortKey 'series.title' or 'date'
  240. * @param string $sortDir 'asc' or 'desc'
  241. * @return array|object|string
  242. */
  243. public function getHistory($page = 1, $pageSize = 10, $sortKey = 'series.title', $sortDir = 'asc')
  244. {
  245. $uri = 'history';
  246. $response = [
  247. 'uri' => $uri,
  248. 'type' => 'get',
  249. 'data' => [
  250. 'page' => $page,
  251. 'pageSize' => $pageSize,
  252. 'sortKey' => $sortKey,
  253. 'sortDir' => $sortDir
  254. ]
  255. ];
  256. return $this->processRequest($response);
  257. }
  258. /**
  259. * Gets missing episode (episodes without files).
  260. *
  261. * @param int $page Page Number
  262. * @param int $pageSize Results per Page
  263. * @param string $sortKey 'series.title' or 'airDateUtc'
  264. * @param string $sortDir 'asc' or 'desc'
  265. * @return array|object|string
  266. */
  267. public function getWantedMissing($page = 1, $pageSize = 10, $sortKey = 'series.title', $sortDir = 'asc')
  268. {
  269. $uri = 'wanted/missing';
  270. $response = [
  271. 'uri' => $uri,
  272. 'type' => 'get',
  273. 'data' => [
  274. 'page' => $page,
  275. 'pageSize' => $pageSize,
  276. 'sortKey' => $sortKey,
  277. 'sortDir' => $sortDir
  278. ]
  279. ];
  280. return $this->processRequest($response);
  281. }
  282. /**
  283. * Displays currently downloading info
  284. *
  285. * @return array|object|string
  286. */
  287. public function getQueue()
  288. {
  289. $uri = 'queue';
  290. $response = [
  291. 'uri' => $uri,
  292. 'type' => 'get',
  293. 'data' => []
  294. ];
  295. return $this->processRequest($response);
  296. }
  297. /**
  298. * Gets all quality profiles
  299. *
  300. * @return array|object|string
  301. */
  302. public function getProfiles()
  303. {
  304. $uri = 'profile';
  305. $response = [
  306. 'uri' => $uri,
  307. 'type' => 'get',
  308. 'data' => []
  309. ];
  310. return $this->processRequest($response);
  311. }
  312. /**
  313. * Get release by episode id
  314. *
  315. * @param $episodeId
  316. * @return string
  317. */
  318. public function getRelease($episodeId)
  319. {
  320. $uri = 'release';
  321. $uriData = [
  322. 'episodeId' => $episodeId
  323. ];
  324. $response = [
  325. 'uri' => $uri,
  326. 'type' => 'get',
  327. 'data' => $uriData
  328. ];
  329. return $this->processRequest($response);
  330. }
  331. /**
  332. * Adds a previously searched release to the download client,
  333. * if the release is still in Sonarr's search cache (30 minute cache).
  334. * If the release is not found in the cache Sonarr will return a 404.
  335. *
  336. * @param $guid
  337. * @return string
  338. */
  339. public function postRelease($guid)
  340. {
  341. $uri = 'release';
  342. $uriData = [
  343. 'guid' => $guid
  344. ];
  345. $response = [
  346. 'uri' => $uri,
  347. 'type' => 'post',
  348. 'data' => $uriData
  349. ];
  350. return $this->processRequest($response);
  351. }
  352. /**
  353. * Push a release to download client
  354. *
  355. * @param $title
  356. * @param $downloadUrl
  357. * @param $downloadProtocol (Usenet or Torrent)
  358. * @param $publishDate (ISO8601 Date)
  359. * @return string
  360. */
  361. public function postReleasePush($title, $downloadUrl, $downloadProtocol, $publishDate)
  362. {
  363. $uri = 'release';
  364. $uriData = [
  365. 'title' => $title,
  366. 'downloadUrl' => $downloadUrl,
  367. 'downloadProtocol' => $downloadProtocol,
  368. 'publishDate' => $publishDate
  369. ];
  370. $response = [
  371. 'uri' => $uri,
  372. 'type' => 'post',
  373. 'data' => $uriData
  374. ];
  375. return $this->processRequest($response);
  376. }
  377. /**
  378. * Gets root folder
  379. *
  380. * @return array|object|string
  381. */
  382. public function getRootFolder()
  383. {
  384. $uri = 'rootfolder';
  385. $response = [
  386. 'uri' => $uri,
  387. 'type' => 'get',
  388. 'data' => []
  389. ];
  390. return $this->processRequest($response);
  391. }
  392. /**
  393. * Returns all series in your collection
  394. *
  395. * @return array|object|string
  396. */
  397. public function getSeries()
  398. {
  399. $uri = 'series';
  400. $response = [
  401. 'uri' => $uri,
  402. 'type' => 'get',
  403. 'data' => []
  404. ];
  405. return $this->processRequest($response);
  406. }
  407. /**
  408. * Adds a new series to your collection
  409. *
  410. * NOTE: if you do not add the required params, then the series wont function.
  411. * Some of these without the others can indeed make a "series". But it wont function properly in Sonarr.
  412. *
  413. * Required: tvdbId (int) title (string) qualityProfileId (int) titleSlug (string) seasons (array)
  414. * See GET output for format
  415. *
  416. * path (string) - full path to the series on disk or rootFolderPath (string)
  417. * Full path will be created by combining the rootFolderPath with the series title
  418. *
  419. * Optional: tvRageId (int) seasonFolder (bool) monitored (bool)
  420. *
  421. * @param array $data
  422. * @param bool|true $onlyFutureEpisodes It can be used to control which episodes Sonarr monitors
  423. * after adding the series, setting to true (default) will only monitor future episodes.
  424. *
  425. * @return array|object|string
  426. */
  427. public function postSeries(array $data, $onlyFutureEpisodes = true)
  428. {
  429. $uri = 'series';
  430. $uriData = [];
  431. // Required
  432. $uriData['tvdbId'] = $data['tvdbId'];
  433. $uriData['title'] = $data['title'];
  434. $uriData['qualityProfileId'] = $data['qualityProfileId'];
  435. if ( array_key_exists('titleSlug', $data) ) { $uriData['titleSlug'] = $data['titleSlug']; }
  436. if ( array_key_exists('seasons', $data) ) { $uriData['seasons'] = $data['seasons']; }
  437. if ( array_key_exists('path', $data) ) { $uriData['path'] = $data['path']; }
  438. if ( array_key_exists('rootFolderPath', $data) ) { $uriData['rootFolderPath'] = $data['rootFolderPath']; }
  439. if ( array_key_exists('tvRageId', $data) ) { $uriData['tvRageId'] = $data['tvRageId']; }
  440. $uriData['seasonFolder'] = ( array_key_exists('seasonFolder', $data) ) ? $data['seasonFolder'] : true;
  441. if ( array_key_exists('monitored', $data) ) { $uriData['monitored'] = $data['monitored']; }
  442. if ( $onlyFutureEpisodes ) {
  443. $uriData['addOptions'] = [
  444. 'ignoreEpisodesWithFiles' => true,
  445. 'ignoreEpisodesWithoutFiles' => true
  446. ];
  447. }
  448. $response = [
  449. 'uri' => $uri,
  450. 'type' => 'post',
  451. 'data' => $uriData
  452. ];
  453. return $this->processRequest($response);
  454. }
  455. /**
  456. * Delete the series with the given ID
  457. *
  458. * @param int $id
  459. * @param bool|true $deleteFiles
  460. * @return string
  461. */
  462. public function deleteSeries($id, $deleteFiles = true)
  463. {
  464. $uri = 'series';
  465. $uriData = [];
  466. $uriData['deleteFiles'] = ($deleteFiles) ? 'true' : 'false';
  467. $response = [
  468. 'uri' => $uri . '/' . $id,
  469. 'type' => 'delete',
  470. 'data' => $uriData
  471. ];
  472. return $this->processRequest($response);
  473. }
  474. /**
  475. * Searches for new shows on trakt
  476. * Search by name or tvdbid
  477. * Example: 'The Blacklist' or 'tvdb:266189'
  478. *
  479. * @param string $searchTerm query string for the search (Use tvdb:12345 to lookup TVDB ID 12345)
  480. * @return string
  481. */
  482. public function getSeriesLookup($searchTerm)
  483. {
  484. $uri = 'series/lookup';
  485. $uriData = [
  486. 'term' => $searchTerm
  487. ];
  488. $response = [
  489. 'uri' => $uri,
  490. 'type' => 'get',
  491. 'data' => $uriData
  492. ];
  493. return $this->processRequest($response);
  494. }
  495. /**
  496. * Get System Status
  497. *
  498. * @return string
  499. */
  500. public function getSystemStatus()
  501. {
  502. $uri = 'system/status';
  503. $response = [
  504. 'uri' => $uri,
  505. 'type' => 'get',
  506. 'data' => []
  507. ];
  508. return $this->preProcessRequest($response);
  509. }
  510. /**
  511. * Process requests with Guzzle
  512. *
  513. * @param array $params
  514. * @return \Psr\Http\Message\ResponseInterface
  515. */
  516. protected function _request(array $params)
  517. {
  518. $client = new Client(['verify' => getCert()]);
  519. $options = [
  520. 'headers' => [
  521. 'X-Api-Key' => $this->apiKey
  522. ]
  523. ];
  524. if ( $this->httpAuthUsername && $this->httpAuthPassword ) {
  525. $options['auth'] = [
  526. $this->httpAuthUsername,
  527. $this->httpAuthPassword
  528. ];
  529. }
  530. if($this->type == 'lidarr'){
  531. $params['version'] = 'v1/';
  532. }
  533. $version = $params['version'] ?? '';
  534. if ( $params['type'] == 'get' ) {
  535. $url = $this->url . '/api/' . $version . $params['uri'] . '?' . http_build_query($params['data']);
  536. return $client->get($url, $options);
  537. }
  538. if ( $params['type'] == 'put' ) {
  539. $url = $this->url . '/api/' . $version . $params['uri'];
  540. $options['json'] = $params['data'];
  541. return $client->put($url, $options);
  542. }
  543. if ( $params['type'] == 'post' ) {
  544. $url = $this->url . '/api/' . $version . $params['uri'];
  545. $options['json'] = $params['data'];
  546. return $client->post($url, $options);
  547. }
  548. if ( $params['type'] == 'delete' ) {
  549. $url = $this->url . '/api/' . $version . $params['uri'] . '?' . http_build_query($params['data']);
  550. return $client->delete($url, $options);
  551. }
  552. }
  553. /**
  554. * Process requests, catch exceptions, return json response
  555. *
  556. * @param array $request uri, type, data from method
  557. * @return string json encoded response
  558. */
  559. protected function processRequest(array $request)
  560. {
  561. try {
  562. $versionCheck = $this->getSystemStatus();
  563. $versionCheck = json_decode($versionCheck, true);
  564. $versionCheck = (is_array($versionCheck) && array_key_exists('version', $versionCheck)) ? $versionCheck['version'] : '1.0';
  565. $compare = new Comparator;
  566. switch ($this->type){
  567. case 'sonarr':
  568. $versionCheck = '';
  569. break;
  570. case 'radarr':
  571. $versionCheck = 'v3/';
  572. break;
  573. case 'lidarr':
  574. $versionCheck = 'v1/';
  575. break;
  576. default:
  577. $versionCheck = '';
  578. }
  579. } catch ( \Exception $e ) {
  580. return json_encode(array(
  581. 'error' => array(
  582. 'msg' => $e->getMessage(),
  583. 'code' => $e->getCode(),
  584. ),
  585. ));
  586. exit();
  587. }
  588. try {
  589. $response = $this->_request(
  590. [
  591. 'uri' => $request['uri'],
  592. 'type' => $request['type'],
  593. 'data' => $request['data'],
  594. 'version' => $versionCheck
  595. ]
  596. );
  597. } catch ( \Exception $e ) {
  598. return json_encode(array(
  599. 'error' => array(
  600. 'msg' => $e->getMessage(),
  601. 'code' => $e->getCode(),
  602. ),
  603. ));
  604. exit();
  605. }
  606. return $response->getBody()->getContents();
  607. }
  608. protected function preProcessRequest(array $request)
  609. {
  610. try {
  611. $response = $this->_request(
  612. [
  613. 'uri' => $request['uri'],
  614. 'type' => $request['type'],
  615. 'data' => $request['data']
  616. ]
  617. );
  618. } catch ( \Exception $e ) {
  619. return json_encode(array(
  620. 'error' => array(
  621. 'msg' => $e->getMessage(),
  622. 'code' => $e->getCode(),
  623. ),
  624. ));
  625. exit();
  626. }
  627. return $response->getBody()->getContents();
  628. }
  629. /**
  630. * Verify date is in proper format
  631. *
  632. * @param $date
  633. * @param string $format
  634. * @return bool
  635. */
  636. private function validateDate($date, $format = 'Y-m-d')
  637. {
  638. $d = \DateTime::createFromFormat($format, $date);
  639. return $d && $d->format($format) == $date;
  640. }
  641. }