scrobble-utils.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. /* GNU FM -- a free network service for sharing your music listening habits
  3. Copyright (C) 2013 Free Software Foundation, Inc
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU Affero General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU Affero General Public License for more details.
  12. You should have received a copy of the GNU Affero General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. /**
  16. * Functions used by TrackXML.php:scrobble() and TrackXML.php:updateNowPlaying()
  17. * Similar but not identical to gnukebox/scrobble-utils.php
  18. */
  19. require_once('database.php');
  20. /**
  21. * Get artist ID and add artist to database if it doesnt already exist.
  22. *
  23. * @param string artist Artist name.
  24. * @return int Artist ID.
  25. */
  26. function getArtistID($artist) {
  27. global $adodb;
  28. $query = 'SELECT id FROM Artist WHERE name=?';
  29. $params = array($artist);
  30. $artist_id = $adodb->GetOne($query, $params);
  31. if (!$artist_id) {
  32. // Artist doesn't exist, so we create them
  33. $query = 'INSERT INTO Artist (name) VALUES (?)';
  34. $params = array($artist);
  35. $res = $adodb->Execute($query, $params);
  36. return getArtistID($artist);
  37. } else {
  38. return $artist_id;
  39. }
  40. }
  41. /**
  42. * Get album ID and add album to database if it doesnt already exist.
  43. *
  44. * @param string artist Artist name.
  45. * @param string album Album name.
  46. * @return int Album ID.
  47. *
  48. * @todo Maybe we should return artist ID too, we will need it when db gets normalized
  49. */
  50. function getAlbumID($artist, $album) {
  51. global $adodb;
  52. $query = 'SELECT id FROM Album WHERE name=? AND artist_name=?';
  53. $params = array($album, $artist);
  54. $album_id = $adodb->GetOne($query, $params);
  55. if (!$album_id) {
  56. // Album doesn't exist, so create it
  57. // First check if artist exist, if not create it
  58. $artist_id = getArtistID($artist);
  59. $query = 'INSERT INTO Album (name, artist_name) VALUES (?,?)';
  60. $params = array($album, $artist);
  61. $adodb->Execute($query, $params);
  62. return getAlbumID($artist, $album);
  63. } else {
  64. return $album_id;
  65. }
  66. }
  67. /**
  68. * Get track ID and add track to database if it doesnt already exist.
  69. *
  70. * @param string artist Artist name.
  71. * @param string album Album name.
  72. * @param string track Track name.
  73. * @param string mbid Track's musicbrainz ID.
  74. * @param int duration Track length in seconds.
  75. * @return int Track ID.
  76. *
  77. */
  78. function getTrackID($artist, $album, $track, $mbid, $duration) {
  79. global $adodb;
  80. if ($album === null) {
  81. $query = 'SELECT id FROM Track WHERE name=? AND artist_name=? AND album_name IS NULL';
  82. $params = array($track, $artist);
  83. } else {
  84. $query = 'SELECT id FROM Track WHERE name=? AND artist_name=? AND album_name=?';
  85. $params = array($track, $artist, $album);
  86. }
  87. $track_id = $adodb->GetOne($query, $params);
  88. if (!$track_id) {
  89. // First check if artist and album exists, if not create them
  90. if ($album === null) {
  91. $artist_id = getArtistID($artist);
  92. } else {
  93. $album_id = getAlbumID($artist, $album);
  94. }
  95. // Create new track
  96. $query = 'INSERT INTO Track (name, artist_name, album_name, mbid, duration) VALUES (?,?,?,?,?)';
  97. $params = array($track, $artist, $album, $mbid, $duration);
  98. $adodb->Execute($query, $params);
  99. return getTrackID($artist, $album, $track, $mbid, $duration);
  100. } else {
  101. return $track_id;
  102. }
  103. }
  104. /**
  105. * Get scrobble_track ID and add track to Scrobble_Track db table if it doesnt already exist.
  106. *
  107. * @param string artist Artist name.
  108. * @param string album Album name.
  109. * @param string track Track name.
  110. * @param string mbid Track musicbrainz ID.
  111. * @param int duration Track length in seconds.
  112. * @param int track_id Track ID in Track database table
  113. * @return int Scrobble_Track ID.
  114. */
  115. function getScrobbleTrackID($artist, $album, $track, $mbid, $duration, $track_id) {
  116. global $adodb;
  117. $query = 'SELECT id FROM Scrobble_Track WHERE name=lower(?) AND artist=lower(?)';
  118. $params = array($track, $artist);
  119. if ($album === null) {
  120. $query .= ' AND album IS NULL';
  121. } else {
  122. $query .= ' AND album=lower(?)';
  123. $params[] = $album;
  124. }
  125. if ($mbid === null) {
  126. $query .= ' AND mbid IS NULL';
  127. } else {
  128. $query .= ' AND mbid=lower(?)';
  129. $params[] = $mbid;
  130. }
  131. $scrobbletrack_id = $adodb->GetOne($query, $params);
  132. if (!$scrobbletrack_id) {
  133. $query = 'INSERT INTO Scrobble_Track (name, artist, album, mbid, track) VALUES (lower(?), lower(?), lower(?), lower(?), ?)';
  134. $params = array($track, $artist, $album, $mbid, $track_id);
  135. $res = $adodb->Execute($query, $params);
  136. return getScrobbleTrackID($artist, $album, $track, $mbid, $duration, $track_id);
  137. } else {
  138. return $scrobbletrack_id;
  139. }
  140. }
  141. /**
  142. * Correct artist/album/track/mbid/timestamp input
  143. *
  144. * @param mixed input Input to be corrected.
  145. * @param string type Type of input to be corrected.
  146. * @return array Array(mixed $old_input, mixed $corrected_input, int $corrected)
  147. *
  148. */
  149. function correctInput($input, $type) {
  150. $old = $input;
  151. $new = $old;
  152. if ($type == 'artist' || $type == 'album' || $type == 'track' || $type == 'albumartist') {
  153. //Limit strings to 255 chars
  154. switch (mb_detect_encoding($new)) {
  155. case 'ASCII':
  156. case 'UTF-8':
  157. $new = mb_strcut($new, 0, 255, 'UTF-8');
  158. break;
  159. default:
  160. $new = null;
  161. }
  162. // Remove spam and trim whitespace
  163. $new = str_replace(' (PREVIEW: buy it at www.magnatune.com)', '', $new);
  164. $new = trim($new);
  165. if (empty($new)) {
  166. $new = null;
  167. }
  168. } else if ($type == 'mbid') {
  169. if (isset($new)) {
  170. $new = strtolower(rtrim($new));
  171. if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $new)) {
  172. //do nothing
  173. } else {
  174. $new = null;
  175. }
  176. } else {
  177. $new = null;
  178. }
  179. } else if ($type == 'timestamp') {
  180. $new = (int) $new;
  181. } else if ($type == 'duration') {
  182. if($new) {
  183. $new = (int) $new;
  184. } else {
  185. $new = null;
  186. }
  187. } else if ($type == 'tracknumber') {
  188. if($new) {
  189. $new = (int) $new;
  190. }else {
  191. $new = null;
  192. }
  193. }
  194. $result = array($old, $new, (int)($old != $new));
  195. return $result;
  196. }
  197. /**
  198. * Decide if and why we should ignore a track
  199. *
  200. * @param string input Input data
  201. * @param string type Type of input data
  202. * @return array Array(int $ignored_code, string $ignored_message)
  203. */
  204. function ignoreInput($input, $type) {
  205. $ignored_code = 0;
  206. $ignored_message = '';
  207. if ($type == 'artist' && empty($input)) {
  208. $ignored_code = 1;
  209. $ignored_message = 'Artist was ignored';
  210. }
  211. if ($type == 'track' && empty($input)) {
  212. $ignored_code = 2;
  213. $ignored_message = 'Track was ignored';
  214. }
  215. if ($type == 'timestamp') {
  216. $timestamp_upperlimit = time() + 300;
  217. $timestamp_lowerlimit = 1009000000;
  218. if ($input > $timestamp_upperlimit) {
  219. $ignored_message = 'Timestamp is too new';
  220. $ignored_code = 3;
  221. }
  222. if ($input < $timestamp_lowerlimit) {
  223. $ignored_message = 'Timestamp is too old';
  224. $ignored_code = 4;
  225. }
  226. }
  227. return array($ignored_code, $ignored_message);
  228. }
  229. /**
  230. * Prepare a track for entering the database.
  231. * Tries to correct a track's data or marks it as invalid
  232. *
  233. * @param array t Array of track data.
  234. * @param int userid User ID.
  235. * @param string type Type of track, 'nowplaying' or 'scrobble'.
  236. * @return array Same array as t array, but with corrected data and added metadata.
  237. */
  238. function prepareTrack($userid, $t, $type) {
  239. list($t['track_old'], $t['track'], $t['track_corrected']) = correctInput($t['track'], 'track');
  240. list($t['artist_old'], $t['artist'], $t['artist_corrected']) = correctInput($t['artist'], 'artist');
  241. list($t['album_old'], $t['album'], $t['album_corrected']) = correctInput($t['album'], 'album');
  242. list($t['mbid_old'], $t['mbid'], $t['mbid_corrected']) = correctInput($t['mbid'], 'mbid');
  243. list($t['duration_old'], $t['duration'], $t['duration_corrected']) = correctInput($t['duration'], 'duration');
  244. list($t['albumartist_old'], $t['albumartist'], $t['albumartist_corrected']) = correctInput($t['albumartist'], 'albumartist');
  245. list($t['tracknumber_old'], $t['tracknumber'], $t['tracknumber_corrected']) = correctInput($t['tracknumber'], 'tracknumber');
  246. //TODO not pretty
  247. list($t['ignored_code'], $t['ignored_message']) = ignoreInput($t['artist'], 'artist');
  248. if($t['ignored_code'] === 0) {
  249. list($t['ignored_code'], $t['ignored_message']) = ignoreInput($t['track'], 'track');
  250. }
  251. if ($type == 'scrobble') {
  252. list($t['timestamp_old'], $t['timestamp'], $t['timestamp_corrected']) = correctInput($t['timestamp'], 'timestamp');
  253. if($t['ignored_code'] === 0) {
  254. list($t['ignored_code'], $t['ignored_message']) = ignoreInput($t['timestamp'], 'timestamp');
  255. }
  256. if ($t['ignored_code'] === 0) {
  257. $exists = scrobbleExists($userid, $t['artist'], $t['track'], $t['timestamp']);
  258. if ($exists) {
  259. $t['ignored_code'] = 91; // GNU FM specific
  260. $t['ignored_message'] = 'Already scrobbled';
  261. }
  262. }
  263. }
  264. return $t;
  265. }
  266. /**
  267. * Check if a scrobble has already been added to database.
  268. *
  269. * @param int userid User ID
  270. * @param string artist Artist name
  271. * @param string track Track name
  272. * @param int time Timestamp
  273. * @return bool True is scrobble exists, False if not.
  274. */
  275. function scrobbleExists($userid, $artist, $track, $time) {
  276. global $adodb;
  277. $query = 'SELECT time FROM Scrobbles WHERE userid=? AND artist=? AND track=? AND time=?';
  278. $params = array($userid, $artist, $track, $time);
  279. $res = $adodb->GetOne($query, $params);
  280. if (!$res) {
  281. return false;
  282. } else {
  283. return true;
  284. }
  285. }
  286. /**
  287. * Sends a scrobble on to any other services the user has connected to their account
  288. *
  289. * @todo copied from gnukebox/scrobble-utils.php,
  290. * we should review code and see if we can improve, additional params and batch scrobbling would be cool.
  291. * @todo docs
  292. */
  293. function forwardScrobble($userid, $artist, $album, $track, $time, $mbid, $source, $rating, $length) {
  294. global $adodb, $lastfm_key, $lastfm_secret;
  295. $artist = rawurlencode($artist);
  296. $track = rawurlencode($track);
  297. $album = rawurlencode($album);
  298. $mbid = rawurlencode($mbid);
  299. $source = rawurlencode($source);
  300. $rating = rawurlencode($rating);
  301. $length = rawurlencode($length);
  302. $res = $adodb->CacheGetAll(600, 'SELECT * FROM Service_Connections WHERE userid = ' . $userid . ' AND forward = 1');
  303. foreach ($res as &$row) {
  304. $remote_key = $row['remote_key'];
  305. $ws_url = $row['webservice_url'];
  306. $curl_session = curl_init($ws_url);
  307. $post_vars = '';
  308. if ($album) {
  309. $post_vars .= 'album[0]=' . $album . '&';
  310. }
  311. $post_vars .= 'api_key=' . $lastfm_key . '&artist[0]=' . $artist;
  312. if ($length) {
  313. $post_vars .= '&length[0]=' . $length;
  314. }
  315. if ($mbid) {
  316. $post_vars .= '&mbid[0]=' . $mbid;
  317. }
  318. $post_vars .= '&method=track.scrobble';
  319. if ($rating) {
  320. $post_vars .= '&rating[0]=' . $rating;
  321. }
  322. $post_vars .= '&sk=' . $remote_key;
  323. if ($source) {
  324. $post_vars .= '&source[0]='. $source;
  325. }
  326. $post_vars .= '&timestamp[0]=' . $time . '&track[0]=' . $track;
  327. $sig = urldecode(str_replace('&', '', $post_vars));
  328. $sig = str_replace('=', '', $sig);
  329. $sig = md5($sig . $lastfm_secret);
  330. $post_vars .= '&api_sig=' . $sig;
  331. curl_setopt($curl_session, CURLOPT_POST, true);
  332. curl_setopt($curl_session, CURLOPT_POSTFIELDS, $post_vars);
  333. curl_setopt($curl_session, CURLOPT_RETURNTRANSFER, true);
  334. curl_setopt($curl_session, CURLOPT_CONNECTTIMEOUT, 1);
  335. curl_setopt($curl_session, CURLOPT_TIMEOUT, 1);
  336. $response = curl_exec($curl_session);
  337. curl_close($curl_session);
  338. }
  339. }