diff --git a/mopidy_spotmop/__init__.py b/mopidy_spotmop/__init__.py index c85733a..ad10ff7 100755 --- a/mopidy_spotmop/__init__.py +++ b/mopidy_spotmop/__init__.py @@ -9,9 +9,10 @@ from services.upgrade import upgrade from services.pusher import pusher from services.auth import auth +from services.queuer import queuer from mopidy import config, ext -__version__ = '2.7.3' +__version__ = '2.7.4' __ext_name__ = 'spotmop' __verbosemode__ = False @@ -73,13 +74,15 @@ def spotmop_client_factory(config, core): }), (r'/pusher/([^/]+)', pusher.PusherRequestHandler, { 'core': core, - 'config': config, - 'version': __version__ + 'config': config }), (r'/auth', auth.AuthRequestHandler, { 'core': core, - 'config': config, - 'version': __version__ + 'config': config + }), + (r'/queuer/([^/]*)', queuer.QueuerRequestHandler, { + 'core': core, + 'config': config }), (r"/images/(.*)", tornado.web.StaticFileHandler, { "path": artworklocation diff --git a/mopidy_spotmop/services/auth/auth.py b/mopidy_spotmop/services/auth/auth.py index 3a64346..a275211 100644 --- a/mopidy_spotmop/services/auth/auth.py +++ b/mopidy_spotmop/services/auth/auth.py @@ -11,12 +11,10 @@ class AuthRequestHandler(tornado.web.RequestHandler): def set_default_headers(self): self.set_header("Access-Control-Allow-Origin", "*") - def initialize(self, core, config, version): + def initialize(self, core, config): self.core = core self.config = config - self.version = version - # check if we're able to upgrade, and what our current version is def get(self): url = 'https://accounts.spotify.com/api/token' diff --git a/mopidy_spotmop/services/pusher/identify.py b/mopidy_spotmop/services/pusher/identify.py index a9b7ebd..cf2df5d 100644 --- a/mopidy_spotmop/services/pusher/identify.py +++ b/mopidy_spotmop/services/pusher/identify.py @@ -11,10 +11,9 @@ class IdentifyRequestHandler(tornado.web.RequestHandler): def set_default_headers(self): self.set_header("Access-Control-Allow-Origin", "*") - def initialize(self, core, config, version): + def initialize(self, core, config): self.core = core self.config = config - self.version = version def get(self): ip = self.request.remote_ip diff --git a/mopidy_spotmop/services/pusher/index.html b/mopidy_spotmop/services/pusher/index.html deleted file mode 100644 index 18c584d..0000000 --- a/mopidy_spotmop/services/pusher/index.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - WebSockets Client - - - -Enter text to send to the websocket server: -
- loading - - -
-
- - - \ No newline at end of file diff --git a/mopidy_spotmop/services/pusher/pusher.py b/mopidy_spotmop/services/pusher/pusher.py index 447638b..ce133d6 100644 --- a/mopidy_spotmop/services/pusher/pusher.py +++ b/mopidy_spotmop/services/pusher/pusher.py @@ -47,10 +47,9 @@ def set_default_headers(self): self.set_header("Access-Control-Allow-Headers", "X-Requested-With") self.set_header("Content-Type", "application/json") - def initialize(self, core, config, version): + def initialize(self, core, config): self.core = core self.config = config - self.version = version # get method def get(self, action): @@ -72,12 +71,11 @@ def get(self, action): clientDetailsList.append(client['details']) self.write(json_encode(clientDetailsList)) -def spotmop_pusher_factory(config, core, version): +def spotmop_pusher_factory(config, core): return [ ('/', PusherRequestHandler, { 'core': core, - 'config': config, - 'version': version + 'config': config }) ] diff --git a/mopidy_spotmop/services/queuer/__init__.py b/mopidy_spotmop/services/queuer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mopidy_spotmop/services/queuer/queuer.py b/mopidy_spotmop/services/queuer/queuer.py new file mode 100644 index 0000000..bf03dea --- /dev/null +++ b/mopidy_spotmop/services/queuer/queuer.py @@ -0,0 +1,36 @@ +import os +import tornado.web +import base64 +import urllib, urllib2, json +from tornado.escape import json_encode, json_decode + +from tornado.escape import json_encode +import subprocess + +state = { + 'mode': 'normal' +} + +class QueuerRequestHandler(tornado.web.RequestHandler): + + def set_default_headers(self): + self.set_header("Access-Control-Allow-Origin", "*") + + def initialize(self, core, config): + self.core = core + self.config = config + + ## get the current state + def get(self, action): + self.write(json_encode(state)) + + ## post to update the state + def post(self, mode): + state['mode'] = 'radio' + self.write(json_encode(state)) + + +def spotmop_queuer_factory(config, core): + return [ + ('/', QueuerRequestHandler, {'core': core, 'config': config}) + ] \ No newline at end of file diff --git a/mopidy_spotmop/static/app.js b/mopidy_spotmop/static/app.js index 1b8d115..11f6e47 100644 --- a/mopidy_spotmop/static/app.js +++ b/mopidy_spotmop/static/app.js @@ -30437,7 +30437,9 @@ angular.module('spotmop.browse.album', []) } // this is not strictly accurate, but the only way to get the actual album data is from the track object - $scope.album = response[0].album; + var sourceAlbum = response[0].album; + delete sourceAlbum.images; + $scope.album = sourceAlbum; $scope.album.artists = []; $scope.album.totalTracks = $scope.album.num_tracks; $scope.tracklist = { type: 'localtrack', tracks: response }; @@ -30488,7 +30490,7 @@ angular.module('spotmop.browse.album', []) // we got images from mopidy! if( albumImages.length > 0 ){ - + $scope.album.images = albumImages; // no mopidy artwork, so get album artwork from LastFM @@ -30622,17 +30624,21 @@ angular.module('spotmop.browse.artist', []) }); } $scope.playArtistRadio = function(){ - - NotifyService.notify('Playing all top tracks'); + + NotifyService.notify('Starting artist radio (beta)'); // get the artist's top tracks - SpotifyService.getTopTracks( $stateParams.uri ) + SpotifyService.getRecommendations( 5, 0, $scope.artist.id ) .then( function( response ){ + var uris = []; for( var i = 0; i < response.tracks.length; i++ ){ uris.push( response.tracks[i].uri ); } - MopidyService.playTrack( uris, 0 ); + MopidyService.clearCurrentTrackList() + .then( function(){ + MopidyService.playTrack( uris, 0 ); + }); }); } @@ -32452,8 +32458,11 @@ angular.module('spotmop.directives', []) // when we're told to watch, we watch for changes in the url param (ie sidebar bg) if( $element.attr('watch') ){ $scope.$watch('url', function(newValue, oldValue) { - if (newValue) + if( newValue ){ loadImage(); + }else{ + $element.attr('style', 'background-image: none;'); + } }, true); } @@ -32462,7 +32471,7 @@ angular.module('spotmop.directives', []) // run the preloader function loadImage(){ - + var fullUrl = ''; /* RE-BUILD THIS TO USE PYTHON/TORNADO BACKEND @@ -32701,38 +32710,6 @@ angular.module('spotmop.directives', []) }; }) -// get the appropriate sized image -// DEPRECIATED -.filter('thumbnailImage', function(){ - return function( images ){ - - // what if there are no images? then nada - if( images.length <= 0 ) - return false; - - // loop all the images - for( var i = 0; i < images.length; i++){ - var image = images[i]; - - // this is our preferred size - if( image.height >= 200 && image.height <= 300 ){ - return image.url; - - // let's take it a notch up then - }else if( image.height > 300 && image.height <= 500 ){ - return image.url; - - // nope? let's take it a notch down then - }else if( image.height >= 150 && image.height < 200 ){ - return image.url; - } - }; - - // no thumbnail that suits? just get the first (and highest res) one then - return images[0].url; - } -}) - // standardize our images into a large/medium/small structure for template usage .filter('sizedImages', ['SettingsService', function( SettingsService ){ return function( images ){ @@ -32753,6 +32730,7 @@ angular.module('spotmop.directives', []) var baseUrl = 'http://'+ SettingsService.getSetting('mopidyhost', window.location.hostname); baseUrl += ':'+ SettingsService.getSetting('mopidyport', '6680') image.url = baseUrl +'/spotmop'+ image.uri; + delete image.uri; if( image.height ){ if( image.height >= 650 ){ @@ -33364,7 +33342,7 @@ angular.module('spotmop.common.tracklist', []) // build an array of track uris (and subtract the first one, as we play him immediately) var selectedTracksUris = []; - for( var i = 1; i < selectedTracks.length; i++ ){ + for( var i = 0; i < selectedTracks.length; i++ ){ selectedTracksUris.push( selectedTracks[i].uri ); }; @@ -33374,15 +33352,7 @@ angular.module('spotmop.common.tracklist', []) NotifyService.notify( message ); - // play the first track immediately - MopidyService.playTrack( [ firstSelectedTrack.uri ], 0 ).then( function(){ - - // more tracks to add - if( selectedTracksUris.length > 0 ){ - // add the following tracks to the tracklist - MopidyService.addToTrackList( selectedTracksUris, 1 ); - } - }); + MopidyService.playTrack( selectedTracksUris, 0 ); } } @@ -34324,7 +34294,7 @@ angular.module('spotmop.local', []) // chat with Mopidy and get the images for all these URIs MopidyService.getImages( uris ) .then( function(response){ - + // loop all the response uris for( var key in response ){ @@ -34336,7 +34306,7 @@ angular.module('spotmop.local', []) var index = $scope.allAlbums.indexOf( albumByUri[0] ); // update the album's images - $scope.allAlbums[index].images = $filter('sizedImages')( response[key] ); + $scope.allAlbums[index].images = response[key]; } } }); @@ -34710,7 +34680,6 @@ angular.module('spotmop.services.player', []) } // listen for current track changes - // TODO: Move this into the MopidyService for sanity $rootScope.$on('mopidy:event:trackPlaybackStarted', function( event, tlTrack ){ // only if our new tlTrack differs from our current one @@ -35282,6 +35251,7 @@ angular.module('spotmop.search', []) case 'album' : SpotifyService.getSearchResults( 'album', query, 50 ) .then( function(response){ + console.log( response ); $scope.albums = response.albums; if( response.albums.next ) nextOffset = response.albums.offset + response.albums.limit; @@ -35319,28 +35289,15 @@ angular.module('spotmop.search', []) break; default : - SpotifyService.getSearchResults( 'track', query, 50 ) + SpotifyService.getSearchResults( 'track,album,artist,playlist', query, 50 ) .then( function(response){ + $scope.albums = response.albums; + $scope.artists = response.artists; + $scope.playlists = response.playlists; $scope.tracklist = response.tracks; $scope.tracklist.type = 'track'; $scope.tracklist.tracks = response.tracks.items; }); - - SpotifyService.getSearchResults( 'album', query, 50 ) - .then( function(response){ - $scope.albums = response.albums; - }); - - SpotifyService.getSearchResults( 'artist', query, 50 ) - .then( function(response){ - $scope.artists = response.artists; - }); - - SpotifyService.getSearchResults( 'playlist', query, 50 ) - .then( function(response){ - $scope.playlists = response.playlists; - - }); break; } } @@ -35908,10 +35865,7 @@ angular.module('spotmop.services.mopidy', [ var args = Array.prototype.slice.call(arguments); var self = thisObj || this; - cfpLoadingBar.start(); - cfpLoadingBar.set(0.25); - executeFunctionByName(functionNameToWrap, self, args).then(function(data) { - cfpLoadingBar.complete(); + executeFunctionByName(functionNameToWrap, self, args).then(function(data){ deferred.resolve(data); }, function(err) { NotifyService.error( err ); @@ -36078,23 +36032,35 @@ angular.module('spotmop.services.mopidy', [ getState: function() { return wrapMopidyFunc("mopidy.playback.getState", this)(); }, - playTrack: function(newTracklistUris, trackToPlayIndex) { + playTrack: function(trackUris, trackToPlayIndex) { var self = this; - - // add the surrounding tracks (ie the whole tracklist in focus) - // we add this right to the top of the existing tracklist - return self.mopidy.tracklist.add({ uris: newTracklistUris, at_position: 0 }) - .then( function(){ - - // get the new tracklist - return self.mopidy.tracklist.getTlTracks() - .then(function(tlTracks) { - - // save tracklist for later - self.currentTlTracks = tlTracks; - - return self.mopidy.playback.play({ tl_track: tlTracks[trackToPlayIndex] }); - }, consoleError ); + + cfpLoadingBar.start(); + cfpLoadingBar.set(0.25); + + // add the first track immediately + return self.mopidy.tracklist.add({ uris: [ trackUris.shift() ], at_position: 0 }) + + // then play it + .then( function( response ){ + + // make sure we added the track successfully + // this handles failed adds due to geo-blocked spotify and typos in uris, etc + var playTrack = null; + if( response.length > 0 ){ + playTrack = { tlid: response[0].tlid }; + } + + return self.mopidy.playback.play() + + // now add all the remaining tracks + // note the use of .shift() previously altered the array + .then( function(){ + return self.mopidy.tracklist.add({ uris: trackUris, at_position: 1 }) + .then( function(){ + cfpLoadingBar.complete(); + }); + }, consoleError); }, consoleError); }, playTlTrack: function( tlTrack ){ @@ -37905,27 +37871,32 @@ angular.module('spotmop.services.spotify', []) .success(function( response ){ if( type == 'album' ){ - + var readyToResolve = false; var completeAlbums = []; - var batchesRequired = Math.ceil( response.albums.items.length / 20 ); + var batches = []; // batch our requests - Spotify only allows a max of 20 albums per request, d'oh! - for( var batchCounter = 1; batchCounter < batchesRequired; batchCounter++ ){ - - var batch = response.albums.items.splice(0,20); - var albumids = []; + while( response.albums.items.length ){ + batches.push( response.albums.items.splice(0,20) ); + } + + console.log( batches ); + + // now let's process our batches + for( var i = 0; i < batches.length; i++ ){ - // loop all our albums to build a list of all the album ids we need - for( var i = 0; i < 20; i++ ){ - albumids.push( batch[i].id ); + // loop our batch items to get just IDs + var albumids = []; + for( var j = 0; j < batches[i].length; j++ ){ + albumids.push( batches[i][j].id ); }; // go get the albums service.getAlbums( albumids ) .then( function(albums){ completeAlbums = completeAlbums.concat( albums.albums ); - if( batchCounter >= batchesRequired ){ + if( i >= batches.length - 1 ){ response.albums.items = completeAlbums; deferred.resolve( response ); } diff --git a/mopidy_spotmop/static/app.min.js b/mopidy_spotmop/static/app.min.js index e106981..a814383 100644 --- a/mopidy_spotmop/static/app.min.js +++ b/mopidy_spotmop/static/app.min.js @@ -1,6 +1,6 @@ /** * Mopidy-Spotmop - * Built 2016-07-04 + * Built 2016-07-22 **/ !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){function c(a){var b="length"in a&&a.length,c=_.type(a);return"function"===c||_.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}function d(a,b,c){if(_.isFunction(b))return _.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return _.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(ha.test(b))return _.filter(b,a,c);b=_.filter(b,a)}return _.grep(a,function(a){return U.call(b,a)>=0!==c})}function e(a,b){for(;(a=a[b])&&1!==a.nodeType;);return a}function f(a){var b=oa[a]={};return _.each(a.match(na)||[],function(a,c){b[c]=!0}),b}function g(){Z.removeEventListener("DOMContentLoaded",g,!1),a.removeEventListener("load",g,!1),_.ready()}function h(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=_.expando+h.uid++}function i(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(ua,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:ta.test(c)?_.parseJSON(c):c}catch(e){}sa.set(a,b,c)}else c=void 0;return c}function j(){return!0}function k(){return!1}function l(){try{return Z.activeElement}catch(a){}}function m(a,b){return _.nodeName(a,"table")&&_.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function n(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function o(a){var b=Ka.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function p(a,b){for(var c=0,d=a.length;d>c;c++)ra.set(a[c],"globalEval",!b||ra.get(b[c],"globalEval"))}function q(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(ra.hasData(a)&&(f=ra.access(a),g=ra.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)_.event.add(b,e,j[e][c])}sa.hasData(a)&&(h=sa.access(a),i=_.extend({},h),sa.set(b,i))}}function r(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&_.nodeName(a,b)?_.merge([a],c):c}function s(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ya.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}function t(b,c){var d,e=_(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:_.css(e[0],"display");return e.detach(),f}function u(a){var b=Z,c=Oa[a];return c||(c=t(a,b),"none"!==c&&c||(Na=(Na||_("');$(body).append(b),"undefined"==typeof c.spotify&&(c.spotify={}),"undefined"==typeof c.spotify.AccessToken&&(c.spotify.AccessToken=null),"undefined"==typeof c.spotify.RefreshToken&&(c.spotify.RefreshToken=null),"undefined"==typeof c.spotify.AuthorizationCode&&(c.spotify.AuthorizationCode=null),"undefined"==typeof c.spotify.AccessTokenExpiry&&(c.spotify.AccessTokenExpiry=null),window.addEventListener("message",function(b){if("http://jamesbarnsley.co.nz"!==b.origin)return!1;var d=JSON.parse(b.data);console.info("Spotify authorization successful"),c.spotify.AuthorizationCode=d.authorization_code,c.spotify.AccessToken=d.access_token,c.spotify.RefreshToken=d.refresh_token,a.spotifyOnline=!0,this.authenticationMethod="client",l.getMe().then(function(b){j.setSetting("spotifyuser",b),a.$broadcast("spotmop:spotify:authenticationChanged",this.authenticationMethod)})},!1),this.isAuthorized()?(a.spotifyAuthorized=!0,this.authenticationMethod="client"):(j.setSetting("spotifyuser",!1),a.spotifyAuthorized=!1,this.authenticationMethod="server"),a.$broadcast("spotmop:spotify:online")},logout:function(){c.spotify={},this.authenticationMethod="server",this.refreshToken(),a.$broadcast("spotmop:spotify:authenticationChanged",this.authenticationMethod)},authorize:function(){var a=$(document).find("#authorization-frame");a.attr("src","http://jamesbarnsley.co.nz/spotmop.php?action=authorize&app="+location.protocol+"//"+window.location.host)},isAuthorized:function(){return c.spotify.AuthorizationCode&&c.spotify.RefreshToken?!0:!1},refreshToken:function(){var b=h.defer(),e="";if("client"==this.authenticationMethod)e="http://jamesbarnsley.co.nz/spotmop.php?action=refresh&refresh_token="+c.spotify.RefreshToken;else{if("server"!=this.authenticationMethod)return!1;var f=j.getSetting("mopidyhost",window.location.hostname),g=j.getSetting("mopidyport","6680");e="http://"+f+":"+g+"/spotmop/auth"}return d({method:"GET",url:e,dataType:"json",async:!1,timeout:1e4}).success(function(d){"undefined"!=typeof d.error?(k.error("Spotify authorization error: "+d.error_description),a.spotifyOnline=!1,b.reject(d.error.message)):(c.spotify.AccessToken=d.access_token,c.spotify.AccessTokenExpiry=(new Date).getTime()+36e5,a.spotifyOnline=!0,b.resolve(d))}),b.promise},serviceUnavailable:function(){k.error("Request failed. Spotify API may be temporarily unavailable.")},getFromUri:function(a,b){var c=b.split(":");return"userid"==a&&"user"==c[1]?c[2]:"playlistid"==a&&"playlist"==c[3]?c[4]:"artistid"==a&&"artist"==c[1]?c[2]:"albumid"==a&&"album"==c[1]?c[2]:"trackid"==a&&"track"==c[1]?c[2]:null},uriType:function(a){var b=a.split(":");return"spotify"==b[0]&&"artist"==b[1]?"artist":"spotify"==b[0]&&"album"==b[1]?"album":"spotify"==b[0]&&"user"==b[1]&&"playlist"==b[3]?"playlist":null},getUrl:function(a){var b=h.defer();return d({method:"GET",url:a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getMe:function(){var a=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(b){a.resolve(b)}).error(function(b){k.error(b.error.message),a.reject(b.error.message)}),a.promise):(a.reject(),a.promise)},getUser:function(a){var b=this.getFromUri("userid",a),c=h.defer();return d({method:"GET",url:m+"users/"+b}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},isFollowing:function(a,b){var e=this.getFromUri(a+"id",b),f=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/following/contains?type="+a+"&ids="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},getTrack:function(a){var b=this.getFromUri("trackid",a),c=h.defer();return d({method:"GET",url:m+"tracks/"+b}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getMyTracks:function(a){var b=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/tracks/?limit=50",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise):(b.reject(),b.promise)},addTracksToLibrary:function(a){var b=h.defer();if(!this.isAuthorized())return b.reject(),b.promise;var e=i.get("$http");return e.remove(m+"me/tracks/?limit=50"),d({method:"PUT",url:m+"me/tracks",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},addAlbumsToLibrary:function(a){if(!this.isAuthorized())return e.reject(),e.promise;var b=i.get("$http");b.remove(m+"me/albums?limit=40&offset=0");var e=h.defer();return"array"!=typeof a&&(a=[a]),d({method:"PUT",url:m+"me/albums",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},removeAlbumsFromLibrary:function(a){if(!this.isAuthorized())return b.reject(),b.promise;var b=h.defer();return"array"!=typeof a&&(a=[a]),d({method:"DELETE",url:m+"me/albums",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},deleteTracksFromLibrary:function(a){var b=h.defer();if(!this.isAuthorized())return b.reject(),b.promise;var e=i.get("$http");return e.remove(m+"me/tracks/?limit=50"),d({method:"DELETE",url:m+"me/tracks",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getMyArtists:function(a){var b=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/following?type=artist",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise):(b.reject(),b.promise)},getMyAlbums:function(a,b,e){var f=h.defer();return this.isAuthorized()?("undefined"!=typeof b&&b||(b=20),"undefined"==typeof e&&(e=0),d({cache:!0,method:"GET",url:m+"me/albums?limit="+b+"&offset="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},isFollowingArtist:function(a,b){var e=this.getFromUri("artistid",a),f=h.defer();return this.isAuthorized()?(d({cache:!1,method:"GET",url:m+"me/following/contains?type=artist&ids="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},followArtist:function(a){var b=this.getFromUri("artistid",a),e=h.defer();return this.isAuthorized()?(d({method:"PUT",cache:!1,url:m+"me/following?type=artist&ids="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise):(e.reject(),e.promise)},unfollowArtist:function(a){var b=this.getFromUri("artistid",a),e=h.defer();return this.isAuthorized()?(d({method:"DELETE",cache:!1,url:m+"me/following?type=artist&ids="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise):(e.reject(),e.promise)},getPlaylists:function(a,b){"undefined"==typeof b&&(b=40);var e=h.defer();return d({cache:!1,method:"GET",url:m+"users/"+a+"/playlists?limit="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},getPlaylist:function(a){var b=this.getFromUri("userid",a),e=this.getFromUri("playlistid",a),f=h.defer();return d({cache:!0,method:"GET",url:m+"users/"+b+"/playlists/"+e+"?market="+n,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise},isFollowingPlaylist:function(a,b){var e=this.getFromUri("userid",a),f=this.getFromUri("playlistid",a),g=h.defer();return this.isAuthorized()?(d({cache:!0,method:"GET",url:m+"users/"+e+"/playlists/"+f+"/followers/contains?ids="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise):(g.reject(),g.promise)},followPlaylist:function(a){var b=this.getFromUri("userid",a),e=this.getFromUri("playlistid",a),f=h.defer();return this.isAuthorized()?(d({method:"PUT",url:m+"users/"+b+"/playlists/"+e+"/followers",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},unfollowPlaylist:function(a){var b=this.getFromUri("userid",a),e=this.getFromUri("playlistid",a),f=h.defer();return this.isAuthorized()?(d({method:"DELETE",url:m+"users/"+b+"/playlists/"+e+"/followers",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},featuredPlaylists:function(a){"undefined"==typeof a&&(a=40);var b=g("date")(new Date,"yyyy-MM-ddTHH:mm:ss"),e=j.getSetting("countrycode","NZ"),f=h.defer();return d({cache:!0,method:"GET",url:m+"browse/featured-playlists?timestamp="+b+"&country="+e+"&limit="+a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise},addTracksToPlaylist:function(a,b){var e=this.getFromUri("userid",a),f=this.getFromUri("playlistid",a),g=h.defer();return this.isAuthorized()?(d({method:"POST",url:m+"users/"+e+"/playlists/"+f+"/tracks",dataType:"json",data:JSON.stringify({uris:b}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise):(g.reject(),g.promise)},movePlaylistTracks:function(a,b,e,f){if(!this.isAuthorized())return l.reject(),l.promise;var g=this.getFromUri("userid",a),i=this.getFromUri("playlistid",a);if(g!=j.getSetting("spotifyuser",{id:null}).id)return!1;var l=h.defer();return d({method:"PUT",url:m+"users/"+g+"/playlists/"+i+"/tracks",dataType:"json",data:JSON.stringify({range_start:b,range_length:e,insert_before:f}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){l.resolve(a)}).error(function(a){k.error(a.error.message),l.reject(a.error.message)}),l.promise},deleteTracksFromPlaylist:function(a,b,e){var f=this.getFromUri("userid",a),g=this.getFromUri("playlistid",a),i=h.defer();return d({method:"DELETE",url:m+"users/"+f+"/playlists/"+g+"/tracks",dataType:"json",data:JSON.stringify({snapshot_id:b,positions:e}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){i.resolve(a)}).error(function(a){k.error(a.error.message),i.reject(a.error.message)}),i.promise},createPlaylist:function(a,b){var e=h.defer();return this.isAuthorized()?(d({method:"POST",url:m+"users/"+a+"/playlists/",dataType:"json",data:b,contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise):(e.reject(),e.promise)},updatePlaylist:function(a,b){var e=this.getFromUri("userid",a),f=this.getFromUri("playlistid",a),g=h.defer();return this.isAuthorized()?(d({method:"PUT",url:m+"users/"+e+"/playlists/"+f,dataType:"json",data:b,contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise):(g.reject(),g.promise)},newReleases:function(a,b){"undefined"!=typeof a&&a||(a=40),"undefined"==typeof b&&(b=0);var e=h.defer();return d({cache:!0,method:"GET",url:m+"browse/new-releases?country="+n+"&limit="+a+"&offset="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){for(var b=[],c=Math.ceil(a.albums.items.length/20),d=1;c>d;d++){for(var f=a.albums.items.splice(0,20),g=[],h=0;20>h;h++)g.push(f[h].id);l.getAlbums(g).then(function(f){b=b.concat(f.albums),d>=c&&(a.albums.items=b,e.resolve(a))})}}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},browseCategories:function(a){"undefined"==typeof a&&(a=40);var b=h.defer();return d({cache:!0,method:"GET",url:m+"browse/categories?limit="+a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getCategory:function(a){var b=h.defer();return d({cache:!0,method:"GET",url:m+"browse/categories/"+a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getCategoryPlaylists:function(a,b){"undefined"==typeof b&&(b=40);var e=h.defer();return d({cache:!0,method:"GET",url:m+"browse/categories/"+a+"/playlists?limit="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},getMyFavorites:function(a,b,e,f){if("undefined"==typeof b||!b)var b=25;if("undefined"==typeof e||!e)var e=0;if("undefined"==typeof f||!f)var f="long_term";var g=h.defer();return d({cache:!0,method:"GET",url:m+"me/top/"+a+"?limit="+b+"&offset="+e+"&time_range="+f,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise},getRecommendations:function(a,b,e,f,g){var i=m+"recommendations/?";"undefined"!=typeof a&&a&&(i+="limit="+a),"undefined"!=typeof b&&b&&(i+="&offset="+b),"undefined"!=typeof e&&e&&(i+="&seed_artists="+e),"undefined"!=typeof f&&f&&(i+="&seed_albums="+f),"undefined"!=typeof g&&g&&(i+="&seed_tracks="+g);var j=h.defer();return d({cache:!0,method:"GET",url:i,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){j.resolve(a)}).error(function(a){k.error(a.error.message),j.reject(a.error.message)}),j.promise},getArtist:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getArtists:function(a){var b=this,c="";angular.forEach(a,function(a){""!=c&&(c+=","),c+=b.getFromUri("artistid",a)});var e=h.defer();return d({method:"GET",url:m+"artists/?ids="+c}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},getTopTracks:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b+"/top-tracks?country="+n}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getRelatedArtists:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b+"/related-artists"}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getAlbum:function(a){var b=h.defer(),c=this.getFromUri("albumid",a);return d({method:"GET",url:m+"albums/"+c}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getAlbums:function(a){for(var b=h.defer(),c="",e=0;e0&&(c+=","),c+=a[e];return d({cache:!0,method:"GET",url:m+"albums?ids="+c+"&market="+n}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getArtistAlbums:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b+"/albums?album_type=album,single&market="+n}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},isAlbumInLibrary:function(a){for(var b=h.defer(),e="",f=0;f0&&(e+=","),e+=a[f];return this.isAuthorized()?(d({method:"GET",url:m+"me/albums/contains?ids="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise):(b.reject(),b.promise)},getSearchResults:function(a,b,e,f){"undefined"==typeof e&&(e=10),"undefined"==typeof f&&(f=0);var g=h.defer();return d({cache:!0,method:"GET",url:m+"search?q="+b+"&type="+a+"&country="+n+"&limit="+e+"&offset="+f,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(b){if("album"==a)for(var c=[],d=Math.ceil(b.albums.items.length/20),e=1;d>e;e++){for(var f=b.albums.items.splice(0,20),h=[],i=0;20>i;i++)h.push(f[i].id);l.getAlbums(h).then(function(a){c=c.concat(a.albums),e>=d&&(b.albums.items=c,g.resolve(b))})}else g.resolve(b)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise}},m="https://api.spotify.com/v1/",n=j.getSetting("spotifycountry","NZ");j.getSetting("spotifylocale","en_NZ");return l}]).factory("SpotifyServiceIntercepter",["$q","$rootScope","$injector","$localStorage",function(a,b,c,d){"use strict";function e(a,b,d){function e(a){b.resolve(a)}function f(a){b.reject(a)}var g=c.get("$http");a.headers={Authorization:"Bearer "+d},g(a).then(e,f)}var f=0,g={responseError:function(b){if(b.config.url.search("https://api.spotify.com/")>=0&&3>f){if(401==b.status){f++;var d=a.defer();return c.get("SpotifyService").refreshToken().then(function(a){return"undefined"!=typeof a.error?b:(f--,void e(b.config,d,a.access_token))}),d.promise}if(0==b.status){var d=a.defer();return c.get("SpotifyService").serviceUnavailable(),d.promise}}return b}};return g}]),angular.module("spotmop.settings",[]).config(["$stateProvider",function(a){a.state("settings",{url:"/settings",templateUrl:"app/settings/template.html"}).state("testing",{url:"/testing",templateUrl:"app/settings/testing.template.html"})}]).controller("SettingsController",["$scope","$http","$rootScope","$timeout","MopidyService","SpotifyService","SettingsService","NotifyService","PusherService",function(a,b,c,d,e,f,g,h,i){a.version,a.settings=g.getSettings(),a.currentSubpage="mopidy",a.subpageNavigate=function(b){a.currentSubpage=b},a.authorizeSpotify=function(){f.authorize()},a.refreshSpotifyToken=function(){h.notify("Refreshing token"),f.refreshToken().then(function(){})},a.spotifyLogout=function(){f.logout()},a.upgradeCheck=function(){h.notify("Checking for updates"),g.upgradeCheck().then(function(a){g.setSetting("version",a,"latest"),g.getSetting("version",0,"installed")0&&(c=b.spotifyuser.images[0].url),k.browserNotify(b.title,b.body,c),n.trackEvent("Pusher","Notification received",b.body)}),h.start(),g.start(),b.shiftKeyHeld=!1,b.ctrlKeyHeld=!1,$("body").bind("keydown",function(a){if(16===a.which?b.shiftKeyHeld=!0:17===a.which&&(b.ctrlKeyHeld=!0),!$(document).find(":focus").is(":input")&&j.getSetting("keyboardShortcutsEnabled",!1)){var c=new Array(46,32,13,37,38,39,40,27);$.inArray(a.which,c)>-1&&a.preventDefault()}}).bind("keyup",function(a){!$(document).find(":focus").is(":input")&&j.getSetting("keyboardShortcutsEnabled",!1)&&(46===a.which&&b.$broadcast("spotmop:keyboardShortcut:delete"),32===a.which&&b.$broadcast("spotmop:keyboardShortcut:space"),13===a.which&&b.$broadcast("spotmop:keyboardShortcut:enter"),37===a.which&&b.$broadcast("spotmop:keyboardShortcut:left"),38===a.which&&b.$broadcast("spotmop:keyboardShortcut:up"),39===a.which&&b.$broadcast("spotmop:keyboardShortcut:right"),40===a.which&&b.$broadcast("spotmop:keyboardShortcut:down"),27===a.which&&(b.$broadcast("spotmop:keyboardShortcut:esc"),dragging&&(dragging=!1,$(document).find(".drag-tracer").hide()))),16===a.which&&(b.shiftKeyHeld=!1),17===a.which&&(b.ctrlKeyHeld=!1)})}]),angular.module("spotmop.browse.album",[]).config(["$stateProvider",function(a){a.state("browse.album",{url:"/album/:uri",templateUrl:"app/browse/album/template.html",controller:"AlbumController"})}]).controller("AlbumController",["$scope","$rootScope","$stateParams","$filter","$state","MopidyService","SpotifyService","NotifyService","LastfmService",function(a,b,c,d,e,f,g,h,i){function j(){g.getAlbum(m).then(function(b){a.album=b,a.album.totalTracks=b.tracks.total,a.album.images=b.images,a.tracklist=b.tracks,a.tracklist.type="track",a.tracklist.tracks=b.tracks.items,angular.forEach(a.tracklist.tracks,function(b){b.album=a.album});var d=[];angular.forEach(b.artists,function(a){d.push(a.uri)}),g.getArtists(d).then(function(b){if(a.album.artists=[],b.artists)for(var c=0;c0)a.album.images=c;else if("undefined"!=typeof a.album.musicbrainz_id)i.albumInfoByMbid(a.album.musicbrainz_id).then(function(b){"undefined"!=typeof b.album&&(a.album.images=b.album.image)});else{var e=d[Object.keys(d)[0]];i.albumInfo(e.name.trim(),a.album.name.trim()).then(function(b){"undefined"!=typeof b.album&&(a.album.images=b.album.image)})}})})}function l(b){return"undefined"==typeof b?!1:(n=!0,void g.getUrl(b).then(function(b){a.tracklist.tracks=a.tracklist.tracks.concat(b.items),a.tracklist.next=b.next,n=!1}))}a.album={},a.tracklist={tracks:[]};var m=c.uri;m=m=m.replace("|","/"),a.origin=d("assetOrigin")(m),a.convertedDate=function(){if("undefined"!=typeof a.album.release_date){if(a.mediumScreen())return d("date")(a.album.release_date,"yyyy");if("day"==a.album.release_date_precision)return d("date")(a.album.release_date,"MMMM d, yyyy");if("month"==a.album.release_date_precision)return d("date")(a.album.release_date,"MMMM yyyy");if("year"==a.album.release_date_precision)return a.album.release_date}else if("undefined"!=typeof a.album.date)return a.album.date;return null},a.totalTime=function(){var b=0;return"undefined"!=typeof a.tracklist.tracks&&angular.forEach(a.tracklist.tracks,function(a){"undefined"!=typeof a.duration_ms?b+=a.duration_ms:"undefined"!=typeof a.length&&(b+=a.length)}),Math.round(b/1e5)},a.playAlbum=function(){f.playStream(m)},"spotify"==a.origin?(a.addToLibrary=function(){g.addAlbumsToLibrary(a.album.id).then(function(){a.isInLibrary=!0})},a.removeFromLibrary=function(){g.removeAlbumsFromLibrary(a.album.id).then(function(){a.isInLibrary=!1})},j(),a.$on("spotmop:loadMore",function(){!n&&"undefined"!=typeof a.tracklist.next&&a.tracklist.next&&l(a.tracklist.next)})):a.mopidyOnline?k():a.$on("mopidy:state:online",function(){k()});var n=!1}]),angular.module("spotmop.browse.artist",[]).config(["$stateProvider",function(a){a.state("browse.artist",{url:"/artist/:uri","abstract":!0,templateUrl:"app/browse/artist/template.html",controller:["$scope","$state",function(a,b){"browse.artist"===b.current.name&&b.go("browse.artist.overview")}]}).state("browse.artist.overview",{url:"",templateUrl:"app/browse/artist/overview.template.html",controller:"ArtistOverviewController"}).state("browse.artist.related",{url:"/related",templateUrl:"app/browse/artist/related.template.html",controller:"RelatedArtistsController"}).state("browse.artist.biography",{url:"/biography",templateUrl:"app/browse/artist/biography.template.html",controller:"ArtistBiographyController"}).state("browse.artistalbum",{url:"/artist/:artisturi/:uri",templateUrl:"app/browse/album/template.html",controller:"AlbumController"})}]).controller("ArtistController",["$scope","$rootScope","$timeout","$interval","$stateParams","$sce","$filter","SpotifyService","SettingsService","MopidyService","NotifyService","LastfmService",function(a,b,c,d,e,f,g,h,i,j,k,l){function m(){j.getArtist(n).then(function(b){return b.length<=0?void k.error("Could not load artist: "+n):(a.artist=b[0].artists[0],a.artist.type="localartist","undefined"!=typeof a.artist.musicbrainz_id?l.artistInfoByMbid(a.artist.musicbrainz_id).then(function(b){a.artist.images=b.artist.image,a.artist.stats=b.artist.stats}):l.artistInfo(a.artist.name).then(function(b){a.artist.images=b.artist.image,a.artist.stats=b.artist.stats}),a.tracklist.type="localtrack",void(a.tracklist.tracks=g("limitTo")(b,10)))}),j.getLibraryItems(n).then(function(b){a.albums.items=g("filter")(b,{type:"directory"}&&{type:"album"});for(var c=0;c-1&&(a.albums.items[c].uri=d(a.albums.items[c].uri));var e=function(b){return function(c){"undefined"!=typeof c.album&&(a.albums.items[b].images=c.album.image)}}(c);a.albums.items[c].mbid?l.albumInfoByMbid(a.albums.items[c].mbid).then(e):l.albumInfo(a.albums.items[c].artist.name.trim(),a.albums.items[c].name.trim()).then(e)}})}a.artist={},a.tracklist={type:"track"},a.albums={items:[]},a.relatedArtists={};var n=e.uri;n=n=n.replace("|","/"),a.origin=g("assetOrigin")(n),"spotify"==a.origin?(a.followArtist=function(){h.followArtist(e.uri).then(function(b){a.following=!0})},a.unfollowArtist=function(){h.unfollowArtist(e.uri).then(function(b){a.following=!1})},a.playArtistRadio=function(){k.notify("Starting artist radio (beta)"),h.getRecommendations(5,0,a.artist.id).then(function(a){for(var b=[],c=0;c=4&&9.3>a?"commute":a>=9.3&&11>a?"morning":a>=11&&13.5>a?"midday":a>=13.5&&17>a?"afternoon":a>=17&&19>a?"evening":a>=19&&21>a?"dinner":a>=21&&23>a||a>=0&&4>a?"late":void 0},d.featuredPlaylists(50).then(function(b){a.message=b.message,a.playlists=b.playlists.items})}]),angular.module("spotmop.browse.genre",[]).config(["$stateProvider",function(a){a.state("browse.genre",{url:"/genre",templateUrl:"app/browse/genre/template.html",controller:"GenreController"}).state("browse.genrecategory",{url:"/genre/:categoryid",templateUrl:"app/browse/genre/category.template.html",controller:"GenreCategoryController"}).state("browse.categoryplaylist",{url:"/genre/:categoryid/:uri",templateUrl:"app/browse/playlist/template.html",controller:"PlaylistController"})}]).controller("GenreController",["$scope","$rootScope","SpotifyService","NotifyService",function(a,b,c,d){function e(b){return"undefined"==typeof b?!1:(f=!0,void c.getUrl(b).then(function(b){a.categories.items=a.categories.items.concat(b.categories.items),a.categories.next=b.categories.next,f=!1}))}a.categories=[],c.browseCategories().then(function(b){a.categories=b.categories});var f=!1;a.$on("spotmop:loadMore",function(){!f&&"undefined"!=typeof a.categories.next&&a.categories.next&&e(a.categories.next)})}]).controller("GenreCategoryController",["$scope","$rootScope","SpotifyService","$stateParams",function(a,b,c,d){function e(b){return"undefined"==typeof b?!1:(f=!0,void c.getUrl(b).then(function(b){a.playlists.items=a.playlists.items.concat(b.playlists.items),a.playlists.next=b.playlists.next,f=!1}))}a.category={},a.playlists=[],c.getCategory(d.categoryid).then(function(b){a.category=b,c.getCategoryPlaylists(d.categoryid).then(function(b){a.playlists=b.playlists})});var f=!1;a.$on("spotmop:loadMore",function(){!f&&"undefined"!=typeof a.playlists.next&&a.playlists.next&&e(a.playlists.next)})}]),angular.module("spotmop.browse.new",[]).config(["$stateProvider",function(a){a.state("browse.new",{url:"/new",templateUrl:"app/browse/new/template.html",controller:"NewController"}).state("browse.newalbum",{url:"/new/:uri",templateUrl:"app/browse/album/template.html",controller:"AlbumController"})}]).controller("NewController",["$scope","$element","$rootScope","SpotifyService","MopidyService",function(a,b,c,d,e){function f(b){console.log("loading"),h=!0,d.newReleases(!1,b).then(function(b){a.albums.items=a.albums.items.concat(b.albums.items),g=b.albums.offset+b.albums.limit,h=!1})}a.albums=[],d.newReleases().then(function(b){a.albums=b.albums,a.checkForLazyLoading()});var g=50,h=!1;a.$on("spotmop:loadMore",function(){!h&&g&&f(g)})}]),angular.module("spotmop.browse.playlist",[]).config(["$stateProvider",function(a){a.state("browse.playlist",{url:"/playlist/:uri",templateUrl:"app/browse/playlist/template.html",controller:"PlaylistController"})}]).controller("PlaylistController",["$scope","$rootScope","$filter","$state","$stateParams","$sce","SpotifyService","MopidyService","SettingsService","DialogService","NotifyService",function(a,b,c,d,e,f,g,h,i,j,k){function l(){var b=d.params.uri,e=g.getFromUri("userid",b),f=i.getSetting("spotifyuser",{id:null}).id;if(e!=f)return k.error("Cannot modify to a playlist you don't own"),!1;var h=c("filter")(a.tracklist.tracks,{selected:!0}),j=[];angular.forEach(h,function(b,c){j.push(a.tracklist.tracks.indexOf(b)),b.transitioning=!0}),g.deleteTracksFromPlaylist(a.playlist.uri,a.playlist.snapshot_id,j).then(function(b){"undefined"!=typeof b.error?(k.error(b.error.message),angular.forEach(h,function(a,b){a.transitioning=!1})):(a.tracklist.tracks=c("nullOrUndefined")(a.tracklist.tracks,"selected"),a.playlist.snapshot_id=b.snapshot_id)})}function m(a){var b=[];return angular.forEach(a,function(a){var c=a.track;c.added_at=a.added_at,c.added_by=a.added_by,c.is_local=a.is_local,b.push(c)}),b}function n(b){return"undefined"==typeof b?!1:(o=!0,void g.getUrl(b).then(function(b){a.tracklist.tracks=a.tracklist.tracks.concat(m(b.items)),a.tracklist.next=b.next,o=!1}))}a.playlist={images:[]},a.tracklist={tracks:[],type:"track"},a.totalTime=0,a.following=!1,a.followPlaylist=function(){g.followPlaylist(e.uri).then(function(b){a.following=!0,k.notify("Following playlist"),a.updatePlaylists()})},a.unfollowPlaylist=function(){g.unfollowPlaylist(e.uri).then(function(b){a.following=!1,k.notify("Playlist removed"),a.updatePlaylists()})},a.recoverPlaylist=function(){g.followPlaylist(e.uri).then(function(b){a.following=!0,k.notify("Playlist recovered"),a.updatePlaylists()})},a.editPlaylist=function(){j.create("editPlaylist",a)},a.playPlaylist=function(){h.playStream(a.playlist.uri,a.tracklist.tracks.length)},a.totalTime=function(){var b=0;return a.tracklist.tracks.length>0&&angular.forEach(a.tracklist.tracks,function(a){"undefined"!=typeof a&&(b+=a.duration_ms)}),Math.round(b/1e5)},g.getPlaylist(e.uri).then(function(c){a.playlist=c,a.tracklist.next=c.tracks.next,a.tracklist.previous=c.tracks.previous,a.tracklist.offset=c.tracks.offset,a.tracklist.total=c.tracks.total,a.tracklist.tracks=m(c.tracks.items),a.playlist.description=f.trustAsHtml(a.playlist.description),b.spotifyAuthorized&&g.getUser(a.playlist.owner.uri).then(function(b){a.playlist.owner=b}),b.spotifyAuthorized&&g.isFollowingPlaylist(e.uri,i.getSetting("spotifyuser",{id:null}).id).then(function(b){a.following=$.parseJSON(b)}),"undefined"!=typeof e.categoryid&&g.getCategory(e.categoryid).then(function(b){a.category=b})}),a.$on("spotmop:playlist:reorder",function(b,c,e,f){var h=d.params.uri,j=g.getFromUri("userid",h),l=i.getSetting("spotifyuser",{id:null}).id;if(j!=l)k.error("Cannot edit a playlist you don't own");else{g.movePlaylistTracks(h,c,e,f);for(var m=[],n=0;e>n;n++)m.push(a.tracklist.tracks[c+n]);f>c&&(f-=e),m.reverse(),a.$apply(function(){a.tracklist.tracks.splice(c,e),angular.forEach(m,function(b){a.tracklist.tracks.splice(f,0,b)})})}}),a.$on("spotmop:playlist:deleteSelectedTracks",function(a){l()}),a.$on("spotmop:keyboardShortcut:delete",function(a){l()});var o=!1;a.$on("spotmop:loadMore",function(){!o&&"undefined"!=typeof a.tracklist.next&&a.tracklist.next&&n(a.tracklist.next)})}]),angular.module("spotmop.browse.user",[]).config(["$stateProvider",function(a){a.state("browse.user",{url:"/user/:uri",templateUrl:"app/browse/user/template.html",controller:"UserController"})}]).controller("UserController",["$scope","$rootScope","SpotifyService","$stateParams",function(a,b,c,d){function e(b){return"undefined"==typeof b?!1:(f=!0,void c.getUrl(b).then(function(b){a.playlists=a.playlists.concat(b.items),a.next=b.next,f=!1}))}a.user={},a.playlists=[],c.getUser(d.uri).then(function(b){a.user=b,c.getPlaylists(b.id).then(function(b){a.playlists=b.items,a.next=b.next,a.totalPlaylists=b.total})});var f=!1;a.$on("spotmop:loadMore",function(){!f&&"undefined"!=typeof a.next&&a.next&&e(a.next)})}]),angular.module("spotmop.common.contextmenu",[]).directive("contextmenu",function(){return{restrict:"E",replace:!0,templateUrl:"app/common/contextmenu/template.html",link:function(a,b,c){},controller:["$scope","$rootScope","$element","$timeout","NotifyService",function(a,b,c,d,e){$(document).on("click",function(a){if(!b.isTouchMode()&&1===a.which){var c=$(a.target);c.is("contextmenu")||(c=c.closest("contextmenu")),c.is("contextmenu")||b.$broadcast("spotmop:contextMenu:hide")}}),a.triggerEvent="",a.play=function(){b.$broadcast("spotmop:tracklist:playSelectedTracks"),c.fadeOut("fast"),a.isTouchMode()&&b.$broadcast("spotmop:tracklist:unselectAll")},a.enqueue=function(){b.$broadcast("spotmop:tracklist:enqueueSelectedTracks"),c.fadeOut("fast"),a.isTouchMode()&&b.$broadcast("spotmop:tracklist:unselectAll")},a.unqueue=function(){b.$broadcast("spotmop:tracklist:unqueueSelectedTracks"),c.fadeOut("fast"),a.isTouchMode()&&b.$broadcast("spotmop:tracklist:unselectAll")},a.playNext=function(){b.$broadcast("spotmop:tracklist:enqueueSelectedTracks",!0),c.fadeOut("fast"),a.isTouchMode()&&b.$broadcast("spotmop:tracklist:unselectAll")},a.addToPlaylist=function(){b.$broadcast("spotmop:tracklist:addSelectedTracksToPlaylist"),c.fadeOut("fast")},a.addToPlaylistByUri=function(a){b.$broadcast("spotmop:tracklist:addSelectedTracksToPlaylistByUri",a),c.fadeOut("fast")},a.removeFromPlaylist=function(){b.$broadcast("spotmop:playlist:deleteSelectedTracks"),c.fadeOut("fast")},a.addToLibrary=function(){b.$broadcast("spotmop:tracklist:addSelectedTracksToLibrary"),c.fadeOut("fast")},a.copyURIs=function(){b.$broadcast("spotmop:tracklist:copyURIsToClipboard"),c.fadeOut("fast")},a.copiedToClipboard=function(a){e.notify("Copied selected track URIs to clipboard"),c.fadeOut("fast")},a.selectAll=function(){b.$broadcast("spotmop:tracklist:selectAll")},a.unselectAll=function(){b.$broadcast("spotmop:tracklist:unselectAll"),c.fadeOut("fast")},a.$on("spotmop:contextMenu:show",function(e,f,g){return b.isTouchMode()?!1:void a.$apply(function(){a.context=g,a.triggerEvent="click",d(function(){var a=f.pageY-$(window).scrollTop(),b=f.pageX,d=c.outerWidth(),e=c.outerHeight();b+d>$(window).width()?(b-=d-10,c.addClass("hard-right")):b+d+150>$(window).width()?c.addClass("close-right"):c.removeClass("hard-right close-right"),a+e>$(window).height()?(a-=e,c.addClass("hard-bottom")):a+e+306>$(window).height()?c.addClass("close-bottom"):c.removeClass("hard-bottom close-bottom"),c.css({top:a,left:b+5}).show()})})}),a.$on("spotmop:touchContextMenu:show",function(b,d){c.show(),a.triggerEvent="touch",c.removeClass("hard-bottom close-bottom hard-right close-right"),c.css({top:"auto",left:0}),a.$apply(function(){a.context=d})}),a.$on("spotmop:contextMenu:hide",function(a){c.fadeOut("fast")})}]}}),angular.module("spotmop.directives",[]).config(["cfpLoadingBarProvider",function(a){a.latencyThreshold=250}]).directive("singleclick",function(){return function(a,b,c){b.bind("touchstart click",function(b){b.preventDefault(),b.stopPropagation(),a.$apply(c.singleclick)})}}).directive("candrag",["$rootScope","$filter","MopidyService","SpotifyService","NotifyService","PlayerService",function(a,b,c,d,e,f){return{restrict:"A",scope:{dragobj:"="},link:function(e,g,h){function i(c){a.dragging=!0;var d=!1;if(v.dragActive||(d=!0),v.dragActive=!0,$(document).find(".dropping").removeClass("dropping"),$(document).find(".dropping-within").removeClass("dropping-within"),d){$("body").addClass("dragging");var f="";if("album"==e.dragobj.type||"localalbum"==e.dragobj.type||"artist"==e.dragobj.type||"localartist"==e.dragobj.type||"playlist"==e.dragobj.type){var g=!1;if("undefined"!=typeof e.dragobj.images.small)var g=e.dragobj.images;else if(e.dragobj.images.length>0)var g=b("sizedImages")(e.dragobj.images);g&&(f='
'),f+='
'+e.dragobj.name+"
"}else if("track"==e.dragobj.type||"tltrack"==e.dragobj.type||"localtrack"==e.dragobj.type)for(var h=$(document).find(".track.selected"),i=0;ii;i++)f+='
'+h.eq(i).find(".title").html()+"
";u.html(f),u.show(),$.each($(document).find("#dropzones > .dropzone"),function(a,b){t($(b))?$(b).removeClass("disabled"):$(b).addClass("disabled")})}u.css({left:c.clientX,top:c.clientY});var j=s(c),k=t(j);if(k){j.addClass("dropping");var l=$(c.target);l.hasClass("track")||(l=l.closest(".track")),l.hasClass("track")&&l.addClass("dropping"),j.parent().closest(".droppable").addClass("dropping-within")}}function j(b){a.$broadcast("spotmop:contextMenu:hide"),u.fadeOut("medium"),$("body").removeClass("dragging"),$(document).find(".dropping").removeClass("dropping");var c=s(b),d=t(c);if(d)switch(c.attr("droptype")){case"queue":k();break;case"queuenext":var g=f.state().currentTracklistPosition();k(g);break;case"playlist":l(b);break;case"library":"track"==e.dragobj.type?n():"tltrack"==e.dragobj.type?n():"album"==e.dragobj.type?m():"artist"==e.dragobj.type?o():"playlist"==e.dragobj.type&&p();break;case"queuetracklist":q(b);break;case"playlisttracklist":r(b)}a.dragging=!1}function k(a){if("undefined"==typeof a)var a=null;switch(e.dragobj.type){case"album":c.addToTrackList([e.dragobj.uri],a);break;case"localalbum":c.addToTrackList([e.dragobj.uri],a);break;case"track":for(var b=[],d=$(document).find(".track.selected"),f=0;fg&&(e-=d.length),c.moveTlTracks(f,g,e)}function r(b){var c=$(b.target);c.hasClass("track")||(c=c.closest(".track"));var d=$(v.domobj).closest(".tracklist").find(".track.selected"),e=(c.closest(".tracklist").attr("playlisturi"),Number(c.parent().attr("data-index"))),f=Number(d.first().parent().attr("data-index")),g=Number(d.length);a.$broadcast("spotmop:playlist:reorder",f,g,e)}function s(a){var b=$(a.target);return b.hasClass("droppable")||(b=b.closest(".droppable")),b?b:!1}function t(a){var b=a.attr("dropaccept");return b?(b=JSON.parse(b),b.indexOf(e.dragobj.type)>=0?!0:!1):!1}var u=$(document).find(".drag-tracer"),v={threshold:30,dragStarted:!1,dragActive:!1,startX:!1,starY:!1};g.on("mousedown",function(a){v.dragStarted=!0,v.startX=a.clientX,v.startY=a.clientY,v.domobj=a.currentTarget,"undefined"!=typeof e.dragobj.__model__&&"undefined"==typeof e.dragobj.type&&(e.dragobj.type=e.dragobj.__model__.toLowerCase())}),$(document).on("mouseup",function(a){v.dragActive&&j(a),v.dragStarted=!1,v.dragActive=!1,v.startX=!1,v.startY=!1,v.domobj=!1}),$(document).on("mousemove",function(a){if(v.dragStarted){var b=v.startX-v.threshold,c=v.startX+v.threshold,d=v.startY-v.threshold,e=v.startY+v.threshold;(a.clientXc||a.clientYe)&&i(a)}})}}}]).directive("switch",["$rootScope","SettingsService",function(a,b){return{restrict:"E",scope:{name:"@",label:"@",value:"="},replace:!0,transclude:!0,controller:["$scope","$element","$attrs",function(b,c,d){c.bind("touchstart click",function(c){c.preventDefault(),c.stopPropagation(),b.$apply(function(){b.value=!b.value,a.$broadcast("spotmop:settings:changed",{name:b.name,value:b.value})})})}],template:''}}]).directive("artistlist",["$rootScope","SettingsService",function(a,b){return{restrict:"E",scope:{artists:"="},link:function(a,b,c){a.nolinks=c.hasOwnProperty("nolinks"),a.sentence=c.hasOwnProperty("sentence")},replace:!0,transclude:!0,templateUrl:"app/common/artistlist.template.html"}}]).directive("thumbnail",["$timeout","$http","$filter",function(a,b,c){return{restrict:"E",scope:{images:"=",size:"@",debugging:"@"},replace:!0,transclude:!0,link:function(a,d,e){function f(){if(!a.images)return!1;var e=c("sizedImages")(a.images);e=a.size?e[a.size]:e.small,e&&""!=e&&b({method:"GET",url:e,cache:!0}).success(function(){d.css("background-image","url("+e+")")})}f(),a.$watch("images",function(a,b){f()})},template:'
'}}]).directive("confirmationButton",function(){return{restrict:"E",controller:["$scope","$element",function(a,b){a.text="Button text",a.confirming=!1,a.text=a.defaultText,$(document).on("click",function(c){c.target==b[0]&&1==c.which?a.confirming?"function"==typeof a.$parent[a.onConfirmation]()&&a.$parent[a.onConfirmation]():(a.confirming=!0,a.text=a.confirmationText,a.$apply()):(a.confirming=!1,a.text=a.defaultText,a.$apply())})}],scope:{text:"@",extraClasses:"@",confirmationText:"@",defaultText:"@",onConfirmation:"@"},replace:!0,transclude:!0,template:''}}).directive("slider",["$timeout",function(a){return{restrict:"E",scope:{items:"="},link:function(b,c){function d(a){return"prev"==a&&0>=g?!1:"next"==a&&g>=h?!1:!0}function e(){var a=c.find(".item-container").children().first().height();c.css({height:a+"px"})}var f=c.find(".slides-content"),g=0,h=b.items.length/5-1;b.prev=function(){d("prev")&&(g--,f.animate({left:100*-g+"%"},120))},b.next=function(){d("next")&&(g++,f.animate({left:100*-g+"%"},120))},a(function(){e()},0),$(window).resize(function(){e()})},templateUrl:"app/common/slider.template.html"}}]).directive("textOverImage",function(){return{restrict:"A",link:function(a,b){a.$on("spotmop:detectBackgroundColor",function(a){BackgroundCheck.init({targets:$.merge($(b).parent(),$(document).find("#utilities")),images:b.closest(".intro").find(".image")}),BackgroundCheck.refresh()})}}}).directive("preloadedimage",["$rootScope","$timeout",function(a,b){return{restrict:"E",scope:{url:"@",useproxy:"@",detectbackground:"@",opacity:"@"},link:function(a,b,c){function d(){var c="";c+=a.url;var d=$('');d.load(function(){b.attr("style",'background-image: url("'+c+'");');var d=1;"undefined"!=typeof a.opacity&&(d=a.opacity),b.animate({opacity:d},200)})}b.attr("watch")&&a.$watch("url",function(a,c){a?d():b.attr("style","background-image: none;")},!0),d()},template:""}}]).directive("backgroundparallax",["$rootScope","$timeout","$interval","$http","$filter",function(a,b,c,d,e){return{restrict:"E",terminal:!0,scope:{images:"=",image:"@",opacity:"@"},link:function(a,b,d){function f(a){var c=b.outerWidth(),d=b.outerHeight();if((i.canvas.width!=c||i.canvas.height!=d)&&(i.canvas.width=c,i.canvas.height=d),a.widthc){var e=c/a.width;a.width=a.width*e,a.height=a.height*e}if(a.height'}}]).filter("splitstring",[function(){return function(a,b){var c=a.split(":");return c[b]}}]).filter("nullOrUndefined",[function(){return function(a,b){for(var c=[],d=0;d=b&&(b="0"+b);var c=Math.floor(a/6e4%60);return c+":"+b}}).filter("stripAccents",function(){return function(a){for(var b=[/[\300-\306]/g,/[\340-\346]/g,/[\310-\313]/g,/[\350-\353]/g,/[\314-\317]/g,/[\354-\357]/g,/[\322-\330]/g,/[\362-\370]/g,/[\331-\334]/g,/[\371-\374]/g,/[\321]/g,/[\361]/g,/[\307]/g,/[\347]/g],c=["A","a","E","e","I","i","O","o","U","u","N","n","C","c"],d=0;d=650?c.large=e.url:e.height>=250?c.medium=e.url:c.small=e.url),c.small||(c.small=e.url),c.medium||(c.medium=e.url),c.large||(c.large=e.url)}else if("undefined"!=typeof e.height)e.height&&(e.height>=650?c.large=e.url:e.height>=300?c.medium=e.url:c.small=e.url),c.small||(c.small=e.url),c.medium||(c.medium=e.url),c.large||(c.large=e.url);else if("undefined"!=typeof e["#text"]){if(e["#text"]&&e["#text"].length>0&&""!=e.size)switch(e.size){case"mega":c.large=e["#text"];break;case"extralarge":c.medium=e["#text"],c.large||(c.large=e["#text"]);break;case"large":c.small=e["#text"],c.medium||(c.medium=e["#text"]),c.large||(c.large=e["#text"]);break;case"medium":c.small=e["#text"],c.medium||(c.medium=e["#text"]),c.large||(c.large=e["#text"]);break;case"small":c.small=e["#text"],c.medium||(c.medium=e["#text"]),c.large||(c.large=e["#text"])}}else c.large=e,c.medium=e,c.small=e}return c}}]).filter("shuffle",function(){return function(a){ +var b,c,d;for(b=a.length-1;b>0;b--)c=Math.floor(Math.random()*(b+1)),d=a[b],a[b]=a[c],a[c]=d;return a}}).filter("assetOrigin",function(){return function(a){if("undefined"==typeof a)return!1;var b=a.split(":");return b.length<=0?!1:b[0]}}).filter("assetType",function(){return function(a){if("undefined"==typeof a)return!1;var b=a.split(":");return b.length<=1?!1:b[1]}}).filter("mbid",function(){return function(a){if("undefined"==typeof a)return!1;var b=a.indexOf(":mbid:")+6,c=a.length;return a.substr(b,c)}}),angular.module("spotmop.common.tracklist.service",[]).factory("TracklistService",["$rootScope",function(a){return{getSelectedTracks:function(){console.log("triggered getSelectedTracks")}}}]),angular.module("spotmop.common.track",[]).directive("track",function(){return{restrict:"E",templateUrl:"app/common/tracklist/track.template.html",controller:["$element","$scope","$rootScope","MopidyService","NotifyService",function(a,b,c,d,e){a.mouseup(function(a){1===a.which?(c.isTouchMode()||b.$emit("spotmop:contextMenu:hide"),$(a.target).is("a")||b.$parent.trackClicked(b)):3===a.which&&(b.track.selected||b.$parent.trackClicked(b),b.$emit("spotmop:contextMenu:show",a,"track"))}),a.dblclick(function(a){d.playTrack([b.track.uri],0)})}]}}).directive("tltrack",function(){return{restrict:"E",templateUrl:"app/common/tracklist/tltrack.template.html",link:function(a,b,c){},controller:["$element","$scope","$rootScope","MopidyService","PlayerService",function(a,b,c,d,e){b.state=e.state,b.isCurrentlyPlaying=function(){return b.track.tlid==b.state().currentTlTrack.tlid},b.sourceIconClasses=function(){if("undefined"==typeof b.track.track)return!1;var a=b.track.track.uri.split(":")[0],c="light";return b.isCurrentlyPlaying()&&("spotify"==a&&(c="green"),"local"==a&&(c="yellow"),"soundcloud"==a&&(c="red")),a+" "+c},a.mouseup(function(a){1===a.which?(c.isTouchMode()||b.$emit("spotmop:contextMenu:hide"),$(a.target).is("a")||b.$parent.trackClicked(b)):3===a.which&&(b.track.selected||b.$parent.trackClicked(b),b.$emit("spotmop:contextMenu:show",a,"tltrack"))}),a.dblclick(function(a){d.getCurrentTlTracks().then(function(a){$.each(a,function(a,c){return c.tlid==b.track.tlid?d.playTlTrack({tl_track:c}):void 0})})})}]}}).directive("localtrack",function(){return{restrict:"E",templateUrl:"app/common/tracklist/localtrack.template.html",link:function(a,b,c){},controller:["$element","$scope","$rootScope","MopidyService","PlayerService","NotifyService",function(a,b,c,d,e,f){b.state=e.state,a.mouseup(function(a){1===a.which?(c.isTouchMode()||b.$emit("spotmop:contextMenu:hide"),$(a.target).is("a")||b.$parent.trackClicked(b)):3===a.which&&(b.track.selected||b.$parent.trackClicked(b),b.$emit("spotmop:contextMenu:show",a,"localtrack"))}),a.dblclick(function(a){d.playTrack([b.track.uri],0)})}]}}),angular.module("spotmop.common.tracklist",[]).directive("tracklist",["$compile",function(a){return{restrict:"E",templateUrl:"app/common/tracklist/template.html",scope:{tracks:"=",type:"@",limit:"@"},link:function(a,b,c){},controller:["$element","$scope","$filter","$rootScope","$stateParams","MopidyService","SpotifyService","DialogService","NotifyService","SettingsService","PlayerService",function(a,b,c,d,e,f,g,h,i,j,k){function l(){var a=c("filter")(b.tracks,{selected:!0}),e=[];angular.forEach(a,function(a){"undefined"!=typeof a.track?e.push(a.track.uri):e.push(a.uri)}),d.selectedTrackURIs=e}function m(){if(d.tracklistInFocus===b.$id){var a=c("filter")(b.tracks,{selected:!0}),e=a[0];if("tltrack"==b.type)f.getCurrentTlTracks().then(function(a){$.each(a,function(a,b){return b.tlid==e.tlid?f.playTlTrack({tl_track:b}):void 0})});else{for(var g=[],h=0;h10&&(j+="... this could take some time"),i.notify(j),f.playTrack(g,0)}}}function n(){angular.forEach(b.tracks,function(a){a.selected=!1})}d.selectedTrackURIs=[],$(document).contextmenu(function(a){return $(a.target).closest(".tracklist").length>0?!1:void 0}),b.tracksWrapper=function(){return b.limit&&b.limit>0?c("limitTo")(b.tracks,parseInt(b.limit)):b.tracks},b.$on("spotmop:track:dragging",function(a){});var o=d.$on("spotmop:tracklist:focusChanged",function(a,c){d.tracklistInFocus=c,b.$id!=c&&n()});b.$on("$destroy",o),b.trackClicked=function(a){if(d.$broadcast("spotmop:tracklist:focusChanged",b.$id),!d.dragging){if(d.ctrlKeyHeld||d.isTouchMode()?a.track.selected?a.$apply(function(){a.track.selected=!1}):a.$apply(function(){a.track.selected=!0}):d.ctrlKeyHeld||(angular.forEach(b.tracks,function(a){a.selected=!1}),a.$apply(function(){a.track.selected=!0})),d.shiftKeyHeld){if("undefined"==typeof b.lastSelectedTrack)return void a.$apply(function(){a.track.selected=!0});var e=b.lastSelectedTrack.$index,f=a.$index;a.$index=g;g++)b.tracks[g].selected=!0;b.$apply()}b.lastSelectedTrack=a,d.isTouchMode()&&(c("filter")(b.tracks,{selected:!0}).length>0?d.$broadcast("spotmop:touchContextMenu:show",b.type):d.$broadcast("spotmop:contextMenu:hide")),l()}},b.$on("spotmop:tracklist:enqueueSelectedTracks",function(a,e){if(d.tracklistInFocus===b.$id){var g=null;"undefined"!=typeof e&&1==e&&(g=k.state().currentTracklistPosition());var h=c("filter")(b.tracks,{selected:!0}),j=[];angular.forEach(h,function(a){j.push(a.uri)});var l="Adding "+h.length+" tracks to queue";h.length>10&&(l+="... this could take some time"),i.notify(l),f.addToTrackList(j,g)}}),b.$on("spotmop:tracklist:playSelectedTracks",function(){m()}),b.$on("spotmop:keyboardShortcut:enter",function(){m()}),b.$on("spotmop:tracklist:unqueueSelectedTracks",function(a){if(d.tracklistInFocus===b.$id){var e=c("filter")(b.tracks,{selected:!0}),g=[];angular.forEach(e,function(a){g.push(a.tlid)}),f.removeFromTrackList(g)}}),b.$on("spotmop:tracklist:addSelectedTracksToPlaylist",function(a){if(d.tracklistInFocus===b.$id){var e=c("filter")(b.tracks,{selected:!0}),f=[];angular.forEach(e,function(a){"undefined"!=typeof a.track?f.push(a.track.uri):f.push(a.uri)}),h.create("addToPlaylist",b)}}),b.$on("spotmop:tracklist:addSelectedTracksToPlaylistByUri",function(a,e){if(d.tracklistInFocus===b.$id){var f=c("filter")(b.tracks,{selected:!0}),h=[],j=0;if(angular.forEach(f,function(a){"undefined"!=typeof a.track?"local:"==a.track.uri.substring(0,6)?j++:h.push(a.track.uri):"local:"==a.uri.substring(0,6)?j++:h.push(a.uri)}),j>0){if(h.length<=0)return i.error("Cannot add local tracks to a Spotify playlist"),!1;i.error(j+" local tracks not added to Spotify playlist")}g.addTracksToPlaylist(e,h).then(function(a){i.notify("Added "+h.length+" tracks to playlist")})}}),b.$on("spotmop:tracklist:addSelectedTracksToLibrary",function(a){if(d.tracklistInFocus===b.$id){var e=c("filter")(b.tracks,{selected:!0}),f=[];angular.forEach(e,function(a){"undefined"!=typeof a.track?f.push(g.getFromUri("trackid",a.track.uri)):f.push(g.getFromUri("trackid",a.uri))}),g.addTracksToLibrary(f)}}),b.$on("spotmop:tracklist:selectAll",function(a){n()}),b.$on("spotmop:tracklist:unselectAll",function(a){n()}),b.$on("spotmop:tracklist:copyURIsToClipboard",function(a){var d=c("filter")(b.tracks,{selected:!0}),e="";angular.forEach(d,function(a){""!=e&&(e+=","),e+="undefined"!=typeof a.track?a.track.uri:a.uri}),console.log(e)})}]}}]),angular.module("spotmop.discover",[]).config(["$stateProvider",function(a){a.state("discover",{url:"/discover",templateUrl:"app/discover/template.html",controller:"DiscoverController"})}]).controller("DiscoverController",["$scope","$rootScope","$filter","SpotifyService","SettingsService","NotifyService",function(a,b,c,d,e,f){function g(b){var c=[],e="";angular.forEach(b.track.artists,function(a){c.push({name:a.name,name_encoded:encodeURIComponent(a.name),uri:a.uri}),""!=e&&(e+=","),e+=d.getFromUri("artistid",a.uri)}),a.current.artists=c,d.getRecommendations(!1,!1,e).then(function(b){var c=[];angular.forEach(b.tracks,function(a){var b=a.album;b.artists=a.artists,c.push(b)}),a.current.items=c})}a.favorites=[],a.current=[],a.sections=[],d.getMyFavorites("artists",50,!1,"long_term").then(function(b){a.favorites.items=c("shuffle")(b.items)}),d.getMyFavorites("tracks",50,!1,"short_term").then(function(b){var e=b.items;e=c("shuffle")(b.items),e=c("limitTo")(b.items,5),angular.forEach(e,function(b){d.getRecommendations(!1,!1,!1,!1,b.id).then(function(c){var d=[];angular.forEach(c.tracks,function(a){var b=a.album;b.artists=a.artists,d.push(b)});var e={title:"Because you listened to ",artists:b.artists,items:d};a.sections.push(e)})})}),"undefined"!=typeof a.state().currentTlTrack.track&&g(a.state().currentTlTrack),b.$on("spotmop:currenttrack:loaded",function(a,b){g(b)})}]),angular.module("spotmop.library",[]).config(["$stateProvider",function(a){a.state("library",{url:"/library",templateUrl:"app/library/template.html"}).state("library.playlists",{url:"/playlists",templateUrl:"app/library/playlists.template.html",controller:"LibraryPlaylistsController"}).state("library.playlist",{url:"/playlist/:uri",templateUrl:"app/browse/playlist/template.html",controller:"PlaylistController"}).state("library.tracks",{url:"/tracks",templateUrl:"app/library/tracks.template.html",controller:"LibraryTracksController"}).state("library.artists",{url:"/artists",templateUrl:"app/library/artists.template.html",controller:"LibraryArtistsController"}).state("library.albums",{url:"/albums",templateUrl:"app/library/albums.template.html",controller:"LibraryAlbumsController"})}]).controller("LibraryTracksController",["$scope","$rootScope","$filter","SpotifyService","SettingsService","DialogService",function(a,b,c,d,e,f){function g(a){var b=[];return angular.forEach(a,function(a){var c=a.track;c.added_at=a.added_at,b.push(c)}),b}function h(b){return"undefined"==typeof b?!1:(j=!0,void d.getUrl(b).then(function(b){a.tracklist.tracks=a.tracklist.tracks.concat(g(b.items)),a.tracklist.next=b.next,j=!1}))}a.tracklist={tracks:[],type:"track"};var i=e.getSetting("spotifyuserid",a.$parent.spotifyUser.id);d.getMyTracks(i).then(function(b){a.tracklist=b,a.tracklist.tracks=g(b.items),"undefined"!=typeof b.error&&401==b.error.status&&Spotify.refreshToken()}),a.$on("spotmop:keyboardShortcut:delete",function(b){var e=c("filter")(a.tracklist.tracks,{selected:!0}),f=[];angular.forEach(e,function(a,b){f.push(d.getFromUri("trackid",a.uri))}),d.deleteTracksFromLibrary(f).then(function(b){a.tracklist.tracks=c("filter")(a.tracklist.tracks,{selected:!1})})});var j=!1;a.$on("spotmop:loadMore",function(){!j&&"undefined"!=typeof a.tracklist.next&&a.tracklist.next&&h(a.tracklist.next)})}]).controller("LibraryArtistsController",["$scope","$rootScope","$filter","SpotifyService","SettingsService","DialogService",function(a,b,c,d,e,f){function g(b){return"undefined"==typeof b?!1:(i=!0,void d.getUrl(b).then(function(b){a.artists.items=a.artists.items.concat(b.artists.items),a.artists.next=b.artists.next,i=!1}))}a.artists=[];var h=e.getSetting("spotifyuserid",a.$parent.spotifyUser.id);d.getMyArtists(h).then(function(b){a.artists=b.artists,"undefined"!=typeof b.error&&401==b.error.status&&Spotify.refreshToken()});var i=!1;a.$on("spotmop:loadMore",function(){!i&&"undefined"!=typeof a.artists.next&&a.artists.next&&g(a.artists.next)})}]).controller("LibraryAlbumsController",["$scope","$rootScope","$filter","SpotifyService","SettingsService","DialogService","MopidyService","NotifyService",function(a,b,c,d,e,f,g,h){function i(b){return"undefined"==typeof b?!1:(k=!0,void d.getUrl(b).then(function(b){a.albums.items=a.albums.items.concat(b.items),a.albums.next=b.next,k=!1}))}a.settings=e.getSettings(),a.albums={items:[]};var j=e.getSetting("spotifyuser",{id:null}).id;b.spotifyAuthorized&&d.getMyAlbums(j).then(function(b){a.albums=b}),a.playAlbum=function(a){g.playStream(a.uri)},a.removeFromLibrary=function(b){b.transitioning=!0,d.removeAlbumsFromLibrary(b.id).then(function(c){"undefined"==typeof c.error?a.albums.items.splice(a.albums.items.indexOf(b),1):(h.error(c.error.message),b.transitioning=!1)})};var k=!1;a.$on("spotmop:loadMore",function(){!k&&"undefined"!=typeof a.albums.next&&a.albums.next&&i(a.albums.next)})}]).controller("LibraryPlaylistsController",["$scope","$rootScope","$filter","SpotifyService","SettingsService","DialogService","MopidyService","NotifyService",function(a,b,c,d,e,f,g,h){function i(){g.getPlaylists().then(function(b){a.playlists.items=b,angular.forEach(b,function(b,c){var e=function(b){return function(c){"undefined"==typeof c.error&&(a.playlists.items[b]=c)}}(c);d.getPlaylist(b.uri).then(e)})})}function j(b){return"undefined"==typeof b?!1:(l=!0,void d.getUrl(b).then(function(b){a.playlists.items=a.playlists.items.concat(b.items),a.playlists.next=b.next,l=!1}))}a.createPlaylist=function(){f.create("createPlaylist",a)},a.settings=e.getSettings(),a.playlists={items:[]},a.show=function(b){return"undefined"!=typeof a.settings.playlists&&"undefined"!=typeof a.settings.playlists.onlyshowowned&&a.settings.playlists.onlyshowowned?"jaedb"==b.owner.id?!0:!1:!0};var k=e.getSetting("spotifyuser",{id:null}).id;b.spotifyAuthorized?d.getPlaylists(k).then(function(b){"undefined"!=typeof b.error&&401==b.error.status?Spotify.refreshToken():a.playlists=b}):b.mopidyOnline?i():a.$on("mopidy:state:online",function(){i()});var l=!1;a.$on("spotmop:loadMore",function(){!l&&"undefined"!=typeof a.playlists.next&&a.playlists.next&&j(a.playlists.next)})}]),angular.module("spotmop.local",[]).config(["$stateProvider",function(a){a.state("local",{url:"/local",templateUrl:"app/local/template.html"}).state("local.index",{url:"/index",templateUrl:"app/local/index.html",controller:"LocalController"}).state("local.directory",{url:"/directory/:uri",templateUrl:"app/local/directory.html",controller:"LocalDirectoryController"}).state("local.albums",{url:"/albums",templateUrl:"app/local/albums.html",controller:"LocalAlbumsController"}).state("local.artists",{url:"/artists",templateUrl:"app/local/artists.html",controller:"LocalArtistsController"})}]).controller("LocalController",["$scope","$rootScope","$filter","$stateParams","$localStorage","SpotifyService","SettingsService","DialogService","MopidyService",function(a,b,c,d,e,f,g,h,i){function j(){i.getLibraryItems("local:directory").then(function(b){for(var d=c("filter")(b,{type:"track"}),e=[],f=0;f0&&i.getTracks(e).then(function(b){var c=[];for(var d in b){var e=b[d][0];e.type="localtrack",c.push(e)}a.tracks=c,a.allTracks=c});var g=[];for(f=0;f0){var e=c("filter")(a.allAlbums,{uri:d}),f=a.allAlbums.indexOf(e[0]);a.allAlbums[f].images=b[d]}})}a.settings=h.getSettings(),a.allAlbums=[];var n=50;a.$watch("filterTerm",function(b){n=50,a.albums=c("filter")(a.allAlbums,b),a.albums=c("limitTo")(a.albums,n),a.albums.length>0&&m(a.albums)}),a.mopidyOnline?l():a.$on("mopidy:state:online",function(){l()});var o=!1;a.$on("spotmop:loadMore",function(){o||(o=!0,n+=50,a.filterTerm?(a.albums=c("filter")(a.allAlbums,a.filterTerm),a.albums=c("limitTo")(a.albums,n)):a.albums=c("limitTo")(a.allAlbums,n),f(function(){o=!1,a.albums.length>0&&m(a.albums)},1))})}]).controller("LocalDirectoryController",["$scope","$rootScope","$filter","$stateParams","$localStorage","SpotifyService","SettingsService","DialogService","MopidyService",function(a,b,c,d,e,f,g,h,i){function j(){i.getLibraryItems(l).then(function(b){for(var d=c("filter")(b,{type:"track"}),e=[],f=0;f0&&i.getTracks(e).then(function(b){var c=[];for(var d in b){var e=b[d][0];e.type="localtrack",c.push(e)}a.tracks=c,a.allTracks=c});var g=[];for(f=0;f-1||l.indexOf("local:directory:")>-1)){var m=l.substring(16,l.length);if(""!=m&&(m=m.split("|")),m.length>0)for(var n=0;n=o;o++)"local:directory:"!=l&&(l+="|"),l+=m[o];a.path.push({title:decodeURIComponent(m[n]),uri:l})}l=l.replace("|","/")}a.mopidyOnline?j():a.$on("mopidy:state:online",function(){j()})}]),angular.module("spotmop.player",["spotmop.services.player","spotmop.services.spotify","spotmop.services.mopidy"]).controller("PlayerController",["$scope","$rootScope","$timeout","$interval","$element","PlayerService","MopidyService","SpotifyService","SettingsService",function(a,b,c,d,e,f,g,h,i){a.state=f.state,a.playPause=function(){f.playPause()},a.stop=function(){f.stop()},a.next=function(){f.next()},a.previous=function(){f.previous()},a.seek=function(b){var c,d,e,g,h;c=$(b.target).hasClass("slider")?$(b.target):$(b.target).closest(".slider"),d=c.offset(),e=b.pageX-d.left,g=e/c.innerWidth(),h=Math.round(g*a.state().currentTlTrack.track.length),f.seek(h)},a.setVolume=function(a){var b,c,d,e;b=$(a.target).hasClass("slider")?$(a.target):$(a.target).closest(".slider"),c=b.offset(),d=a.pageX-c.left,e=d/b.innerWidth()*100,e=parseInt(e),f.setVolume(e)},a.toggleRepeat=function(){f.toggleRepeat()},a.toggleRandom=function(){f.toggleRandom()},a.toggleMute=function(){f.toggleMute()},a.toggleConsume=function(){f.toggleConsume()},a.$on("mopidy:event:tracklistChanged",function(b){g.getCurrentTlTracks().then(function(b){a.$parent.currentTracklist=b})})}]),angular.module("spotmop.services.player",[]).factory("PlayerService",["$rootScope","$interval","$filter","SettingsService","MopidyService","SpotifyService","NotifyService","LastfmService",function(a,b,c,d,e,f,g,h){function i(){e.getRepeat().then(function(a){p.isRepeat=a}),e.getRandom().then(function(a){p.isRandom=a}),e.getMute().then(function(a){p.isMute=a}),e.getConsume().then(function(a){p.isConsume=a})}function j(a){"undefined"==typeof a?e.getTimePosition().then(function(a){p.playPosition=a}):p.playPosition=a}function k(a){"undefined"!=typeof a?p.volume=a:e.getVolume().then(function(a){p.volume=a})}function l(a){"undefined"!=typeof a?("playing"==a?p.playing=!0:p.playing=!1,o()):e.getState().then(function(a){"playing"==a?p.playing=!0:p.playing=!1,o()})}function m(b){var d=function(b){if(p.currentTlTrack=b,"undefined"!=typeof b.track.album.images&&b.track.album.images.length>0){var d=[{__model__:"Image",uri:b.track.album.images}];p.currentTlTrack.track.images=c("sizedImages")(d),a.$broadcast("spotmop:currenttrack:loaded",p.currentTlTrack)}else if("spotify:"==b.track.uri.substring(0,8))f.getTrack(b.track.uri).then(function(b){"undefined"!=typeof b.album&&(p.currentTlTrack.track.images=c("sizedImages")(b.album.images)),a.$broadcast("spotmop:currenttrack:loaded",p.currentTlTrack)});else{var e=encodeURIComponent(b.track.artists[0].name),g=encodeURIComponent(b.track.album.name);e&&g&&h.albumInfo(e,g).then(function(b){p.currentTlTrack.track.image=!1,"undefined"!=typeof b.album&&(p.currentTlTrack.track.images=c("sizedImages")(b.album.image)),a.$broadcast("spotmop:currenttrack:loaded",p.currentTlTrack)})}j(),o()};"undefined"!=typeof b?d(b):e.getCurrentTlTrack().then(function(a){null!==a&&void 0!==a&&(a.track.name.indexOf("[loading]")>-1?e.lookup(a.track.uri).then(function(a){d(a[0])}):d(a))})}function n(){e.getCurrentTlTracks().then(function(b){a.currentTracklist=b,(!b||b.length<=0)&&(p.currentTlTrack=!1,o())})}function o(){var a=p.currentTlTrack.track,b="No track playing";if(a){var c="\u25a0 ",d="";$.each(a.artists,function(a,b){""!=d&&(d+=", "),d+=b.name}),p.playing&&(c="\u25b6 "),b=c+" "+a.name+" - "+d}document.title=b}var p={playing:!1,isRepeat:!1,isRandom:!1,isMute:!1,isConsume:!1,volume:100,playPosition:0,currentTlTrack:!1,currentTracklistPosition:function(){if(p.currentTlTrack){var b=c("filter")(a.currentTracklist,{tlid:p.currentTlTrack.tlid}),d=0;return b.length>0&&(d=a.currentTracklist.indexOf(b[0])+1),d}return null},playPositionPercent:function(){return p.currentTlTrack?(p.playPosition/p.currentTlTrack.track.length*100).toFixed(2):0}};a.$on("mopidy:state:online",function(){i(),m(),l(),k(),n(),e.getState().then(function(a){"playing"==a?p.playing=!0:p.playing=!1})}),a.$on("mopidy:event:tracklistChanged",function(a,b){n()}),a.$on("mopidy:event:optionsChanged",function(a,b){i()}),a.$on("mopidy:event:playbackStateChanged",function(a,b){l(b.new_state)}),a.$on("mopidy:event:seeked",function(a,b){j(b.time_position)}),a.$on("mopidy:event:volumeChanged",function(a,b){b.volume!=p.volume&&k(b.volume)}),a.$on("mopidy:event:trackPlaybackStarted",function(a,b){("undefined"==typeof p.currentTlTrack.track||p.currentTlTrack.track.uri!=b.tl_track.track.uri)&&(p.currentTlTrack=b.tl_track,m(b.tl_track),l())}),b(function(){p.playing&&"undefined"!=typeof p.currentTlTrack&&"undefined"!=typeof p.currentTlTrack.track&&p.playPosition=100&&(p.volume=100),q.setVolume(p.volume),g.shortcut("volume-up"))}),a.$on("spotmop:keyboardShortcut:down",function(b){a.ctrlKeyHeld&&(p.volume-=10,p.volume<0&&(p.volume=0),q.setVolume(p.volume),g.shortcut("volume-down"))});var q={state:function(){return p},playPause:function(){p.playing?(e.pause(),p.playing=!1):(e.play(),p.playing=!0)},stop:function(){e.stopPlayback(),p.playing=!1},next:function(){e.next()},previous:function(){e.previous()},seek:function(a){p.playPosition=a,e.seek(a)},setVolume:function(a){p.volume=a,e.setVolume(a)},toggleRepeat:function(){p.isRepeat?e.setRepeat(!1).then(function(a){p.isRepeat=!1}):e.setRepeat(!0).then(function(a){p.isRepeat=!0}),console.log(p)},toggleRandom:function(){p.isRandom?e.setRandom(!1).then(function(a){p.isRandom=!1}):e.setRandom(!0).then(function(a){p.isRandom=!0})},toggleMute:function(){p.isMute?e.setMute(!1).then(function(a){p.isMute=!1}):e.setMute(!0).then(function(a){p.isMute=!0})},toggleConsume:function(){p.isConsume?e.setConsume(!1).then(function(a){p.isConsume=!1}):e.setConsume(!0).then(function(a){p.isConsume=!0})}};return q}]),angular.module("spotmop.queue",[]).config(["$stateProvider",function(a){a.state("queue",{url:"/queue",templateUrl:"app/queue/template.html",controller:"QueueController"})}]).controller("QueueController",["$scope","$rootScope","$filter","$timeout","$state","MopidyService","SpotifyService","DialogService",function(a,b,c,d,e,f,g,h){function i(b){var c=0;$.each(b,function(a,b){c+=b.track.length}),a.totalTime=Math.round(c/1e5)}a.totalTime=0,a.tracks=b.currentTracklist,a.limit=50;var j=!1;a.$on("spotmop:loadMore",function(){!j&&a.tracks.length>=a.limit&&(a.limit+=50,j=!0,d(function(){j=!1},100))}),a.addUri=function(){h.create("addbyuri",a)},a.clearQueue=function(){f.clearCurrentTrackList()},b.$watch(function(a){return a.currentTracklist},function(b,c){a.tracks=b,i(b)}),a.$on("spotmop:keyboardShortcut:delete",function(b){var d=c("filter")(a.tracks,{selected:!0}),e=[];angular.forEach(d,function(a,b){e.push(a.tlid)}),a.$apply(function(){a.tracks=c("filter")(a.tracks,{selected:!1})}),f.removeFromTrackList(e)})}]),angular.module("spotmop.search",[]).config(["$stateProvider",function(a){a.state("search",{url:"/search/:query/:type",templateUrl:"app/search/template.html",controller:"SearchController",params:{type:{squash:!0,value:"all"},query:{squash:!0,value:null}}})}]).controller("SearchController",["$scope","$rootScope","$state","$stateParams","$timeout","$filter","SpotifyService","MopidyService",function(a,b,c,d,e,f,g,h){function i(c,d){if("undefined"==typeof c)var c=a.type;switch(c){case"track":g.getSearchResults("track",d,50).then(function(b){a.tracklist.tracks=a.tracklist.tracks.concat(b.tracks.items),a.tracklist.type="track",a.tracklist.next=b.tracks.next,l=b.tracks.next?b.tracks.offset+b.tracks.limit:!1});break;case"album":g.getSearchResults("album",d,50).then(function(b){console.log(b),a.albums=b.albums,l=b.albums.next?b.albums.offset+b.albums.limit:!1});break;case"artist":g.getSearchResults("artist",d,50).then(function(b){a.artists=b.artists,a.next=b.artists.next,a.offset=b.artists.offset});break;case"playlist":g.getSearchResults("playlist",d,50).then(function(b){a.playlists=b.playlists,a.next=b.playlists.next,a.offset=b.playlists.offset});break;case"other":b.mopidyOnline?j(a.query):b.$on("mopidy:state:online",function(){j(a.query)});break;default:g.getSearchResults("track,album,artist,playlist",d,50).then(function(b){a.albums=b.albums,a.artists=b.artists,a.playlists=b.playlists,a.tracklist=b.tracks,a.tracklist.type="track",a.tracklist.tracks=b.tracks.items})}}function j(b){h.search(b,!1,["soundcloud:","local:"]).then(function(b){for(var c=0;c0&&console.log("A dialog already exists..."),$("body").append(b('')(c))},remove:function(){$("body").children(".dialog").fadeOut(200,function(){$(this).remove()})}}}]).directive("dialog",["$compile",function(a){return{restrict:"E",replace:!0,transclude:!0,scope:{type:"@"},templateUrl:"app/services/dialog/template.html",link:function(b,c){c.find(".content").html(a("<"+b.type+"dialog />")(b))},controller:["$scope","$element","DialogService",function(a,b,c){a.closeDisabled=!1,"initialsetup"==a.type&&(a.closeDisabled=!0),a.closeDialog=function(){c.remove()},a.$on("spotmop:keyboardShortcut:esc",function(b){a.closeDisabled||c.remove()})}]}}]).directive("createplaylistdialog",function(){return{restrict:"E",replace:!0,transclude:!0,templateUrl:"app/services/dialog/createplaylist.template.html",controller:["$scope","$element","$rootScope","DialogService","SettingsService","SpotifyService",function(a,b,c,d,e,f){a.playlistPublic="true",a.savePlaylist=function(){a.playlistName&&""!=a.playlistName?(a.saving=!0,"true"==a.playlistPublic?a.playlistPublic=!0:a.playlistPublic=!1,f.createPlaylist(a.$parent.spotifyUser.id,{name:a.playlistName,"public":a.playlistPublic}).then(function(b){a.$parent.playlists.items.push(b),a.$parent.updatePlaylists(),d.remove(),c.$broadcast("spotmop:notifyUser",{id:"saved",message:"Saved",autoremove:!0})})):a.error=!0}}]}}).directive("editplaylistdialog",function(){return{restrict:"E",replace:!0,transclude:!0,templateUrl:"app/services/dialog/editplaylist.template.html",controller:["$scope","$element","$rootScope","DialogService","SpotifyService",function(a,b,c,d,e){a.playlistNewName=a.$parent.playlist.name,a.playlistNewPublic=a.$parent.playlist["public"].toString(),a.saving=!1,a.savePlaylist=function(){a.playlistNewName&&""!=a.playlistNewName?(a.saving=!0,"true"==a.playlistNewPublic?a.playlistNewPublic=!0:a.playlistNewPublic=!1,e.updatePlaylist(a.$parent.playlist.uri,{name:a.playlistNewName,"public":a.playlistNewPublic}).then(function(b){a.$parent.playlist.name=a.playlistNewName,a.$parent.playlist["public"]=a.playlistNewPublic,a.$parent.updatePlaylists(),d.remove(),c.$broadcast("spotmop:notifyUser",{id:"saved",message:"Saved",autoremove:!0})})):a.error=!0}}]}}).directive("addtoplaylistdialog",function(){return{restrict:"E",replace:!0,transclude:!0,templateUrl:"app/services/dialog/addtoplaylist.template.html",controller:["$scope","$element","$rootScope","$filter","DialogService","SpotifyService","SettingsService","NotifyService",function(a,b,c,d,e,f,g,h){a.playlists=[];var i=g.getSetting("spotifyuser",{id:"undefined"}).id;f.getPlaylists(i,50).then(function(b){a.playlists=d("filter")(b.items,{owner:{id:i}})}),a.playlistSelected=function(b){var g=d("filter")(a.$parent.tracks,{selected:!0}),i=[];angular.forEach(g,function(a){"undefined"!=typeof a.track?i.push(a.track.uri):i.push(a.uri)}),f.addTracksToPlaylist(b.uri,i).then(function(a){e.remove(),c.$broadcast("spotmop:tracklist:unselectAll"),h.notify(i.length+" tracks added to "+b.name)})}}]}}).directive("volumecontrolsdialog",function(){return{restrict:"E",replace:!0,transclude:!0,templateUrl:"app/services/dialog/volumecontrols.template.html",controller:["$scope","$element","$rootScope","$filter","DialogService","PlayerService",function(a,b,c,d,e,f){a.state=function(){return f.state()},a.setVolume=function(a){var b,c,d,e;b=$(a.target).hasClass("slider")?$(a.target):$(a.target).closest(".slider"), +c=b.offset(),d=a.pageX-c.left,e=d/b.innerWidth()*100,e=parseInt(e),f.setVolume(e)}}]}}).directive("initialsetupdialog",function(){return{restrict:"E",replace:!0,transclude:!0,templateUrl:"app/services/dialog/initialsetup.template.html",controller:["$scope","$element","$rootScope","$filter","DialogService","SettingsService","SpotifyService",function(a,b,c,d,e,f,g){a.settings=f.getSettings(),f.setSetting("spotify",!0,"authorizationenabled"),f.setSetting("spotmop","default","pointerMode"),a.saving=!1,a.save=function(){a.name&&""!=a.name?(a.saving=!0,f.getSetting("spotify",!1,"authorizationenabled")&&g.authorize(),f.setSetting("pushername",a.name),e.remove()):a.error=!0}}]}}).directive("addbyuridialog",function(){return{restrict:"E",replace:!0,transclude:!0,templateUrl:"app/services/dialog/addbyuri.template.html",controller:["$scope","$element","DialogService","SpotifyService","MopidyService",function(a,b,c,d,e){a.saving=!1,a.add=function(){a.uri&&""!=a.uri?(a.error=!1,a.saving=!0,e.addToTrackList([a.uri])["catch"](function(b){a.saving=!1,a.error=!0}).then(function(b){a.error||c.remove()})):a.error=!0}}]}}),angular.module("spotmop.services.lastfm",[]).factory("LastfmService",["$rootScope","$resource","$localStorage","$http","$interval","$timeout","$filter","$q","SettingsService","NotifyService",function(a,b,c,d,e,f,g,h,i,j){var k={sendRequest:function(a){var b=h.defer();return d({cache:!0,method:"GET",url:l+"?format=json&api_key="+m+"&"+a}).success(function(a){b.resolve(a)}).error(function(a){j.error(a.error.message),b.reject(a.error.message)}),b.promise},trackInfo:function(a,b){return a=encodeURIComponent(a),this.sendRequest("method=track.getInfo&track="+b+"&artist="+a)},albumInfo:function(a,b){return a=encodeURIComponent(a),b=encodeURIComponent(b),this.sendRequest("method=album.getInfo&album="+b+"&artist="+a)},albumInfoByMbid:function(a){return this.sendRequest("method=album.getInfo&mbid="+a)},artistInfo:function(a){return a=encodeURIComponent(a),this.sendRequest("method=artist.getInfo&artist="+a)},artistInfoByMbid:function(a){return this.sendRequest("method=artist.getInfo&mbid="+a)}},l="http://ws.audioscrobbler.com/2.0",m=i.getSetting("lastfmkey","4320a3ef51c9b3d69de552ac083c55e3");return k}]),angular.module("spotmop.services.mopidy",[]).factory("MopidyService",["$q","$rootScope","$cacheFactory","$location","$timeout","SettingsService","PusherService","NotifyService","cfpLoadingBar",function(a,b,c,d,e,f,g,h,i){function j(b,c){return function(){var d=a.defer(),e=Array.prototype.slice.call(arguments),f=c||this;return k(b,f,e).then(function(a){d.resolve(a)},function(a){h.error(a),d.reject(a)}),d.promise}}function k(a,b,c){for(var d=a.split("."),e=d.pop(),f=0;f0&&(d={tlid:b[0].tlid}),c.mopidy.playback.play().then(function(){return c.mopidy.tracklist.add({uris:a,at_position:1}).then(function(){i.complete()})},m)},m)},playTlTrack:function(a){return this.mopidy.playback.play(a)},playStream:function(a,b){i.start(),i.set(.25);var c=this;c.stopPlayback(!0).then(function(){c.mopidy.tracklist.clear()},m).then(function(){c.mopidy.tracklist.add({at_position:0,uri:a}).then(function(){i.complete()})},m).then(function(){c.mopidy.playback.play()},m)},play:function(){return j("mopidy.playback.play",this)()},pause:function(){return j("mopidy.playback.pause",this)()},stopPlayback:function(a){return j("mopidy.playback.stop",this)()},previous:function(){return j("mopidy.playback.previous",this)()},next:function(){var a=f.getSetting("pushername","User"),b=f.getSetting("pusherip",null);return g.send({title:"Track skipped",body:a+" vetoed this track!",clientip:b,spotifyuser:JSON.stringify(f.getSetting("spotifyuser",{}))}),j("mopidy.playback.next",this)()},getRepeat:function(){return j("mopidy.tracklist.getRepeat",this)()},setRepeat:function(a){return j("mopidy.tracklist.setRepeat",this)([a])},getRandom:function(){return j("mopidy.tracklist.getRandom",this)()},setRandom:function(a){return j("mopidy.tracklist.setRandom",this)([a])},getConsume:function(){return j("mopidy.tracklist.getConsume",this)()},setConsume:function(a){return j("mopidy.tracklist.setConsume",this)([a])},getCurrentTrackList:function(){return j("mopidy.tracklist.getTracks",this)()},clearCurrentTrackList:function(){return j("mopidy.tracklist.clear",this)()},getCurrentTlTracks:function(){return j("mopidy.tracklist.getTlTracks",this)()},addToTrackList:function(a,b){if("undefined"==typeof b)var b=null;return j("mopidy.tracklist.add",this)({uris:a,at_position:b})},removeFromTrackList:function(a){var b=this;b.mopidy.tracklist.remove({tlid:a}).then(function(){return!0})}}}]),angular.module("spotmop.services.notify",[]).factory("NotifyService",["$rootScope","$compile","$interval","$timeout","SettingsService",function(a,b,c,d,e){return{notify:function(a,b){if("undefined"==typeof b)var b=2500;var c=$(''+a+"");$("#notifications").append(c),b&&d(function(){c.fadeOut(200,function(){c.remove()})},b)},error:function(a,b){if("undefined"==typeof b)var b=2500;var c=$(''+a+"");$("#notifications").append(c),b&&d(function(){c.fadeOut(200,function(){c.remove()})},b)},spotifyAuthenticationError:function(){this.error("Please authenticate with Spotify - you can find this under settings")},shortcut:function(a){$("#notifications").find("notification.keyboard-shortcut").remove();var b=$('');$("#notifications").append(b),d(function(){b.fadeOut(200,function(){b.remove()})},1500)},browserNotify:function(a,b,c){if(e.getSetting("notificationsDisabled",!1))return!1;var d=window.Notification||window.mozNotification||window.webkitNotification;if("undefined"==typeof d)return!1;if("undefined"!=typeof d&&d.requestPermission(function(a){}),"undefined"==typeof c)var c="";new d(a,{body:b,dir:"auto",lang:"EN",tag:"spotmopNotification",icon:c});return!0}}}]).directive("notification",function(){return{restrict:"AE",link:function(a,b,c){console.log(b)}}}),angular.module("spotmop.services.pusher",[]).factory("PusherService",["$rootScope","$http","$q","$localStorage","$cacheFactory","$templateCache","SettingsService","NotifyService",function(a,b,c,d,e,f,g,h){"undefined"==typeof d.pusher&&(d.pusher={});var i="http://"+g.getSetting("mopidyhost",window.location.hostname);i+=":"+g.getSetting("mopidyport","6680"),i+="/spotmop/";var j={pusher:{},isConnected:!1,start:function(){var b=g.getSetting("mopidyhost",window.location.hostname),c=g.getSetting("pusherport","6681");try{var d="ws://"+b+":"+c+"/pusher",i=new WebSocket(d);i.onopen=function(){a.$broadcast("spotmop:pusher:online"),this.isConnected=!0},i.onmessage=function(b){var c=JSON.parse(b.data);if(c.pusher)if(c.startup){console.info("Pusher connected as "+c.details.id),g.setSetting("pusherid",c.details.id),g.setSetting("pusherip",c.details.ip),g.getSetting("version",0,"installed")!=c.version&&(h.notify("New version detected, clearing caches..."),e.get("$http").removeAll(),f.removeAll(),g.setSetting("version",c.version,"installed"),g.runUpgrade());var d=g.getSetting("pushername","");d&&j.setMe(d)}else c.id==g.getSetting("pusherid","")||g.getSetting("pusherdisabled",!1)||a.$broadcast("spotmop:pusher:received",c)},i.onclose=function(){a.$broadcast("spotmop:pusher:offline"),j.isConnected=!1,setTimeout(function(){j.start()},5e3)},j.pusher=i}catch(k){console.log("Connecting with Pusher failed with the following error message: "+k)}},stop:function(){this.pusher=null,this.isConnected=!1},send:function(a){a.pusher=!0,a.id=g.getSetting("pusherid",null),j.pusher.send(JSON.stringify(a))},setMe:function(a){var b=g.getSetting("pusherid",null);$.ajax({method:"GET",cache:!1,url:i+"pusher/me?id="+b+"&name="+a})},getConnections:function(){var a=c.defer();return b({method:"GET",cache:!1,url:i+"pusher/connections"}).success(function(b){a.resolve(b)}).error(function(b){h.error(b.error.message),a.reject(b.error.message)}),a.promise}};return j}]),angular.module("spotmop.services.spotify",[]).factory("SpotifyService",["$rootScope","$resource","$localStorage","$http","$interval","$timeout","$filter","$q","$cacheFactory","SettingsService","NotifyService",function(a,b,c,d,e,f,g,h,i,j,k){var l={authenticationMethod:"server",start:function(){var b=$('');$(body).append(b),"undefined"==typeof c.spotify&&(c.spotify={}),"undefined"==typeof c.spotify.AccessToken&&(c.spotify.AccessToken=null),"undefined"==typeof c.spotify.RefreshToken&&(c.spotify.RefreshToken=null),"undefined"==typeof c.spotify.AuthorizationCode&&(c.spotify.AuthorizationCode=null),"undefined"==typeof c.spotify.AccessTokenExpiry&&(c.spotify.AccessTokenExpiry=null),window.addEventListener("message",function(b){if("http://jamesbarnsley.co.nz"!==b.origin)return!1;var d=JSON.parse(b.data);console.info("Spotify authorization successful"),c.spotify.AuthorizationCode=d.authorization_code,c.spotify.AccessToken=d.access_token,c.spotify.RefreshToken=d.refresh_token,a.spotifyOnline=!0,this.authenticationMethod="client",l.getMe().then(function(b){j.setSetting("spotifyuser",b),a.$broadcast("spotmop:spotify:authenticationChanged",this.authenticationMethod)})},!1),this.isAuthorized()?(a.spotifyAuthorized=!0,this.authenticationMethod="client"):(j.setSetting("spotifyuser",!1),a.spotifyAuthorized=!1,this.authenticationMethod="server"),a.$broadcast("spotmop:spotify:online")},logout:function(){c.spotify={},this.authenticationMethod="server",this.refreshToken(),a.$broadcast("spotmop:spotify:authenticationChanged",this.authenticationMethod)},authorize:function(){var a=$(document).find("#authorization-frame");a.attr("src","http://jamesbarnsley.co.nz/spotmop.php?action=authorize&app="+location.protocol+"//"+window.location.host)},isAuthorized:function(){return c.spotify.AuthorizationCode&&c.spotify.RefreshToken?!0:!1},refreshToken:function(){var b=h.defer(),e="";if("client"==this.authenticationMethod)e="http://jamesbarnsley.co.nz/spotmop.php?action=refresh&refresh_token="+c.spotify.RefreshToken;else{if("server"!=this.authenticationMethod)return!1;var f=j.getSetting("mopidyhost",window.location.hostname),g=j.getSetting("mopidyport","6680");e="http://"+f+":"+g+"/spotmop/auth"}return d({method:"GET",url:e,dataType:"json",async:!1,timeout:1e4}).success(function(d){"undefined"!=typeof d.error?(k.error("Spotify authorization error: "+d.error_description),a.spotifyOnline=!1,b.reject(d.error.message)):(c.spotify.AccessToken=d.access_token,c.spotify.AccessTokenExpiry=(new Date).getTime()+36e5,a.spotifyOnline=!0,b.resolve(d))}),b.promise},serviceUnavailable:function(){k.error("Request failed. Spotify API may be temporarily unavailable.")},getFromUri:function(a,b){var c=b.split(":");return"userid"==a&&"user"==c[1]?c[2]:"playlistid"==a&&"playlist"==c[3]?c[4]:"artistid"==a&&"artist"==c[1]?c[2]:"albumid"==a&&"album"==c[1]?c[2]:"trackid"==a&&"track"==c[1]?c[2]:null},uriType:function(a){var b=a.split(":");return"spotify"==b[0]&&"artist"==b[1]?"artist":"spotify"==b[0]&&"album"==b[1]?"album":"spotify"==b[0]&&"user"==b[1]&&"playlist"==b[3]?"playlist":null},getUrl:function(a){var b=h.defer();return d({method:"GET",url:a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getMe:function(){var a=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(b){a.resolve(b)}).error(function(b){k.error(b.error.message),a.reject(b.error.message)}),a.promise):(a.reject(),a.promise)},getUser:function(a){var b=this.getFromUri("userid",a),c=h.defer();return d({method:"GET",url:m+"users/"+b}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},isFollowing:function(a,b){var e=this.getFromUri(a+"id",b),f=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/following/contains?type="+a+"&ids="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},getTrack:function(a){var b=this.getFromUri("trackid",a),c=h.defer();return d({method:"GET",url:m+"tracks/"+b}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getMyTracks:function(a){var b=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/tracks/?limit=50",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise):(b.reject(),b.promise)},addTracksToLibrary:function(a){var b=h.defer();if(!this.isAuthorized())return b.reject(),b.promise;var e=i.get("$http");return e.remove(m+"me/tracks/?limit=50"),d({method:"PUT",url:m+"me/tracks",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},addAlbumsToLibrary:function(a){if(!this.isAuthorized())return e.reject(),e.promise;var b=i.get("$http");b.remove(m+"me/albums?limit=40&offset=0");var e=h.defer();return"array"!=typeof a&&(a=[a]),d({method:"PUT",url:m+"me/albums",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},removeAlbumsFromLibrary:function(a){if(!this.isAuthorized())return b.reject(),b.promise;var b=h.defer();return"array"!=typeof a&&(a=[a]),d({method:"DELETE",url:m+"me/albums",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},deleteTracksFromLibrary:function(a){var b=h.defer();if(!this.isAuthorized())return b.reject(),b.promise;var e=i.get("$http");return e.remove(m+"me/tracks/?limit=50"),d({method:"DELETE",url:m+"me/tracks",dataType:"json",data:JSON.stringify({ids:a}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getMyArtists:function(a){var b=h.defer();return this.isAuthorized()?(d({method:"GET",url:m+"me/following?type=artist",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise):(b.reject(),b.promise)},getMyAlbums:function(a,b,e){var f=h.defer();return this.isAuthorized()?("undefined"!=typeof b&&b||(b=20),"undefined"==typeof e&&(e=0),d({cache:!0,method:"GET",url:m+"me/albums?limit="+b+"&offset="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},isFollowingArtist:function(a,b){var e=this.getFromUri("artistid",a),f=h.defer();return this.isAuthorized()?(d({cache:!1,method:"GET",url:m+"me/following/contains?type=artist&ids="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},followArtist:function(a){var b=this.getFromUri("artistid",a),e=h.defer();return this.isAuthorized()?(d({method:"PUT",cache:!1,url:m+"me/following?type=artist&ids="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise):(e.reject(),e.promise)},unfollowArtist:function(a){var b=this.getFromUri("artistid",a),e=h.defer();return this.isAuthorized()?(d({method:"DELETE",cache:!1,url:m+"me/following?type=artist&ids="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise):(e.reject(),e.promise)},getPlaylists:function(a,b){"undefined"==typeof b&&(b=40);var e=h.defer();return d({cache:!1,method:"GET",url:m+"users/"+a+"/playlists?limit="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},getPlaylist:function(a){var b=this.getFromUri("userid",a),e=this.getFromUri("playlistid",a),f=h.defer();return d({cache:!0,method:"GET",url:m+"users/"+b+"/playlists/"+e+"?market="+n,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise},isFollowingPlaylist:function(a,b){var e=this.getFromUri("userid",a),f=this.getFromUri("playlistid",a),g=h.defer();return this.isAuthorized()?(d({cache:!0,method:"GET",url:m+"users/"+e+"/playlists/"+f+"/followers/contains?ids="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise):(g.reject(),g.promise)},followPlaylist:function(a){var b=this.getFromUri("userid",a),e=this.getFromUri("playlistid",a),f=h.defer();return this.isAuthorized()?(d({method:"PUT",url:m+"users/"+b+"/playlists/"+e+"/followers",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},unfollowPlaylist:function(a){var b=this.getFromUri("userid",a),e=this.getFromUri("playlistid",a),f=h.defer();return this.isAuthorized()?(d({method:"DELETE",url:m+"users/"+b+"/playlists/"+e+"/followers",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise):(f.reject(),f.promise)},featuredPlaylists:function(a){"undefined"==typeof a&&(a=40);var b=g("date")(new Date,"yyyy-MM-ddTHH:mm:ss"),e=j.getSetting("countrycode","NZ"),f=h.defer();return d({cache:!0,method:"GET",url:m+"browse/featured-playlists?timestamp="+b+"&country="+e+"&limit="+a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){f.resolve(a)}).error(function(a){k.error(a.error.message),f.reject(a.error.message)}),f.promise},addTracksToPlaylist:function(a,b){var e=this.getFromUri("userid",a),f=this.getFromUri("playlistid",a),g=h.defer();return this.isAuthorized()?(d({method:"POST",url:m+"users/"+e+"/playlists/"+f+"/tracks",dataType:"json",data:JSON.stringify({uris:b}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise):(g.reject(),g.promise)},movePlaylistTracks:function(a,b,e,f){if(!this.isAuthorized())return l.reject(),l.promise;var g=this.getFromUri("userid",a),i=this.getFromUri("playlistid",a);if(g!=j.getSetting("spotifyuser",{id:null}).id)return!1;var l=h.defer();return d({method:"PUT",url:m+"users/"+g+"/playlists/"+i+"/tracks",dataType:"json",data:JSON.stringify({range_start:b,range_length:e,insert_before:f}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){l.resolve(a)}).error(function(a){k.error(a.error.message),l.reject(a.error.message)}),l.promise},deleteTracksFromPlaylist:function(a,b,e){var f=this.getFromUri("userid",a),g=this.getFromUri("playlistid",a),i=h.defer();return d({method:"DELETE",url:m+"users/"+f+"/playlists/"+g+"/tracks",dataType:"json",data:JSON.stringify({snapshot_id:b,positions:e}),contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){i.resolve(a)}).error(function(a){k.error(a.error.message),i.reject(a.error.message)}),i.promise},createPlaylist:function(a,b){var e=h.defer();return this.isAuthorized()?(d({method:"POST",url:m+"users/"+a+"/playlists/",dataType:"json",data:b,contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise):(e.reject(),e.promise)},updatePlaylist:function(a,b){var e=this.getFromUri("userid",a),f=this.getFromUri("playlistid",a),g=h.defer();return this.isAuthorized()?(d({method:"PUT",url:m+"users/"+e+"/playlists/"+f,dataType:"json",data:b,contentType:"application/json; charset=utf-8",headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise):(g.reject(),g.promise)},newReleases:function(a,b){"undefined"!=typeof a&&a||(a=40),"undefined"==typeof b&&(b=0);var e=h.defer();return d({cache:!0,method:"GET",url:m+"browse/new-releases?country="+n+"&limit="+a+"&offset="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){for(var b=[],c=Math.ceil(a.albums.items.length/20),d=1;c>d;d++){for(var f=a.albums.items.splice(0,20),g=[],h=0;20>h;h++)g.push(f[h].id);l.getAlbums(g).then(function(f){b=b.concat(f.albums),d>=c&&(a.albums.items=b,e.resolve(a))})}}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},browseCategories:function(a){"undefined"==typeof a&&(a=40);var b=h.defer();return d({cache:!0,method:"GET",url:m+"browse/categories?limit="+a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getCategory:function(a){var b=h.defer();return d({cache:!0,method:"GET",url:m+"browse/categories/"+a,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getCategoryPlaylists:function(a,b){"undefined"==typeof b&&(b=40);var e=h.defer();return d({cache:!0,method:"GET",url:m+"browse/categories/"+a+"/playlists?limit="+b,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},getMyFavorites:function(a,b,e,f){if("undefined"==typeof b||!b)var b=25;if("undefined"==typeof e||!e)var e=0;if("undefined"==typeof f||!f)var f="long_term";var g=h.defer();return d({cache:!0,method:"GET",url:m+"me/top/"+a+"?limit="+b+"&offset="+e+"&time_range="+f,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){g.resolve(a)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise},getRecommendations:function(a,b,e,f,g){var i=m+"recommendations/?";"undefined"!=typeof a&&a&&(i+="limit="+a),"undefined"!=typeof b&&b&&(i+="&offset="+b),"undefined"!=typeof e&&e&&(i+="&seed_artists="+e),"undefined"!=typeof f&&f&&(i+="&seed_albums="+f),"undefined"!=typeof g&&g&&(i+="&seed_tracks="+g);var j=h.defer();return d({cache:!0,method:"GET",url:i,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){j.resolve(a)}).error(function(a){k.error(a.error.message),j.reject(a.error.message)}),j.promise},getArtist:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getArtists:function(a){var b=this,c="";angular.forEach(a,function(a){""!=c&&(c+=","),c+=b.getFromUri("artistid",a)});var e=h.defer();return d({method:"GET",url:m+"artists/?ids="+c}).success(function(a){e.resolve(a)}).error(function(a){k.error(a.error.message),e.reject(a.error.message)}),e.promise},getTopTracks:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b+"/top-tracks?country="+n}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getRelatedArtists:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b+"/related-artists"}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},getAlbum:function(a){var b=h.defer(),c=this.getFromUri("albumid",a);return d({method:"GET",url:m+"albums/"+c}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getAlbums:function(a){for(var b=h.defer(),c="",e=0;e0&&(c+=","),c+=a[e];return d({cache:!0,method:"GET",url:m+"albums?ids="+c+"&market="+n}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise},getArtistAlbums:function(a){var b=this.getFromUri("artistid",a),c=h.defer();return d({cache:!0,method:"GET",url:m+"artists/"+b+"/albums?album_type=album,single&market="+n}).success(function(a){c.resolve(a)}).error(function(a){k.error(a.error.message),c.reject(a.error.message)}),c.promise},isAlbumInLibrary:function(a){for(var b=h.defer(),e="",f=0;f0&&(e+=","),e+=a[f];return this.isAuthorized()?(d({method:"GET",url:m+"me/albums/contains?ids="+e,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(a){b.resolve(a)}).error(function(a){k.error(a.error.message),b.reject(a.error.message)}),b.promise):(b.reject(),b.promise)},getSearchResults:function(a,b,e,f){"undefined"==typeof e&&(e=10),"undefined"==typeof f&&(f=0);var g=h.defer();return d({cache:!0,method:"GET",url:m+"search?q="+b+"&type="+a+"&country="+n+"&limit="+e+"&offset="+f,headers:{Authorization:"Bearer "+c.spotify.AccessToken}}).success(function(b){if("album"==a){for(var c=[],d=[];b.albums.items.length;)d.push(b.albums.items.splice(0,20));console.log(d);for(var e=0;e=d.length-1&&(b.albums.items=c,g.resolve(b))})}}else g.resolve(b)}).error(function(a){k.error(a.error.message),g.reject(a.error.message)}),g.promise}},m="https://api.spotify.com/v1/",n=j.getSetting("spotifycountry","NZ");j.getSetting("spotifylocale","en_NZ");return l}]).factory("SpotifyServiceIntercepter",["$q","$rootScope","$injector","$localStorage",function(a,b,c,d){"use strict";function e(a,b,d){function e(a){b.resolve(a)}function f(a){b.reject(a)}var g=c.get("$http");a.headers={Authorization:"Bearer "+d},g(a).then(e,f)}var f=0,g={responseError:function(b){if(b.config.url.search("https://api.spotify.com/")>=0&&3>f){if(401==b.status){f++;var d=a.defer();return c.get("SpotifyService").refreshToken().then(function(a){return"undefined"!=typeof a.error?b:(f--,void e(b.config,d,a.access_token))}),d.promise}if(0==b.status){var d=a.defer();return c.get("SpotifyService").serviceUnavailable(),d.promise}}return b}};return g}]),angular.module("spotmop.settings",[]).config(["$stateProvider",function(a){a.state("settings",{url:"/settings",templateUrl:"app/settings/template.html"}).state("testing",{url:"/testing",templateUrl:"app/settings/testing.template.html"})}]).controller("SettingsController",["$scope","$http","$rootScope","$timeout","MopidyService","SpotifyService","SettingsService","NotifyService","PusherService",function(a,b,c,d,e,f,g,h,i){a.version,a.settings=g.getSettings(),a.currentSubpage="mopidy",a.subpageNavigate=function(b){a.currentSubpage=b},a.authorizeSpotify=function(){f.authorize()},a.refreshSpotifyToken=function(){h.notify("Refreshing token"),f.refreshToken().then(function(){})},a.spotifyLogout=function(){f.logout()},a.upgradeCheck=function(){h.notify("Checking for updates"),g.upgradeCheck().then(function(a){g.setSetting("version",a,"latest"),g.getSetting("version",0,"installed")
- +
diff --git a/mopidy_spotmop/static/app/search/template.html b/mopidy_spotmop/static/app/search/template.html index 7de0f6a..4592d29 100644 --- a/mopidy_spotmop/static/app/search/template.html +++ b/mopidy_spotmop/static/app/search/template.html @@ -49,7 +49,7 @@

dragobj="artist" draggable="false">
- +
@@ -64,6 +64,7 @@

+

Albums

@@ -152,7 +153,7 @@

A dragobj="album" draggable="false">
- +
@@ -177,7 +178,7 @@

- +
diff --git a/src/app/browse/album/controller.js b/src/app/browse/album/controller.js index de92323..b875c15 100644 --- a/src/app/browse/album/controller.js +++ b/src/app/browse/album/controller.js @@ -189,7 +189,9 @@ angular.module('spotmop.browse.album', []) } // this is not strictly accurate, but the only way to get the actual album data is from the track object - $scope.album = response[0].album; + var sourceAlbum = response[0].album; + delete sourceAlbum.images; + $scope.album = sourceAlbum; $scope.album.artists = []; $scope.album.totalTracks = $scope.album.num_tracks; $scope.tracklist = { type: 'localtrack', tracks: response }; @@ -240,7 +242,7 @@ angular.module('spotmop.browse.album', []) // we got images from mopidy! if( albumImages.length > 0 ){ - + $scope.album.images = albumImages; // no mopidy artwork, so get album artwork from LastFM diff --git a/src/app/browse/album/template.html b/src/app/browse/album/template.html index 2ea40a3..96f0632 100644 --- a/src/app/browse/album/template.html +++ b/src/app/browse/album/template.html @@ -5,7 +5,7 @@
- +
diff --git a/src/app/browse/artist/controller.js b/src/app/browse/artist/controller.js index 0c789a4..ccd4d60 100644 --- a/src/app/browse/artist/controller.js +++ b/src/app/browse/artist/controller.js @@ -75,17 +75,21 @@ angular.module('spotmop.browse.artist', []) }); } $scope.playArtistRadio = function(){ - - NotifyService.notify('Playing all top tracks'); + + NotifyService.notify('Starting artist radio (beta)'); // get the artist's top tracks - SpotifyService.getTopTracks( $stateParams.uri ) + SpotifyService.getRecommendations( 5, 0, $scope.artist.id ) .then( function( response ){ + var uris = []; for( var i = 0; i < response.tracks.length; i++ ){ uris.push( response.tracks[i].uri ); } - MopidyService.playTrack( uris, 0 ); + MopidyService.clearCurrentTrackList() + .then( function(){ + MopidyService.playTrack( uris, 0 ); + }); }); } diff --git a/src/app/common/directives.js b/src/app/common/directives.js index 4754733..f1fec35 100644 --- a/src/app/common/directives.js +++ b/src/app/common/directives.js @@ -685,8 +685,11 @@ angular.module('spotmop.directives', []) // when we're told to watch, we watch for changes in the url param (ie sidebar bg) if( $element.attr('watch') ){ $scope.$watch('url', function(newValue, oldValue) { - if (newValue) + if( newValue ){ loadImage(); + }else{ + $element.attr('style', 'background-image: none;'); + } }, true); } @@ -695,7 +698,7 @@ angular.module('spotmop.directives', []) // run the preloader function loadImage(){ - + var fullUrl = ''; /* RE-BUILD THIS TO USE PYTHON/TORNADO BACKEND @@ -934,38 +937,6 @@ angular.module('spotmop.directives', []) }; }) -// get the appropriate sized image -// DEPRECIATED -.filter('thumbnailImage', function(){ - return function( images ){ - - // what if there are no images? then nada - if( images.length <= 0 ) - return false; - - // loop all the images - for( var i = 0; i < images.length; i++){ - var image = images[i]; - - // this is our preferred size - if( image.height >= 200 && image.height <= 300 ){ - return image.url; - - // let's take it a notch up then - }else if( image.height > 300 && image.height <= 500 ){ - return image.url; - - // nope? let's take it a notch down then - }else if( image.height >= 150 && image.height < 200 ){ - return image.url; - } - }; - - // no thumbnail that suits? just get the first (and highest res) one then - return images[0].url; - } -}) - // standardize our images into a large/medium/small structure for template usage .filter('sizedImages', function( SettingsService ){ return function( images ){ @@ -986,6 +957,7 @@ angular.module('spotmop.directives', []) var baseUrl = 'http://'+ SettingsService.getSetting('mopidyhost', window.location.hostname); baseUrl += ':'+ SettingsService.getSetting('mopidyport', '6680') image.url = baseUrl +'/spotmop'+ image.uri; + delete image.uri; if( image.height ){ if( image.height >= 650 ){ diff --git a/src/app/common/tracklist/tracklist.directive.js b/src/app/common/tracklist/tracklist.directive.js index ee46c04..b0995ef 100644 --- a/src/app/common/tracklist/tracklist.directive.js +++ b/src/app/common/tracklist/tracklist.directive.js @@ -254,7 +254,7 @@ angular.module('spotmop.common.tracklist', []) // build an array of track uris (and subtract the first one, as we play him immediately) var selectedTracksUris = []; - for( var i = 1; i < selectedTracks.length; i++ ){ + for( var i = 0; i < selectedTracks.length; i++ ){ selectedTracksUris.push( selectedTracks[i].uri ); }; @@ -264,15 +264,7 @@ angular.module('spotmop.common.tracklist', []) NotifyService.notify( message ); - // play the first track immediately - MopidyService.playTrack( [ firstSelectedTrack.uri ], 0 ).then( function(){ - - // more tracks to add - if( selectedTracksUris.length > 0 ){ - // add the following tracks to the tracklist - MopidyService.addToTrackList( selectedTracksUris, 1 ); - } - }); + MopidyService.playTrack( selectedTracksUris, 0 ); } } diff --git a/src/app/local/controller.js b/src/app/local/controller.js index 6b9de75..26ba3f4 100644 --- a/src/app/local/controller.js +++ b/src/app/local/controller.js @@ -238,7 +238,7 @@ angular.module('spotmop.local', []) // chat with Mopidy and get the images for all these URIs MopidyService.getImages( uris ) .then( function(response){ - + // loop all the response uris for( var key in response ){ @@ -250,7 +250,7 @@ angular.module('spotmop.local', []) var index = $scope.allAlbums.indexOf( albumByUri[0] ); // update the album's images - $scope.allAlbums[index].images = $filter('sizedImages')( response[key] ); + $scope.allAlbums[index].images = response[key]; } } }); diff --git a/src/app/player/service.js b/src/app/player/service.js index 0125e48..05e49ce 100644 --- a/src/app/player/service.js +++ b/src/app/player/service.js @@ -100,7 +100,6 @@ angular.module('spotmop.services.player', []) } // listen for current track changes - // TODO: Move this into the MopidyService for sanity $rootScope.$on('mopidy:event:trackPlaybackStarted', function( event, tlTrack ){ // only if our new tlTrack differs from our current one diff --git a/src/app/search/controller.js b/src/app/search/controller.js index d026463..d80adf5 100644 --- a/src/app/search/controller.js +++ b/src/app/search/controller.js @@ -108,6 +108,7 @@ angular.module('spotmop.search', []) case 'album' : SpotifyService.getSearchResults( 'album', query, 50 ) .then( function(response){ + console.log( response ); $scope.albums = response.albums; if( response.albums.next ) nextOffset = response.albums.offset + response.albums.limit; @@ -145,28 +146,15 @@ angular.module('spotmop.search', []) break; default : - SpotifyService.getSearchResults( 'track', query, 50 ) + SpotifyService.getSearchResults( 'track,album,artist,playlist', query, 50 ) .then( function(response){ + $scope.albums = response.albums; + $scope.artists = response.artists; + $scope.playlists = response.playlists; $scope.tracklist = response.tracks; $scope.tracklist.type = 'track'; $scope.tracklist.tracks = response.tracks.items; }); - - SpotifyService.getSearchResults( 'album', query, 50 ) - .then( function(response){ - $scope.albums = response.albums; - }); - - SpotifyService.getSearchResults( 'artist', query, 50 ) - .then( function(response){ - $scope.artists = response.artists; - }); - - SpotifyService.getSearchResults( 'playlist', query, 50 ) - .then( function(response){ - $scope.playlists = response.playlists; - - }); break; } } diff --git a/src/app/search/template.html b/src/app/search/template.html index 7de0f6a..4592d29 100644 --- a/src/app/search/template.html +++ b/src/app/search/template.html @@ -49,7 +49,7 @@

dragobj="artist" draggable="false">
- +
@@ -64,6 +64,7 @@

+

Albums

@@ -152,7 +153,7 @@

A dragobj="album" draggable="false">
- +
@@ -177,7 +178,7 @@

- +
diff --git a/src/app/services/mopidy/service.js b/src/app/services/mopidy/service.js index 930ed93..d95318c 100755 --- a/src/app/services/mopidy/service.js +++ b/src/app/services/mopidy/service.js @@ -27,10 +27,7 @@ angular.module('spotmop.services.mopidy', [ var args = Array.prototype.slice.call(arguments); var self = thisObj || this; - cfpLoadingBar.start(); - cfpLoadingBar.set(0.25); - executeFunctionByName(functionNameToWrap, self, args).then(function(data) { - cfpLoadingBar.complete(); + executeFunctionByName(functionNameToWrap, self, args).then(function(data){ deferred.resolve(data); }, function(err) { NotifyService.error( err ); @@ -197,23 +194,35 @@ angular.module('spotmop.services.mopidy', [ getState: function() { return wrapMopidyFunc("mopidy.playback.getState", this)(); }, - playTrack: function(newTracklistUris, trackToPlayIndex) { + playTrack: function(trackUris, trackToPlayIndex) { var self = this; - - // add the surrounding tracks (ie the whole tracklist in focus) - // we add this right to the top of the existing tracklist - return self.mopidy.tracklist.add({ uris: newTracklistUris, at_position: 0 }) - .then( function(){ - - // get the new tracklist - return self.mopidy.tracklist.getTlTracks() - .then(function(tlTracks) { - - // save tracklist for later - self.currentTlTracks = tlTracks; - - return self.mopidy.playback.play({ tl_track: tlTracks[trackToPlayIndex] }); - }, consoleError ); + + cfpLoadingBar.start(); + cfpLoadingBar.set(0.25); + + // add the first track immediately + return self.mopidy.tracklist.add({ uris: [ trackUris.shift() ], at_position: 0 }) + + // then play it + .then( function( response ){ + + // make sure we added the track successfully + // this handles failed adds due to geo-blocked spotify and typos in uris, etc + var playTrack = null; + if( response.length > 0 ){ + playTrack = { tlid: response[0].tlid }; + } + + return self.mopidy.playback.play() + + // now add all the remaining tracks + // note the use of .shift() previously altered the array + .then( function(){ + return self.mopidy.tracklist.add({ uris: trackUris, at_position: 1 }) + .then( function(){ + cfpLoadingBar.complete(); + }); + }, consoleError); }, consoleError); }, playTlTrack: function( tlTrack ){ diff --git a/src/app/services/spotify/service.js b/src/app/services/spotify/service.js index 041a2ab..b5c43c1 100644 --- a/src/app/services/spotify/service.js +++ b/src/app/services/spotify/service.js @@ -1427,27 +1427,32 @@ angular.module('spotmop.services.spotify', []) .success(function( response ){ if( type == 'album' ){ - + var readyToResolve = false; var completeAlbums = []; - var batchesRequired = Math.ceil( response.albums.items.length / 20 ); + var batches = []; // batch our requests - Spotify only allows a max of 20 albums per request, d'oh! - for( var batchCounter = 1; batchCounter < batchesRequired; batchCounter++ ){ - - var batch = response.albums.items.splice(0,20); - var albumids = []; + while( response.albums.items.length ){ + batches.push( response.albums.items.splice(0,20) ); + } + + console.log( batches ); + + // now let's process our batches + for( var i = 0; i < batches.length; i++ ){ - // loop all our albums to build a list of all the album ids we need - for( var i = 0; i < 20; i++ ){ - albumids.push( batch[i].id ); + // loop our batch items to get just IDs + var albumids = []; + for( var j = 0; j < batches[i].length; j++ ){ + albumids.push( batches[i][j].id ); }; // go get the albums service.getAlbums( albumids ) .then( function(albums){ completeAlbums = completeAlbums.concat( albums.albums ); - if( batchCounter >= batchesRequired ){ + if( i >= batches.length - 1 ){ response.albums.items = completeAlbums; deferred.resolve( response ); }