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