diff --git a/app/common/main-controller.js b/app/common/main-controller.js index 3752673..465ee3e 100644 --- a/app/common/main-controller.js +++ b/app/common/main-controller.js @@ -443,27 +443,33 @@ return $sce.trustAsHtml(html); }; - function loadTrackPosition() { - //TODO: HYZ: Unit test + $scope.loadTrackPosition = function () { if (utils.browserStorageCheck()) { // Load Saved Song var song = angular.fromJson(localStorage.getItem('CurrentSong')); if (song) { player.load(song); - // Load Saved Queue - var items = angular.fromJson(localStorage.getItem('CurrentQueue')); - if (items) { - player.queue = items; - if (player.queue.length > 0) { - notifications.updateMessage(player.queue.length + ' Saved Song(s)', true); - } - if (globals.settings.Debug) { console.log('Play Queue Loaded From localStorage: ' + player.queue.length + ' song(s)'); } - } } } else { if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); } } - } + }; + + $scope.loadQueue = function () { + if(utils.browserStorageCheck()) { + // load Saved queue + var queue = angular.fromJson(localStorage.getItem('CurrentQueue')); + if (queue) { + player.queue = queue; + if (player.queue.length > 0) { + notifications.updateMessage(player.queue.length + ' Saved Song(s)', true); + } + if (globals.settings.Debug) { console.log('Play Queue Loaded From localStorage: ' + player.queue.length + ' song(s)'); } + } + } else { + if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); } + } + }; /* Launch on Startup */ $scope.loadSettings(); @@ -476,7 +482,8 @@ if ($scope.loggedIn()) { //$scope.ping(); if (globals.settings.SaveTrackPosition) { - loadTrackPosition(); + $scope.loadQueue(); + $scope.loadTrackPosition(); //FIXME: HYZ: player.startSaveTrackPosition(); } } diff --git a/app/common/main-controller_test.js b/app/common/main-controller_test.js index 045974b..bf6e350 100644 --- a/app/common/main-controller_test.js +++ b/app/common/main-controller_test.js @@ -1,30 +1,132 @@ describe("Main controller", function() { 'use strict'; - describe("updateFavorite -", function() { + var scope, $rootScope, utils, globals, model, notifications, player; + beforeEach(function() { + module('JamStash'); - it("when starring a song, it notifies the user that the star was saved", function() { + inject(function ($controller, _$rootScope_, _$document_, _$window_, _$location_, _$cookieStore_, _utils_, _globals_, _model_, _notifications_, _player_) { + $rootScope = _$rootScope_; + scope = $rootScope.$new(); + utils = _utils_; + globals = _globals_; + model = _model_; + notifications = _notifications_; + player = _player_; + + $controller('AppController', { + $scope: scope, + $rootScope: $rootScope, + $document: _$document_, + $window: _$window_, + $location: _$location_, + $cookieStore: _$cookieStore_, + utils: utils, + globals: globals, + model: model, + notifications: notifications, + player: player + }); + }); + }); + + xdescribe("updateFavorite -", function() { + + xit("when starring a song, it notifies the user that the star was saved", function() { }); - it("when starring an album, it notifies the user that the star was saved", function() { + xit("when starring an album, it notifies the user that the star was saved", function() { }); - it("when starring an artist, it notifies the user that the star was saved", function() { + xit("when starring an artist, it notifies the user that the star was saved", function() { }); - it("given that the Subsonic server returns an error, when starring something, it notifies the user with the error message", function() { + xit("given that the Subsonic server returns an error, when starring something, it notifies the user with the error message", function() { //TODO: move to higher level }); - it("given that the Subsonic server is unreachable, when starring something, it notifies the user with the HTTP error code", function() { + xit("given that the Subsonic server is unreachable, when starring something, it notifies the user with the HTTP error code", function() { //TODO: move to higher level }); }); - describe("toggleSetting -", function() { + xdescribe("toggleSetting -", function() { }); + + describe("load from localStorage -", function() { + var fakeStorage; + beforeEach(function() { + fakeStorage = {}; + + spyOn(localStorage, "getItem").and.callFake(function(key) { + return fakeStorage[key]; + }); + spyOn(utils, "browserStorageCheck").and.returnValue(true); + }); + + describe("loadTrackPosition -", function() { + beforeEach(function() { + spyOn(player, "load"); + }); + + it("Given that we previously saved the current track's position in local Storage, it loads the song we saved into the player", function() { + var song = { + id: 8626, + name: 'Pectinatodenticulate', + artist: 'Isiah Hosfield', + album: 'Tammanyize' + }; + fakeStorage = { + 'CurrentSong': song + }; + + scope.loadTrackPosition(); + + expect(localStorage.getItem).toHaveBeenCalledWith('CurrentSong'); + expect(player.load).toHaveBeenCalledWith(song); + }); + + it("Given that we didn't save anything in local Storage, it doesn't load anything", function() { + scope.loadTrackPosition(); + expect(localStorage.getItem).toHaveBeenCalledWith('CurrentSong'); + expect(player.load).not.toHaveBeenCalled(); + }); + }); + + describe("loadQueue -", function() { + beforeEach(function() { + spyOn(notifications, "updateMessage"); + player.queue = []; + }); + + it("Given that we previously saved the playing queue in local Storage, it fills the player's queue with what we saved and notifies the user", function() { + var queue = [ + { id: 8705 }, + { id: 1617 }, + { id: 9812 } + ]; + fakeStorage = { + 'CurrentQueue': queue + }; + + scope.loadQueue(); + + expect(localStorage.getItem).toHaveBeenCalledWith('CurrentQueue'); + expect(player.queue).toEqual(queue); + expect(notifications.updateMessage).toHaveBeenCalledWith('3 Saved Song(s)', true); + }); + + it("Given that we didn't save anything in local Storage, it doesn't load anything", function() { + scope.loadQueue(); + + expect(localStorage.getItem).toHaveBeenCalledWith('CurrentQueue'); + expect(player.queue).toEqual([]); + expect(notifications.updateMessage).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/app/player/player-directive.js b/app/player/player-directive.js index 9a682d9..a7c584d 100644 --- a/app/player/player-directive.js +++ b/app/player/player-directive.js @@ -1,14 +1,19 @@ -angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstash.settings']) +/** + * jamstash.player.directive module + * + * Encapsulates the jPlayer plugin. It watches the player service for the song to play, load or restart. + * It also enables jPlayer to attach event handlers to our UI through css Selectors. + */ +angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstash.settings', 'jamstash.subsonic.service']) -.directive('jplayer', ['player', 'globals', function(playerService, globals) { +.directive('jplayer', ['player', 'globals', 'subsonic', function(playerService, globals, subsonic) { 'use strict'; return { restrict: 'EA', template: '
', - link: function(scope, element, attrs) { + link: function(scope, element) { var $player = element.children('div'); - console.log($player); var audioSolution = 'html,flash'; if (globals.settings.ForceFlash) { audioSolution = 'flash,html'; @@ -17,7 +22,7 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas var updatePlayer = function() { $player.jPlayer({ // Flash fallback for outdated browser not supporting HTML5 audio/video tags - // http://jplayer.org/download/ + // TODO: Hyz: Replace in Grunt ! swfPath: 'bower_components/jplayer/dist/jplayer/jquery.jplayer.swf', wmode: 'window', solution: audioSolution, @@ -37,8 +42,8 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas }, play: function() { console.log('jplayer play'); - $('#playermiddle').css('visibility', 'visible'); - $('#songdetails').css('visibility', 'visible'); + scope.revealControls(); + scope.scrobbled = false; }, pause: function() { console.log('jplayer pause'); @@ -47,18 +52,37 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas console.log('jplayer ended'); playerService.nextTrack(); scope.$apply(); + }, + timeupdate: function (event) { + // Scrobble song once percentage is reached + var p = event.jPlayer.status.currentPercentAbsolute; + if (!scope.scrobbled && p > 30) { + if (globals.settings.Debug) { console.log('LAST.FM SCROBBLE - Percent Played: ' + p); } + subsonic.scrobble(scope.currentSong); + scope.scrobbled = true; + } } }); }; updatePlayer(); + scope.currentSong = {}; + scope.scrobbled = false; + scope.$watch(function () { return playerService.getPlayingSong(); }, function (newVal) { console.log('playingSong changed !'); - $player.jPlayer('setMedia', {'mp3': newVal.url}) - .jPlayer('play'); + scope.currentSong = newVal; + $player.jPlayer('setMedia', {'mp3': newVal.url}); + if(playerService.loadSong === true) { + // Do not play, only load + playerService.loadSong = false; + scope.revealControls(); + } else { + $player.jPlayer('play'); + } }); scope.$watch(function () { @@ -70,6 +94,12 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas playerService.restartSong = false; } }); + + scope.revealControls = function () { + $('#playermiddle').css('visibility', 'visible'); + $('#songdetails').css('visibility', 'visible'); + }; + } //end link }; }]); diff --git a/app/player/player-directive_test.js b/app/player/player-directive_test.js index 0b3cc71..5c78a1b 100644 --- a/app/player/player-directive_test.js +++ b/app/player/player-directive_test.js @@ -1,27 +1,26 @@ describe("jplayer directive", function() { 'use strict'; - var element, scope, playerService, globalsService, $player, playingSong; - - function mockGetPlayingSong() { - return playingSong; - } + var element, scope, playerService, globals, subsonic, $player, playingSong; beforeEach(function() { playingSong = {}; module('jamstash.player.directive', function($provide) { // Mock the player service $provide.decorator('player', function($delegate) { - $delegate.getPlayingSong = jasmine.createSpy('getPlayingSong').and.callFake(mockGetPlayingSong); + $delegate.getPlayingSong = jasmine.createSpy('getPlayingSong').and.callFake(function() { + return playingSong; + }); $delegate.nextTrack = jasmine.createSpy('nextTrack'); return $delegate; }); }); - inject(function($rootScope, $compile, _player_, _globals_) { + inject(function($rootScope, $compile, _player_, _globals_, _subsonic_) { playerService = _player_; - globalsService = _globals_; + globals = _globals_; + subsonic = _subsonic_; // Compile the directive scope = $rootScope.$new(); element = ''; @@ -31,14 +30,38 @@ describe("jplayer directive", function() { $player = element.children('div'); }); - it("When the player service's current playing song changes, it sets jplayer's media and plays the song", function() { - spyOn($.fn, "jPlayer").and.returnValue($.fn); + describe("When the player service's current song changes,", function() { - playingSong = {url: 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate'}; - scope.$apply(); + beforeEach(function() { + spyOn($.fn, "jPlayer").and.returnValue($.fn); + playingSong = {url: 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate'}; + }); - expect($player.jPlayer).toHaveBeenCalledWith('setMedia', {'mp3': 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate'}); - expect($player.jPlayer).toHaveBeenCalledWith('play'); + it("it sets jPlayer's media and stores the song for future scrobbling", function() { + scope.$apply(); + + expect($player.jPlayer).toHaveBeenCalledWith('setMedia', {'mp3': 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate'}); + expect(scope.currentSong).toEqual(playingSong); + }); + + it("if the player service's loadSong flag is true, it does not play the song and it displays the player controls", function() { + spyOn(scope, "revealControls"); + + playerService.loadSong = true; + scope.$apply(); + + expect($player.jPlayer).not.toHaveBeenCalledWith('play'); + expect(playerService.loadSong).toBeFalsy(); + expect(scope.revealControls).toHaveBeenCalled(); + }); + + it("otherwise, it plays it", function() { + playerService.loadSong = false; + scope.$apply(); + + expect($player.jPlayer).toHaveBeenCalledWith('play'); + expect(playerService.loadSong).toBeFalsy(); + }); }); it("When the player service's restartSong flag is true, it restarts the current song and resets the flag to false", function() { @@ -51,7 +74,7 @@ describe("jplayer directive", function() { expect(playerService.restartSong).toBeFalsy(); }); - it("When jplayer has finished the current song, it plays the next track using the", function() { + it("When jplayer has finished the current song, it asks the player service for the next track", function() { var e = $.jPlayer.event.ended; $player.trigger(e); @@ -59,12 +82,67 @@ describe("jplayer directive", function() { }); it("When jPlayer starts to play the current song, it displays the player controls", function() { - affix('#playermiddle').css('visibility', 'hidden'); - affix('#songdetails').css('visibility', 'hidden'); + spyOn(scope, "revealControls"); + var e = $.jPlayer.event.play; $player.trigger(e); + expect(scope.revealControls).toHaveBeenCalled(); + }); + + it("When jPlayer starts to play the current song, it resets the scrobbled flag to false", function() { + scope.scrobbled = true; + + var e = $.jPlayer.event.play; + $player.trigger(e); + + expect(scope.scrobbled).toBeFalsy(); + }); + + it("revealControls - it displays the song details and the player controls", function() { + affix('#playermiddle').css('visibility', 'hidden'); + affix('#songdetails').css('visibility', 'hidden'); + + scope.revealControls(); + expect($('#playermiddle').css('visibility')).toEqual('visible'); expect($('#songdetails').css('visibility')).toEqual('visible'); }); + + describe("Scrobbling -", function() { + var fakejPlayer, timeUpdate; + beforeEach(function() { + spyOn(subsonic, "scrobble"); + scope.currentSong = { + id: 5375 + }; + + // Fake jPlayer's internal _trigger event because I can't trigger a manual timeupdate + fakejPlayer = { + element: $player, + status: { currentPercentAbsolute: 31 } + }; + timeUpdate = $.jPlayer.event.timeupdate; + }); + + it("Given a song that hasn't been scrobbled yet, When jPlayer reaches 30 percent of it, it scrobbles to last.fm using the subsonic service and sets the flag to true", function() { + scope.scrobbled = false; + + // Trigger our fake timeupdate + $.jPlayer.prototype._trigger.call(fakejPlayer, timeUpdate); + + expect(subsonic.scrobble).toHaveBeenCalledWith(scope.currentSong); + expect(scope.scrobbled).toBeTruthy(); + }); + + it("Given a song that has already been scrobbled, when jPlayer reaches 30 percent of it, it does not scrobble again and leaves the flag to true", function() { + scope.scrobbled = true; + + // Trigger our fake timeupdate + $.jPlayer.prototype._trigger.call(fakejPlayer, timeUpdate); + + expect(subsonic.scrobble).not.toHaveBeenCalled(); + expect(scope.scrobbled).toBeTruthy(); + }); + }); }); diff --git a/app/player/player-service.js b/app/player/player-service.js index ed25174..4ff8493 100644 --- a/app/player/player-service.js +++ b/app/player/player-service.js @@ -1,7 +1,7 @@ /** * jamstash.player.service Module * -* Enables app-wide control of the behavior of the player directive. +* Manages the player and playing queue. Use it to play a song, go to next track or add songs to the queue. */ angular.module('jamstash.player.service', ['jamstash.settings', 'angular-underscore/utils']) @@ -13,9 +13,9 @@ angular.module('jamstash.player.service', ['jamstash.settings', 'angular-undersc playingIndex: -1, playingSong: {}, restartSong: false, + loadSong: false, play: function(song) { - console.log('player service - play()', song); var songIndexInQueue; // Find the song's index in the queue, if it's in there songIndexInQueue = player.queue.indexOf(song); @@ -31,22 +31,20 @@ angular.module('jamstash.player.service', ['jamstash.settings', 'angular-undersc }, playFirstSong: function() { - console.log('player service - playFirstSong()'); player.playingIndex = 0; player.play(player.queue[0]); }, load: function(song) { - console.log('player service - load()'); + player.loadSong = true; + player.play(song); }, restart: function() { - console.log('player service - restart()'); player.restartSong = true; }, nextTrack: function() { - console.log('player service - nextTrack()'); if((player.playingIndex + 1) < player.queue.length) { var nextTrack = player.queue[player.playingIndex + 1]; player.playingIndex++; @@ -55,7 +53,6 @@ angular.module('jamstash.player.service', ['jamstash.settings', 'angular-undersc }, previousTrack: function() { - console.log(('player service - previousTrack()')); if((player.playingIndex - 1) > 0) { var previousTrack = player.queue[player.playingIndex - 1]; player.playingIndex--; @@ -66,22 +63,18 @@ angular.module('jamstash.player.service', ['jamstash.settings', 'angular-undersc }, emptyQueue: function() { - console.log('player service - emptyQueue()'); player.queue = []; }, shuffleQueue: function() { - console.log('player service - shuffleQueue()'); player.queue = _.shuffle(player.queue); }, addSong: function(song) { - console.log('player service - addSong()'); player.queue.push(song); }, removeSong: function(song) { - console.log('player service - removeSong()'); var index = player.queue.indexOf(song); player.queue.splice(index, 1); }, diff --git a/app/player/player-service_test.js b/app/player/player-service_test.js index 0246a00..799d1d9 100644 --- a/app/player/player-service_test.js +++ b/app/player/player-service_test.js @@ -180,9 +180,18 @@ describe("Player service -", function() { player.restart(); expect(player.restartSong).toBeTruthy(); }); + + it("When I load the song, the flag for the directive is set", function() { + spyOn(player, "play"); + + player.load(song); + + expect(player.loadSong).toBeTruthy(); + expect(player.play).toHaveBeenCalledWith(song); + }); }); - describe("Given that there is no song in my playing queue", function() { + describe("Given that my playing queue is empty", function() { beforeEach(function() { player.queue = []; diff --git a/app/player/player.html b/app/player/player.html index 1921aaa..f3646c2 100644 --- a/app/player/player.html +++ b/app/player/player.html @@ -5,10 +5,10 @@