1 define([ 2 '../lib/class', 3 '../src/playlist.js', 4 '../src/api/lfmclient.js', 5 '../src/player.js', 6 '../lib/jquery.url.js', 7 '../lib/jquery.cookie.js' 8 ], 9 function (Class, Playlist, API, Player) { 10 11 /** 12 * Transistor is a library for building Last.fm Radio clients 13 * See http://github.com/baseonmars/Transistor and 14 * http://www.last.fm/api for details 15 * @class 16 * @author dane <dane@last.fm> 17 */ 18 var Transistor = Class.extend( 19 /** @lends Transistor# */ 20 { 21 22 /** 23 * @constructs 24 * @param cfg Configuration object 25 * @param [cfg.player] A player 26 * @param [cfg.sm2] SoundManager instance, else window.soundManager used 27 * @param [cfg.api] Api client to use 28 * @param {String} [cfg.key] Your api key 29 * @param {String} [cfg.secret] Your api secret 30 * @param {String} [cfg.session] Your api session 31 * @param {String} [cfg.scrobble=true] Should tracks be scrobbled 32 * and now playing notifications be sent? 33 */ 34 init: function(cfg) { 35 cfg = cfg || {}; 36 37 this.player = cfg.player || new Player(cfg.sm2); 38 this.api = cfg.api || new API(cfg.key, cfg.secret, cfg.session); 39 this.playlist = cfg.playlist || new Playlist(); 40 this.scrobbling = false === cfg.scrobble ? false : true; 41 42 this.playlist.api = this.api; 43 this.player.playlist = this.playlist; 44 this.station = null; 45 46 var self = this; 47 48 amplify.subscribe('transistorplayer:finished', this, this.onFinished); 49 amplify.subscribe('transistorplayer:playing', this, this.onNowPlaying); 50 amplify.subscribe('transistorplayer:scrobblepoint', this, this.onScrobblePoint); 51 amplify.subscribe('transistorplayer:whileplaying', this, this.onPlayback); 52 53 var token = jQuery.url().param('token'); 54 55 if (!this.api.session && token) { 56 this.getSession(token); 57 } else { 58 if (jQuery.cookie('transistor')); 59 this.auth(); 60 } 61 }, 62 63 /** 64 * Authenticate a user, or enter them into the web auth flow 65 * @param {String} [session] The session key 66 */ 67 auth: function(session) { 68 69 if (session) { 70 this.api.session = session; 71 } else { 72 var cookie = jQuery.cookie('transistor'); 73 if (cookie) { 74 this.api.session = cookie; 75 } else { 76 this.api.authFlow(); 77 } 78 } 79 }, 80 81 /** 82 * Deauthenticates the current user, clearing the transistor cookie 83 */ 84 deauth: function () { 85 jQuery.cookie('transistor', null); 86 jQuery.cookie('transistorU', null); 87 this.api.session = null; 88 }, 89 90 /** 91 * @private 92 * Exchanges an auth token into a web service session 93 * @param {String} token The token to exchange for a session 94 */ 95 getSession: function (token) { 96 97 var response = this.api.request('auth.getSession', { 98 token: token 99 }); 100 101 var auth = this.api.request('auth.getSession', { 102 token: token 103 }); 104 105 var self = this; 106 auth.done(function (session) { 107 108 self.api.session = session.key; 109 110 jQuery.cookie('transistor', session.key, {expires: 365}); 111 jQuery.cookie('transistorU', session.username, {expires: 365}); 112 113 amplify.publish('transistor:authorised', session); 114 115 // clean up the url 116 if (window.history && window.history.pushState) { 117 window.history.pushState(null, window.document.title, window.location.pathname); 118 } 119 }); 120 121 auth.fail(function (data) { 122 console.log('Auth failed', data); 123 }); 124 125 }, 126 127 128 /** 129 * Tune the authenticated user to the given station url 130 * @param {String} url The station url, including the lastfm:// protocol 131 * @param {Function} [ok] Success callback 132 * @param {Function} [error] Failure callback 133 * @returns {Promise} The promise return by the api request, with callbacks attached 134 */ 135 tune: function(url, ok, error) { 136 137 var self = this; 138 var request = this.api.request("radio.tune", {station: url}); 139 request.done(ok).fail(error); 140 141 request.done(function (data) { 142 self.station = data; 143 amplify.publish("transistor:tuned", data); 144 }); 145 146 return request; 147 }, 148 149 /** 150 * Plays a new track if none is playing. Requests the next track from 151 * the playlist. If the playlist is empty a request to the 152 * radio.getPlaylist service is sent. If a track is paused this method 153 * resumes it's playback. 154 * @param {Function} [ok] Success callback 155 * @param {Function} [error] Failure callback 156 * @returns {Promise} The promise return by the api request, with callbacks attached 157 */ 158 play: function(ok, error) { 159 160 var request = jQuery.Deferred(); 161 request.done(ok).fail(error); 162 163 if (this.player.hasTrack()) { 164 try { 165 this.player.play(); 166 request.resolve(); 167 } catch(e) { 168 request.reject(e); 169 } 170 } else { 171 request = this.next(ok, error); 172 } 173 174 return request; 175 }, 176 177 /** 178 * Pause the currently playing track 179 */ 180 pause: function() { 181 182 this.paused = true; 183 this.player.pause(); 184 }, 185 186 /** 187 * Skips the current track and starts playing the next 188 * item on the playlist. 189 * @param {Function} [ok] Success callback 190 * @param {Function} [error] Failure callback 191 * @returns {Promise} The promise return by the api request, with callbacks attached 192 */ 193 skip: function(ok, error) { 194 195 var oldPlaying = this.playlist.current(); 196 197 var request = this.next().done(ok).fail(error); 198 if (oldPlaying) { 199 amplify.publish("transistor:skipped", oldPlaying); 200 } 201 202 return request; 203 }, 204 205 /** 206 * Requests the next track from the playlist and starts 207 * playing it. 208 * This method does not fire any amplify events 209 * @param {Function} [ok] Success callback 210 * @param {Function} [error] Failure callback 211 * @returns {Promise} The promise return by the api request, with callbacks attached 212 */ 213 next: function(ok, error) { 214 215 var next = this.playlist.next(); 216 var request = jQuery.Deferred().done(ok).fail(error); 217 218 if (!next) { 219 220 request = this.api.request("radio.getPlaylist", {rtp: 1}); 221 222 var self = this; 223 request.done(function(data) { 224 self.playlist.append(data.tracks); 225 self.player.play(self.playlist.next()); 226 }); 227 request.fail(error); 228 } else { 229 try { 230 this.player.play(next); 231 request.resolve(); 232 } catch (e) { 233 request.reject(e); 234 } 235 } 236 237 return request; 238 }, 239 240 /** 241 * Love a track. 242 * @param [track] The track to love, if not supplied 243 * uses currently playing track 244 * @param {Function} [ok] Success callback 245 * @param {Function} [error] Failure callback 246 */ 247 love: function (track, ok, error) { 248 249 track = track || this.playlist.current(); 250 251 var request = this.api.request('track.love', { 252 track: track.title, 253 artist: track.artist 254 }).done(ok).fail(error); 255 256 request.done(function() { 257 track.loved = true; 258 amplify.publish('transistor:loved', track); 259 }); 260 261 return request; 262 }, 263 264 /** 265 * Unlove a track. 266 * @param [track] The track to unlove, if not supplied 267 * uses currently playing track 268 * @param {Function} [ok] Success callback 269 * @param {Function} [error] Failure callback 270 */ 271 unlove: function(track, ok, error) { 272 273 track = track || this.playlist.current(); 274 275 var request = this.api.request('track.unlove', { 276 track: track.title, 277 artist: track.artist 278 }).done(ok).fail(error); 279 280 request.done(function() { 281 track.loved = false; 282 amplify.publish('transistor:unloved', track); 283 }); 284 285 return request; 286 }, 287 288 /** 289 * Ban's and skip's a track 290 * @param [track] The track to ban 291 * @param {Function} [ok] Success callback 292 * @param {Function} [error] Failure callback 293 * @returns {Promise} The promise return by the api request, with callbacks attached 294 */ 295 ban: function (track, ok, error) { 296 297 track = track || this.playlist.current(); 298 299 var request = this.api.request('track.ban', { 300 track: track.title, 301 artist: track.artist 302 }).done(ok).fail(error); 303 304 var self = this; 305 request.done(function () { 306 track.banned = true; 307 amplify.publish('transistor:banned', track); 308 self.skip(); 309 }); 310 311 return request; 312 }, 313 314 /** 315 * Scrobble a track 316 * @param [track] The track to scrobble, uses currently playing 317 * track if none provided. 318 * @param {Function} [ok] Success callback 319 * @param {Function} [error] Failure callback 320 * @returns {Promise} The promise return by the api request, with callbacks attached 321 */ 322 scrobble: function (track, ok, error) { 323 324 var request; 325 if (this.scrobbling) { 326 track = track || this.playlist.current(); 327 328 request = this.api.request("track.scrobble", { 329 "artist" : track.artist, 330 "track" : track.title, 331 "timestamp" : track.start, 332 "album" : track.album, 333 "duration" : Math.floor(track.duration/1000), 334 "chosenByUser": track.chosenByUser ? 1 : 0 335 }).done(ok).fail(error); 336 337 request.done(function () { 338 amplify.publish('transistor:scrobbled', track); 339 }); 340 } else { 341 342 request = jQuery.Deferred().fail(error).reject(); 343 } 344 345 return request; 346 }, 347 348 /** 349 * Set the player volume 350 * @param {Number} vol The volume, valid ranges are 0-100 351 */ 352 setVolume: function(vol) { 353 354 this.player.setVolume(vol); 355 }, 356 357 /** 358 * Should the player send scrobble and now player notifications 359 * @param {Boolean} scrobble true = scrobbling, false = private listening 360 */ 361 setScrobble: function(scrobble) { 362 363 this.scrobble = scrobble; 364 }, 365 366 /** 367 * Get the username of the authorised user 368 * @returns {String|Boolean} Username if authed or false 369 */ 370 getUsername: function () { 371 372 var username = false; 373 if (this.api.session) { 374 username = jQuery.cookie('transistorU'); 375 } 376 return username; 377 }, 378 379 /** 380 * Return the point at which a given track should be scrobbled 381 * @param track The track 382 * @returns {Number} The position at which the track should 383 * be scrobbled, in milliseconds 384 */ 385 scrobblePoint: function(track) { 386 387 var minScrobble = 30000, maxScrobble = 240000; 388 var duration = track.duration; 389 var scrobblePoint, midPoint; 390 391 if (duration >= minScrobble) { 392 midPoint = Math.floor(duration / 2); 393 if (midPoint >= maxScrobble) { 394 scrobblePoint = maxScrobble; 395 } else { 396 scrobblePoint = midPoint; 397 } 398 } else { 399 scrobblePoint = false; 400 } 401 return scrobblePoint; 402 }, 403 404 /** 405 * @private 406 * Now playing callback, call when a track starts playing to 407 * send a now playing request 408 * @param id The id of the player 409 * @param track The now playing track 410 */ 411 onNowPlaying: function(id, track) { 412 413 if (this.player.id !== id) return; 414 415 track.start = Math.floor((new Date()).getTime() / 1000); 416 track.scrobblePoint = this.scrobblePoint(track); 417 418 var request; 419 if (this.scrobbling) { 420 this.api.request('track.updateNowPlaying', { 421 'track' : track.title, 422 'artist' : track.artist, 423 'album' : track.album, 424 'duration' : Math.floor(track.duration/1000) 425 }); 426 } 427 }, 428 429 /** 430 * @private 431 * Track finished callback, call when a track completes 432 * @param id The id of the player 433 * @param track The now playing track 434 */ 435 onFinished: function(id, track) { 436 437 if (this.player.id !== id) return; 438 439 this.play(); 440 }, 441 442 /** 443 * @private 444 * On playback callback, call periodically to report 445 * position status. 446 * @param id The id of the player 447 * @param track The playing track 448 * @param timings The timings for the track 449 * @param timings.position The position of the track in 450 * milliseconds 451 * @param timings.duration The duration of the track as 452 * reported by the player (This may be adjusted as the track 453 * is downloaded). 454 */ 455 onPlayback: function(id, track, timings) { 456 457 if (this.player.id !== id) return; 458 459 if (!track.scrobbled && 460 track.scrobblePoint && 461 timings.position >= track.scrobblePoint) { 462 track.scrobbled = true; 463 this.scrobble(track); 464 } 465 } 466 467 }); 468 469 return Transistor; 470 }); 471