musicbrainz.class.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <?php
  2. /**
  3. * Bibliotheca
  4. *
  5. * Copyright 2018-2023 Johannes Keßler
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see http://www.gnu.org/licenses/gpl-3.0.
  19. */
  20. /**
  21. * Class Musicbrainz
  22. *
  23. * Simple search for an artist and album
  24. *
  25. * https://musicbrainz.org/doc/Development
  26. * https://musicbrainz.org/doc/MusicBrainz_API/Examples
  27. * https://musicbrainz.org/doc/MusicBrainz_API/Search
  28. * https://musicbrainz.org/doc/MusicBrainz_API
  29. */
  30. class Musicbrainz {
  31. /**
  32. * @var bool DEBUG
  33. */
  34. private bool $_DEBUG = false;
  35. /**
  36. * @var string The user agent used to make curl calls
  37. */
  38. private mixed $_BROWSER_AGENT = '';
  39. /**
  40. * @var string The user agent lang used to make curl calls
  41. */
  42. private mixed $_BROWSER_LANG = '';
  43. /**
  44. * @var string The user agent accept used to make curl calls
  45. */
  46. private mixed $_BROWSER_ACCEPT = '';
  47. /**
  48. * @var string The musicbrainz API release endpoint
  49. */
  50. private string $_RELEASE_ENDPOINT = 'http://musicbrainz.org/ws/2/release/';
  51. /**
  52. * @var string The endpoint for images
  53. */
  54. private string $_IMAGE_ENDPOINT = 'http://coverartarchive.org/release/';
  55. /**
  56. * @var int The amount of entries returned for release search
  57. */
  58. private int $_resultLimit = 10;
  59. /**
  60. * Musicbrainz constructor.
  61. *
  62. * @param $options array
  63. */
  64. public function __construct(array $options) {
  65. if(isset($options['debug']) && !empty($options['debug'])) {
  66. $this->_DEBUG = true;
  67. }
  68. if(isset($options['resultLimit']) && !empty($options['resultLimit'])) {
  69. $this->_resultLimit = $options['resultLimit'];
  70. }
  71. $this->_BROWSER_AGENT = $options['browserAgent'];
  72. $this->_BROWSER_LANG = $options['browserLang'];
  73. $this->_BROWSER_ACCEPT = $options['browserAccept'];
  74. }
  75. /**
  76. * Search for a release fpr the given artist and album name
  77. *
  78. * http://musicbrainz.org/ws/2/release/?query=artist:broilers%20AND%20release:muerte%20AND%20format:CD&fmt=json
  79. *
  80. * [releaseID] = title - artist - status - date - country - disambiguation - packaging - track-count
  81. *
  82. *
  83. * @param string $artist The artist to search for
  84. * @param string $album The album of the artist to search for
  85. *
  86. * @return array
  87. */
  88. public function searchForRelease(string $artist, string $album): array {
  89. $ret = array();
  90. if(!empty($artist) && !empty($album)) {
  91. $artist = urlencode($artist);
  92. $album = urlencode($album);
  93. $url = $this->_RELEASE_ENDPOINT;
  94. $url .= '?&fmt=json&limit='.$this->_resultLimit.'&query=';
  95. $url .= 'artist:'.$artist.'%20AND%20release:'.$album.'%20AND%20format:CD';
  96. if(DEBUG) Summoner::sysLog("[DEBUG] musicbrainz release url: $url");
  97. $do = $this->_curlCall($url);
  98. $data = '';
  99. if(!empty($do)) {
  100. $data = json_decode($do, true);
  101. if(!empty($data)) {
  102. if(DEBUG) Summoner::sysLog("[DEBUG] musicbrainz releases json data:".Summoner::cleanForLog($data));
  103. }
  104. else {
  105. Summoner::sysLog("[ERROR] musicbrainz invalid releases json data:".Summoner::cleanForLog($do));
  106. }
  107. }
  108. if(!empty($data)) {
  109. if(isset($data['releases'])) {
  110. foreach($data['releases'] as $release) {
  111. if(isset($release['title'])
  112. && isset($release['status'])
  113. && isset($release['date'])
  114. && isset($release['country'])
  115. && isset($release['artist-credit'][0]['name'])) {
  116. $ret[$release['id']] = $release['title'].' - '.$release['artist-credit'][0]['name'].'; '.$release['status'].'; '.$release['date'].'; '.$release['country'];
  117. if(isset($release['disambiguation'])) {
  118. $ret[$release['id']] .= '; '.$release['disambiguation'];
  119. }
  120. if(isset($release['packaging'])) {
  121. $ret[$release['id']] .= '; '.$release['packaging'];
  122. }
  123. if(isset($release['track-count'])) {
  124. $ret[$release['id']] .= '; tracks: '.$release['track-count'];
  125. }
  126. }
  127. }
  128. }
  129. }
  130. }
  131. return $ret;
  132. }
  133. /**
  134. * Get the information from musicBrainz by given release ID
  135. * https://musicbrainz.org/doc/MusicBrainz_API/Examples#Release
  136. *
  137. * http://musicbrainz.org/ws/2/release/59211ea4-ffd2-4ad9-9a4e-941d3148024a?inc=recordings&fmt=json
  138. *
  139. * [album] => title
  140. * [date] => date
  141. * [artist] => artist-credit name
  142. * [tracks] => number - title - min
  143. * [image] => img url
  144. * [runtime] => summed up runtime in minutes from tracks
  145. *
  146. * @param string $releaseId
  147. * @return array
  148. */
  149. public function getReleaseInfo(string $releaseId): array {
  150. $ret = array();
  151. if(!empty($releaseId)) {
  152. $url = $this->_RELEASE_ENDPOINT;
  153. $url .= $releaseId;
  154. $url .= '?&fmt=json&inc=recordings+artist-credits';
  155. $do = $this->_curlCall($url);
  156. $data = '';
  157. if(!empty($do)) {
  158. $data = json_decode($do, true);
  159. if(!empty($data)) {
  160. if(DEBUG) Summoner::sysLog("[DEBUG] musicbrainz release json data:".Summoner::cleanForLog($data));
  161. }
  162. else {
  163. Summoner::sysLog("[ERROR] musicbrainz invalid release json data:".Summoner::cleanForLog($do));
  164. }
  165. }
  166. if(!empty($data)) {
  167. $ret['id'] = isset($data['id']) ? $data['id'] : '';
  168. $ret['album'] = isset($data['title']) ? $data['title'] : '';
  169. $ret['date'] = isset($data['date']) ? $data['date'] : '';
  170. $ret['artist'] = isset($data['artist-credit'][0]['name']) ? $data['artist-credit'][0]['name'] : '';
  171. $ret['tracks'] = '';
  172. $ret['image'] = '';
  173. $ret['runtime'] = 0;
  174. foreach($data['media'] as $media) {
  175. foreach($media['tracks'] as $track) {
  176. $ret['runtime'] += $track['length'];
  177. $l = (int) round($track['length'] / 1000);
  178. $l = date("i:s",$l);
  179. $ret['tracks'] .= $track['number'].' - '.$track['title'].' - '.$l."\n";
  180. }
  181. }
  182. $ret['runtime'] = round($ret['runtime'] / 1000 / 60);
  183. // image
  184. $do = $this->_curlCall($this->_IMAGE_ENDPOINT.$releaseId);
  185. if(!empty($do)) {
  186. $imageData = json_decode($do, true);
  187. if(!empty($imageData)) {
  188. if(DEBUG) Summoner::sysLog("[DEBUG] image release json data:".Summoner::cleanForLog($imageData));
  189. $ret['image'] = isset($imageData['images'][0]['image']) ? $imageData['images'][0]['image'] : '';
  190. }
  191. else {
  192. Summoner::sysLog("[ERROR] image invalid release json data:".Summoner::cleanForLog($do));
  193. }
  194. }
  195. }
  196. }
  197. return $ret;
  198. }
  199. /**
  200. * Download given URL to a tmp file
  201. * make sure to remove the tmp file after use
  202. *
  203. * @param string $url
  204. * @return string
  205. */
  206. public function downloadCover(string $url): string {
  207. $ret = '';
  208. $_tmpFile = tempnam(sys_get_temp_dir(), "bibliotheca-");
  209. $fh = fopen($_tmpFile,"w+");
  210. if($fh !== false) {
  211. $ch = curl_init($url);
  212. curl_setopt($ch, CURLOPT_FILE, $fh);
  213. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
  214. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  215. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  216. curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
  217. curl_setopt($ch, CURLOPT_USERAGENT, $this->_BROWSER_AGENT);
  218. if($this->_DEBUG) {
  219. curl_setopt($ch, CURLOPT_VERBOSE, true);
  220. curl_setopt($ch, CURLOPT_HEADERFUNCTION,
  221. function($curl, $header) use (&$_headers) {
  222. $len = strlen($header);
  223. $header = explode(':', $header, 2);
  224. if (count($header) < 2) { // ignore invalid headers
  225. return $len;
  226. }
  227. $_headers[strtolower(trim($header[0]))][] = trim($header[1]);
  228. return $len;
  229. }
  230. );
  231. }
  232. curl_exec($ch);
  233. curl_close($ch);
  234. if($this->_DEBUG) {
  235. Summoner::sysLog('[DEBUG] '.__METHOD__.' headers '.Summoner::cleanForLog($_headers));
  236. }
  237. $ret = $_tmpFile;
  238. }
  239. fclose($fh);
  240. return $ret;
  241. }
  242. /**
  243. * execute a curl call to the given $url
  244. *
  245. * @param string $url The request url
  246. * @return string
  247. */
  248. private function _curlCall(string $url): string {
  249. $ret = '';
  250. $ch = curl_init();
  251. curl_setopt($ch, CURLOPT_URL, $url);
  252. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  253. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
  254. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  255. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  256. curl_setopt($ch, CURLOPT_MAXREDIRS, 2);
  257. curl_setopt($ch, CURLOPT_USERAGENT, $this->_BROWSER_AGENT);
  258. curl_setopt($ch, CURLOPT_HTTPHEADER, array(
  259. 'Accept: '.$this->_BROWSER_ACCEPT,
  260. 'Accept-Language: '.$this->_BROWSER_LANG)
  261. );
  262. if($this->_DEBUG) {
  263. $_headers = array();
  264. curl_setopt($ch, CURLOPT_VERBOSE, true);
  265. curl_setopt($ch, CURLOPT_HEADERFUNCTION,
  266. function($curl, $header) use (&$_headers) {
  267. $len = strlen($header);
  268. $header = explode(':', $header, 2);
  269. if (count($header) < 2) { // ignore invalid headers
  270. return $len;
  271. }
  272. $_headers[strtolower(trim($header[0]))][] = trim($header[1]);
  273. return $len;
  274. }
  275. );
  276. }
  277. $do = curl_exec($ch);
  278. if(is_string($do) === true) {
  279. $ret = $do;
  280. }
  281. curl_close($ch);
  282. if($this->_DEBUG) {
  283. Summoner::sysLog('[DEBUG] '.__METHOD__.' headers '.Summoner::cleanForLog($_headers));
  284. }
  285. return $ret;
  286. }
  287. }