diff --git a/.jshintrc b/.jshintrc index e7e5d2b..dc1d78b 100644 --- a/.jshintrc +++ b/.jshintrc @@ -18,6 +18,7 @@ "smarttabs": true, "globals": { "_": false, + "affix": false, "after": false, "afterEach": false, "angular": false, @@ -32,6 +33,6 @@ "spyOn": false }, "browser": true, - "node": true, - "jquery": true + "jquery": true, + "node": true } diff --git a/app/app.js b/app/app.js index 66085f1..5af6e04 100755 --- a/app/app.js +++ b/app/app.js @@ -1,54 +1,51 @@ - -/* Declare app level module */ -angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', - 'jamstash.subsonic.ctrl', 'jamstash.archive.ctrl']) - -.config(['$routeProvider',function($routeProvider) { - 'use strict'; - - $routeProvider - .when('/index', { redirectTo: '/library' }) - .when('/settings', { templateUrl: 'settings/settings.html', controller: 'SettingsCtrl' }) - .when('/queue', { templateUrl: 'queue/queue.html', controller: 'QueueCtrl' }) - .when('/library', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicCtrl' }) - .when('/library/:artistId', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicCtrl', reloadOnSearch: false }) - .when('/library/:artistId/:albumId', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicCtrl', reloadOnSearch: false }) - .when('/podcasts', { templateUrl: 'podcasts/podcasts.html', controller: 'PodcastCtrl' }) - .when('/archive', { templateUrl: 'archive/archive.html', controller: 'ArchiveCtrl' }) - .when('/archive/:artist', { templateUrl: 'archive/archive.html', controller: 'ArchiveCtrl' }) - .when('/archive/:artist/:album', { templateUrl: 'archive/archive.html', controller: 'ArchiveCtrl' }) - .otherwise({ redirectTo: '/index' }); -}]) - -.config(['$httpProvider',function($httpProvider) { - 'use strict'; - - $httpProvider.interceptors.push(['$rootScope', '$location', '$q', 'globals', function ($rootScope, $location, $q, globals) { - return { - 'request': function (request) { - // if we're not logged-in to the AngularJS app, redirect to login page - //$rootScope.loggedIn = $rootScope.loggedIn || globals.settings.Username; - $rootScope.loggedIn = false; - if (globals.settings.Username != "" && globals.settings.Password != "" && globals.settings.Server != "") { - $rootScope.loggedIn = true; - } - var path = ''; - path = $location.path(); - if (globals.settings.Debug) { console.log('Logged In: ' + $rootScope.loggedIn); } - if (globals.settings.Debug) { console.log('Current Path: ' + path); } - if (!$rootScope.loggedIn && path != '/settings' && path.search('archive') < 0) { - $location.path('/settings'); - } - return request; - }, - 'responseError': function (rejection) { - // if we're not logged-in to the web service, redirect to login page - if (rejection.status === 401 && $location.path() != '/settings') { - $rootScope.loggedIn = false; - $location.path('/settings'); - } - return $q.reject(rejection); - } - }; - }]); -}]); \ No newline at end of file +'use strict'; + +/* Declare app level module */ +angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', + 'jamstash.subsonic.controller', 'jamstash.archive.controller', 'jamstash.player.controller', 'jamstash.queue.controller', 'jamstash.persistence']) + +.config(['$routeProvider',function($routeProvider) { + $routeProvider + .when('/index', { redirectTo: '/library' }) + .when('/settings', { templateUrl: 'settings/settings.html', controller: 'SettingsController' }) + .when('/queue', { templateUrl: 'queue/queue.html', controller: 'QueueController' }) + .when('/library', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicController' }) + .when('/library/:artistId', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicController', reloadOnSearch: false }) + .when('/library/:artistId/:albumId', { templateUrl: 'subsonic/subsonic.html', controller: 'SubsonicController', reloadOnSearch: false }) + .when('/podcasts', { templateUrl: 'podcasts/podcasts.html', controller: 'PodcastController' }) + .when('/archive', { templateUrl: 'archive/archive.html', controller: 'ArchiveController' }) + .when('/archive/:artist', { templateUrl: 'archive/archive.html', controller: 'ArchiveController' }) + .when('/archive/:artist/:album', { templateUrl: 'archive/archive.html', controller: 'ArchiveController' }) + .otherwise({ redirectTo: '/index' }); +}]) + +.config(['$httpProvider',function($httpProvider) { + $httpProvider.interceptors.push(['$rootScope', '$location', '$q', 'globals', function ($rootScope, $location, $q, globals) { + return { + 'request': function (request) { + // if we're not logged-in to the AngularJS app, redirect to login page + //$rootScope.loggedIn = $rootScope.loggedIn || globals.settings.Username; + $rootScope.loggedIn = false; + if (globals.settings.Username != "" && globals.settings.Password != "" && globals.settings.Server != "") { + $rootScope.loggedIn = true; + } + var path = ''; + path = $location.path(); + if (globals.settings.Debug) { console.log('Logged In: ' + $rootScope.loggedIn); } + if (globals.settings.Debug) { console.log('Current Path: ' + path); } + if (!$rootScope.loggedIn && path != '/settings' && path.search('archive') < 0) { + $location.path('/settings'); + } + return request; + }, + 'responseError': function (rejection) { + // if we're not logged-in to the web service, redirect to login page + if (rejection.status === 401 && $location.path() != '/settings') { + $rootScope.loggedIn = false; + $location.path('/settings'); + } + return $q.reject(rejection); + } + }; + }]); +}]); diff --git a/app/archive/archive-service.js b/app/archive/archive-service.js index 141359d..d20b42a 100644 --- a/app/archive/archive-service.js +++ b/app/archive/archive-service.js @@ -3,10 +3,11 @@ * * Access Archive.org */ -angular.module('jamstash.archive.service', ['jamstash.settings', 'jamstash.model', 'jamstash.notifications']) +angular.module('jamstash.archive.service', ['jamstash.settings', 'jamstash.model', 'jamstash.notifications', + 'jamstash.player.service']) -.factory('archive', ['$rootScope', '$http', '$q', '$sce', 'globals', 'model', 'utils', 'map', 'notifications', - function($rootScope, $http, $q, $sce, globals, model, utils, map, notifications) { +.factory('archive', ['$rootScope', '$http', '$q', '$sce', 'globals', 'model', 'utils', 'map', 'notifications', 'player', + function($rootScope, $http, $q, $sce, globals, model, utils, map, notifications, player) { 'use strict'; var index = { shortcuts: [], artists: [] }; @@ -179,20 +180,20 @@ angular.module('jamstash.archive.service', ['jamstash.settings', 'jamstash.model angular.forEach(items, function (item, key) { var song = mapSong(key, item, server, dir, identifier, coverart); if (song) { - $rootScope.queue.push(song); + player.queue.push(song); } }); notifications.updateMessage(Object.keys(items).length + ' Song(s) Added to Queue', true); } else if (action == 'play') { - $rootScope.queue = []; + player.queue = []; angular.forEach(items, function (item, key) { var song = mapSong(key, item, server, dir, identifier, coverart); if (song) { - $rootScope.queue.push(song); + player.queue.push(song); } }); - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); + var next = player.queue[0]; + player.play(next); notifications.updateMessage(Object.keys(items).length + ' Song(s) Added to Queue', true); } else { content.album = []; @@ -213,4 +214,4 @@ angular.module('jamstash.archive.service', ['jamstash.settings', 'jamstash.model return deferred.promise; } }; -}]); \ No newline at end of file +}]); diff --git a/app/archive/archive.js b/app/archive/archive.js index fd6592b..fe12d8a 100644 --- a/app/archive/archive.js +++ b/app/archive/archive.js @@ -1,11 +1,11 @@ -/** -* jamstash.archive.ctrl Module +/** +* jamstash.archive.controller Module * * Access Archive.org */ -angular.module('jamstash.archive.ctrl', ['jamstash.archive.service']) +angular.module('jamstash.archive.controller', ['jamstash.archive.service']) -.controller('ArchiveCtrl', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$timeout', 'utils', 'globals', 'model', 'notifications', 'player', 'archive', 'json', +.controller('ArchiveController', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$timeout', 'utils', 'globals', 'model', 'notifications', 'player', 'archive', 'json', function($scope, $rootScope, $location, $routeParams, $http, $timeout, utils, globals, model, notifications, player, archive, json){ 'use strict'; @@ -179,4 +179,4 @@ angular.module('jamstash.archive.ctrl', ['jamstash.archive.service']) $scope.addSavedCollection($routeParams.artist); } /* End Startup */ -}]); \ No newline at end of file +}]); diff --git a/app/common/main-controller.js b/app/common/main-controller.js index 377db03..2418001 100644 --- a/app/common/main-controller.js +++ b/app/common/main-controller.js @@ -1,18 +1,18 @@ -angular.module('JamStash') -.controller('AppCtrl', ['$scope', '$rootScope', '$document', '$window', '$location', '$cookieStore', '$http', 'utils', 'globals', 'model', 'notifications', 'player', - function($scope, $rootScope, $document, $window, $location, $cookieStore, $http, utils, globals, model, notifications, player) { +angular.module('JamStash') +.controller('AppController', ['$scope', '$rootScope', '$document', '$window', '$location', '$cookieStore', '$http', 'utils', 'globals', 'model', 'notifications', 'player', 'persistence', 'Page', + function($scope, $rootScope, $document, $window, $location, $cookieStore, $http, utils, globals, model, notifications, player, persistence, Page) { 'use strict'; $rootScope.settings = globals.settings; $rootScope.song = []; - $rootScope.queue = []; $rootScope.playingSong = null; $rootScope.MusicFolders = []; $rootScope.Genres = []; $rootScope.Messages = []; - + $rootScope.SelectedMusicFolder = ""; $rootScope.unity = null; + $scope.Page = Page; $rootScope.loggedIn = function () { if (globals.settings.Server !== '' && globals.settings.Username !== '' && globals.settings.Password !== '') { return true; @@ -27,13 +27,6 @@ $rootScope.go = function (path) { $location.path(path); }; - /* - $scope.playSong = function (loadonly, data) { - $scope.$apply(function () { - $rootScope.playSong(loadonly, data); - }); - } - */ // Reads cookies and sets globals.settings values $scope.loadSettings = function () { @@ -109,18 +102,14 @@ } }; - $scope.$watchCollection('queue', function(newItem, oldItem) { - if (oldItem.length != newItem.length - && globals.settings.ShowQueue) { - $rootScope.showQueue(); + $scope.$watchCollection(function () { + return player.queue; + }, function(newQueue) { + if (newQueue !== undefined && newQueue.length > 0 && globals.settings.ShowQueue) { + $scope.showQueue(); } - /* - for (var index in newCol) { - var item = newCol[index]; - item.order = parseInt(index) + 1; - } - */ }); + $rootScope.showQueue = function () { $('#SideBar').css('display', 'block'); $('#right-component').removeClass('lgcolumn_expanded'); @@ -130,14 +119,7 @@ $('#right-component').addClass('lgcolumn_expanded'); }; $scope.toggleQueue = function () { - if ($('#SideBar').css('display') == 'none') { - $rootScope.showQueue(); - } else { - $rootScope.hideQueue(); - } - }; - $scope.toggleQueue = function () { - if ($('#SideBar').css('display') == 'none') { + if ($('#SideBar').css('display') === 'none') { $rootScope.showQueue(); } else { $rootScope.hideQueue(); @@ -160,16 +142,22 @@ }; $scope.fancyboxOpenImage = function (url) { - utils.fancyboxOpenImage(url); + $.fancybox.open({ + helpers : { + overlay : { + css : { + 'background' : 'rgba(0, 0, 0, 0.15)' + } + } + }, + hideOnContentClick: true, + type: 'image', + openEffect: 'none', + closeEffect: 'none', + href: url + }); }; - $('#audiocontainer .scrubber').mouseover(function (e) { - $('.audiojs .scrubber').stop().animate({ height: '8px' }); - }); - $('#audiocontainer .scrubber').mouseout(function (e) { - $('.audiojs .scrubber').stop().animate({ height: '4px' }); - }); - $(document).on("click", ".message", function(){ $(this).remove(); }); @@ -177,23 +165,13 @@ // Global Functions window.onbeforeunload = function () { if (!globals.settings.Debug) { - if ($rootScope.queue.length > 0) { + if (player.queue.length > 0) { return "You're about to end your session, are you sure?"; } } }; $rootScope.showIndex = false; - $scope.dragStart = function (e, ui) { - ui.item.data('start', ui.item.index()); - }; - $scope.dragEnd = function (e, ui) { - var start = ui.item.data('start'), - end = ui.item.index(); - $rootScope.queue.splice(end, 0, - $rootScope.queue.splice(start, 1)[0]); - $scope.$apply(); - }; - $(document).on( 'click', 'message', function() { + $(document).on( 'click', 'message', function() { $(this).fadeOut(function () { $(this).remove(); }); return false; }) @@ -226,9 +204,9 @@ $('#left-component').stop().scrollTo(el, 400); } } else if (unicode == 39 || unicode == 176) { // right arrow - $rootScope.nextTrack(); + player.nextTrack(); } else if (unicode == 37 || unicode == 177) { // back arrow - $rootScope.previousTrack(); + player.previousTrack(); } else if (unicode == 32 || unicode == 179 || unicode.toString() == '0179') { // spacebar player.playPauseSong(); return false; @@ -282,13 +260,15 @@ }); }; $rootScope.playAll = function (songs) { - $rootScope.queue = []; + // TODO: Hyz: Replace + player.queue = []; $rootScope.selectAll(songs); $rootScope.addSongsToQueue(); - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); + var next = player.queue[0]; + player.play(next); }; $rootScope.playFrom = function (index, songs) { + // TODO: Hyz: Replace var from = songs.slice(index,songs.length); $scope.selectedSongs = []; angular.forEach(from, function (item, key) { @@ -296,33 +276,28 @@ item.selected = true; }); if ($scope.selectedSongs.length > 0) { - $rootScope.queue = []; + player.queue = []; $rootScope.addSongsToQueue(); - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); + var next = player.queue[0]; + player.play(next); } }; $rootScope.addSongsToQueue = function () { + // TODO: Hyz: Replace if ($scope.selectedSongs.length !== 0) { angular.forEach($scope.selectedSongs, function (item, key) { - $rootScope.queue.push(item); + player.queue.push(item); item.selected = false; }); notifications.updateMessage($scope.selectedSongs.length + ' Song(s) Added to Queue', true); $scope.selectedSongs.length = 0; } - }; - $scope.addSongToQueue = function (data) { - $rootScope.queue.push(data); }; $rootScope.removeSong = function (item, songs) { + // TODO: Hyz: Replace var index = songs.indexOf(item); songs.splice(index, 1); }; - $scope.removeSongFromQueue = function (item) { - var index = $rootScope.queue.indexOf(item) - $rootScope.queue.splice(index, 1); - }; $scope.isActive = function (route) { return route === $location.path(); }; @@ -351,33 +326,6 @@ } }); }; - $scope.queueRemoveSelected = function (data, event) { - angular.forEach($scope.selectedSongs, function (item, key) { - var index = $rootScope.queue.indexOf(item); - if (index > -1) { - $rootScope.queue.splice(index, 1); - } - }); - }; - $scope.queueEmpty = function () { - //self.selectedSongs([]); - $rootScope.queue = []; - $.fancybox.close(); - }; - $scope.queueTotal = function () { - var total = 0; - utils.arrayForEach(self.queue(), function (item) { - total += parseInt(item.duration()); - }); - if (self.queue().length > 0) { - return self.queue().length + ' song(s), ' + utils.secondsToTime(total) + ' total time'; - } else { - return '0 song(s), 00:00:00 total time'; - } - }; - $scope.queueShuffle = function () { - $rootScope.queue.sort(function () { return 0.5 - Math.random(); }); - }; $scope.selectedSongs = []; $scope.selectSong = function (data) { var i = $scope.selectedSongs.indexOf(data); @@ -417,6 +365,7 @@ } }); }; + $scope.updateFavorite = function (item) { var id = item.id; var starred = item.starred; @@ -441,7 +390,6 @@ $scope.toTrusted = function (html) { return $sce.trustAsHtml(html); }; - /* Launch on Startup */ $scope.loadSettings(); @@ -454,8 +402,8 @@ if ($scope.loggedIn()) { //$scope.ping(); if (globals.settings.SaveTrackPosition) { - player.loadTrackPosition(); - player.startSaveTrackPosition(); + persistence.loadQueue(); + persistence.loadTrackPosition(); } } /* End Startup */ diff --git a/app/common/main-controller_test.js b/app/common/main-controller_test.js new file mode 100644 index 0000000..73655d7 --- /dev/null +++ b/app/common/main-controller_test.js @@ -0,0 +1,81 @@ +describe("Main controller", function() { + 'use strict'; + + var scope, mockGlobals, player, utils; + beforeEach(function() { + mockGlobals = { + settings: { + ShowQueue: false, + Debug: true + } + }; + + module('JamStash', function($provide) { + $provide.value('globals', mockGlobals); + }); + + inject(function ($controller, $rootScope, _$document_, _$window_, _$location_, _$cookieStore_, _utils_, globals, _model_, _notifications_, _player_, _locker_, _Page_) { + scope = $rootScope.$new(); + player = _player_; + utils = _utils_; + + spyOn(utils, "switchTheme"); + + $controller('AppController', { + $scope: scope, + $rootScope: $rootScope, + $document: _$document_, + $window: _$window_, + $location: _$location_, + $cookieStore: _$cookieStore_, + utils: utils, + globals: globals, + model: _model_, + notifications: _notifications_, + player: player, + locker: _locker_, + Page: _Page_ + }); + }); + player.queue = []; + }); + + xdescribe("updateFavorite -", function() { + + xit("when starring a song, 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() { + + }); + + xit("when starring an artist, it notifies the user that the star was saved", 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 + }); + + 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 + }); + }); + + xdescribe("toggleSetting -", function() { + + }); + + it("given that the global setting 'ShowQueue' is true, when the playing queue's length changes and is not empty, it shows the queue", function() { + mockGlobals.settings.ShowQueue = true; + player.queue = [{ + id: 684 + }]; + spyOn(scope, "showQueue"); + + scope.$apply(); + + expect(scope.showQueue).toHaveBeenCalled(); + }); +}); diff --git a/app/common/notification-service.js b/app/common/notification-service.js index 45a53fe..98cb152 100644 --- a/app/common/notification-service.js +++ b/app/common/notification-service.js @@ -3,12 +3,12 @@ * * Provides access to the notification UI. */ -angular.module('jamstash.notifications', []) +angular.module('jamstash.notifications', ['jamstash.player.service', 'jamstash.utils']) -.service('notifications', ['$rootScope', 'globals', function($rootScope, globals) { +.service('notifications', ['$rootScope', '$window', '$interval', 'globals', 'player', 'utils', + function($rootScope, $window, $interval, globals, player, utils) { 'use strict'; - var msgIndex = 1; this.updateMessage = function (msg, autohide) { if (msg !== '') { var id = $rootScope.Messages.push(msg) - 1; @@ -21,47 +21,34 @@ angular.module('jamstash.notifications', []) } }; this.requestPermissionIfRequired = function () { - if (window.Notify.isSupported() && window.Notify.needsPermission()) { + if (this.isSupported() && !this.hasPermission()) { window.Notify.requestPermission(); } }; - this.hasNotificationPermission = function () { - return (window.Notify.needsPermission() === false); + this.hasPermission = function () { + return !$window.Notify.needsPermission(); }; - this.hasNotificationSupport = function () { + this.isSupported = function () { return window.Notify.isSupported(); }; - var notifications = []; - this.showNotification = function (pic, title, text, type, bind) { - if (this.hasNotificationPermission()) { - //closeAllNotifications() - var settings = {}; - if (bind = '#NextTrack') { - settings.notifyClick = function () { - $rootScope.nextTrack(); + this.showNotification = function (song) { + if (this.hasPermission()) { + var notification = new Notify(utils.toHTML.un(song.name), { + body: utils.toHTML.un(song.artist + " - " + song.album), + icon: song.coverartthumb, + notifyClick: function () { + player.nextTrack(); this.close(); - }; - } - if (type === 'text') { - settings.body = text; - settings.icon = pic; - } else if (type === 'html') { - settings.body = text; - } - var notification = new Notify(title, settings); - notifications.push(notification); - setTimeout(function (notWin) { - notWin.close(); - }, globals.settings.Timeout, notification); + $rootScope.$apply(); + } + }); + $interval(function() { + notification.close(); + }, globals.settings.Timeout); notification.show(); } else { console.log("showNotification: No Permission"); } }; - this.closeAllNotifications = function () { - for (var notification in notifications) { - notifications[notification].close(); - } - }; -}]); \ No newline at end of file +}]); diff --git a/app/common/notification-service_test.js b/app/common/notification-service_test.js new file mode 100644 index 0000000..ac5e569 --- /dev/null +++ b/app/common/notification-service_test.js @@ -0,0 +1,108 @@ +describe("Notifications service - ", function() { + 'use strict'; + + var notifications, $window, $interval, player, utils, mockGlobals, + NotificationObj; + beforeEach(function() { + mockGlobals = { + settings: { + Timeout: 30000 + } + }; + + module('jamstash.notifications', function ($provide) { + $provide.value('globals', mockGlobals); + }); + + inject(function (_notifications_, _$window_, _$interval_, _player_, _utils_) { + notifications = _notifications_; + $window = _$window_; + player = _player_; + utils = _utils_; + $interval = _$interval_; + }); + + spyOn(player, "nextTrack"); + spyOn(utils.toHTML, "un").and.callFake(function (arg) { return arg; }); + + // Mock the Notify object + $window.Notify = jasmine.createSpyObj("Notify", ["isSupported", "needsPermission", "requestPermission"]); + NotificationObj = jasmine.createSpyObj("Notification", ["show", "close"]); + spyOn($window, "Notify").and.callFake(function (title, settings) { + NotificationObj.simulateClick = settings.notifyClick; + return NotificationObj; + }); + }); + + it("can check whether we have the permission to display notifications in the current browser", function() { + $window.Notify.needsPermission.and.returnValue(false); + + expect(notifications.hasPermission()).toBeTruthy(); + expect($window.Notify.needsPermission).toHaveBeenCalled(); + }); + + it("can check whether the current browser supports notifications", function() { + $window.Notify.isSupported.and.returnValue(true); + + expect(notifications.isSupported()).toBeTruthy(); + expect($window.Notify.isSupported).toHaveBeenCalled(); + }); + + it("can request Notification permission for the current browser", function() { + spyOn(notifications, "isSupported").and.returnValue(true); + spyOn(notifications, "hasPermission").and.returnValue(false); + + notifications.requestPermissionIfRequired(); + + expect($window.Notify.requestPermission).toHaveBeenCalled(); + }); + + describe("When I show a notification, given a song,", function() { + var song; + beforeEach(function() { + song = { + coverartthumb: "https://backjaw.com/overquantity/outpitch?a=redredge&b=omnivoracious#promotement", + name: "Unhorny", + artist: "Saturnina Koster", + album: "Trepidate" + }; + spyOn(notifications, "hasPermission").and.returnValue(true); + }); + it("it checks the permissions, displays the title, the artist's name and the album picture in a notification", function() { + notifications.showNotification(song); + + expect(notifications.hasPermission).toHaveBeenCalled(); + expect($window.Notify).toHaveBeenCalledWith(song.name, { + body: song.artist + " - " + song.album, + icon: song.coverartthumb, + notifyClick: jasmine.any(Function) + }); + expect(NotificationObj.show).toHaveBeenCalled(); + }); + + it("when I click on it, it plays the next track of the queue", function() { + notifications.showNotification(song); + NotificationObj.simulateClick(); + + expect(player.nextTrack).toHaveBeenCalled(); + expect(NotificationObj.close).toHaveBeenCalled(); + }); + + it("given that the global Timeout setting is set to 10 seconds, it closes itself after 10 seconds", function() { + mockGlobals.settings.Timeout = 10000; + + notifications.showNotification(song); + $interval.flush(10001); + + expect(NotificationObj.close).toHaveBeenCalled(); + }); + + it("if we don't have the permission to display notifications, nothing happens", function() { + notifications.hasPermission.and.returnValue(false); + + notifications.showNotification(song); + + expect(NotificationObj.show).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/common/page-service.js b/app/common/page-service.js new file mode 100644 index 0000000..89747da --- /dev/null +++ b/app/common/page-service.js @@ -0,0 +1,64 @@ +/** +* jamstash.page Module +* +* Set the page's title from anywhere, the angular way +*/ +angular.module('jamstash.page', ['jamstash.settings', 'jamstash.utils']) + +.factory('Page', ['$interval', 'globals', 'utils', function($interval, globals, utils){ + 'use strict'; + + var title = 'Jamstash'; + var timer; + return { + title: function() { return title; }, + setTitle: function(newTitle) { + title = newTitle; + return this; + }, + setTitleSong: function(song) { + if (song.artist !== undefined && song.name !== undefined) { + title = utils.toHTML.un(song.artist) + " - " + utils.toHTML.un(song.name); + } else { + title = 'Jamstash'; + } + if (globals.settings.ScrollTitle) { + this.scrollTitle(); + } + + return this; + }, + scrollTitle: function() { + var shift = { + "left": function (a) { + a.push(a.shift()); + }, + "right": function (a) { + a.unshift(a.pop()); + } + }; + var opts = { + text: title, + dir: "left", + speed: 1200 + }; + + var t = (opts.text).split(""); + if (!t) { + return; + } + t.push(" "); + if (timer !== undefined) { + $interval.cancel(timer); + } + timer = $interval(function () { + var f = shift[opts.dir]; + if (f) { + f(t); + title = t.join(""); + } + }, opts.speed); + return this; + } + }; +}]); diff --git a/app/common/page-service_test.js b/app/common/page-service_test.js new file mode 100644 index 0000000..b1994d2 --- /dev/null +++ b/app/common/page-service_test.js @@ -0,0 +1,54 @@ +describe("Page service", function() { + 'use strict'; + + var mockGlobals, Page, utils, $interval; + beforeEach(function() { + + mockGlobals = { + settings: { + ScrollTitle: false + } + }; + + module('jamstash.page', function ($provide) { + $provide.value('globals', mockGlobals); + }); + + inject(function (_Page_, _utils_, _$interval_) { + Page = _Page_; + utils = _utils_; + $interval = _$interval_; + }); + spyOn(utils.toHTML, "un").and.callFake(function (arg) { return arg; }); + }); + + describe("Given a song,", function() { + var song; + beforeEach(function() { + song = { + artist: 'Merlyn Nurse', + name: 'Exsiccator tumble' + }; + }); + + it("it displays its artist and its name as the page's title", function() { + Page.setTitleSong(song); + expect(Page.title()).toBe('Merlyn Nurse - Exsiccator tumble'); + }); + + it("if the global setting 'ScrollTitle' is true, it scrolls the page title", function() { + spyOn(Page, "scrollTitle"); + mockGlobals.settings.ScrollTitle = true; + + Page.setTitleSong(song); + + expect(Page.scrollTitle).toHaveBeenCalled(); + }); + }); + + it("Given a title, it can scroll it", function() { + Page.setTitle('unbeloved omnificent supergravitate').scrollTitle(); + $interval.flush(1201); + expect(Page.title()).toBe('nbeloved omnificent supergravitate u'); + }); +}); diff --git a/app/common/persistence-service.js b/app/common/persistence-service.js new file mode 100644 index 0000000..a77cded --- /dev/null +++ b/app/common/persistence-service.js @@ -0,0 +1,58 @@ +'use strict'; +/** +* jamstash.persistence Module +* +* Provides load, save and delete operations for the current song and queue. +* Data storage provided by HTML5 localStorage. +*/ +angular.module('jamstash.persistence', ['jamstash.settings', 'jamstash.player.service', 'jamstash.notifications', 'angular-locker']) + +.config(['lockerProvider', function (lockerProvider) { + lockerProvider.setDefaultDriver('local') + .setDefaultNamespace('jamstash') + .setEventsEnabled(false); +}]) + +.service('persistence', ['globals', 'player', 'notifications', 'locker', function(globals, player, notifications, locker){ + this.loadTrackPosition = function () { + // Load Saved Song + var song = locker.get('CurrentSong'); + if (song) { + player.load(song); + } + if (globals.settings.Debug) { console.log('Current Position Loaded from localStorage: ', song); } + }; + + this.saveTrackPosition = function (song) { + locker.put('CurrentSong', song); + if (globals.settings.Debug) { console.log('Saving Current Position: ', song); } + }; + + this.deleteTrackPosition = function () { + locker.forget('CurrentSong'); + if (globals.settings.Debug) { console.log('Removing Current Position from localStorage'); } + }; + + this.loadQueue = function () { + // load Saved queue + var queue = locker.get('CurrentQueue'); + if (queue) { + player.addSongs(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)'); } + } + }; + + this.saveQueue = function () { + locker.put('CurrentQueue', player.queue); + if (globals.settings.Debug) { console.log('Saving Queue: ' + player.queue.length + ' songs'); } + }; + + this.deleteQueue = function () { + locker.forget('CurrentQueue'); + if (globals.settings.Debug) { console.log('Removing Play Queue from localStorage'); } + }; +}]); + diff --git a/app/common/persistence-service_test.js b/app/common/persistence-service_test.js new file mode 100644 index 0000000..4bb9d48 --- /dev/null +++ b/app/common/persistence-service_test.js @@ -0,0 +1,126 @@ +describe("Persistence service", function() { + 'use strict'; + + var persistence, player, notifications, locker; + var song; + beforeEach(function() { + module('jamstash.persistence'); + + inject(function (_persistence_, _player_, _notifications_, _locker_) { + persistence = _persistence_; + player = _player_; + notifications = _notifications_; + locker = _locker_; + }); + + song = { + id: 8626, + name: 'Pectinatodenticulate', + artist: 'Isiah Hosfield', + album: 'Tammanyize' + }; + player.queue = []; + }); + + describe("load from localStorage -", function() { + var fakeStorage; + beforeEach(function() { + fakeStorage = {}; + + spyOn(locker, "get").and.callFake(function(key) { + return fakeStorage[key]; + }); + }); + + 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() { + fakeStorage = { 'CurrentSong': song }; + + persistence.loadTrackPosition(); + + expect(locker.get).toHaveBeenCalledWith('CurrentSong'); + expect(player.load).toHaveBeenCalledWith(song); + }); + + it("Given that we didn't save anything in local Storage, it doesn't load anything", function() { + persistence.loadTrackPosition(); + expect(locker.get).toHaveBeenCalledWith('CurrentSong'); + expect(player.load).not.toHaveBeenCalled(); + }); + }); + + describe("loadQueue -", function() { + beforeEach(function() { + spyOn(notifications, "updateMessage"); + spyOn(player, "addSongs").and.callFake(function (songs) { + // Update the queue length so that notifications work + player.queue.length += songs.length; + }); + }); + + 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 }; + + persistence.loadQueue(); + + expect(locker.get).toHaveBeenCalledWith('CurrentQueue'); + expect(player.addSongs).toHaveBeenCalledWith(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() { + persistence.loadQueue(); + + expect(locker.get).toHaveBeenCalledWith('CurrentQueue'); + expect(player.addSongs).not.toHaveBeenCalled(); + expect(notifications.updateMessage).not.toHaveBeenCalled(); + }); + }); + }); + + describe("save from localStorage -", function() { + beforeEach(function() { + spyOn(locker, "put"); + }); + + it("it saves the current track's position in local Storage", function() { + persistence.saveTrackPosition(song); + expect(locker.put).toHaveBeenCalledWith('CurrentSong', song); + }); + + it("it saves the playing queue in local Storage", function() { + player.queue = [ + { id: 1245 }, + { id: 7465 }, + { id: 948 } + ]; + persistence.saveQueue(); + expect(locker.put).toHaveBeenCalledWith('CurrentQueue', player.queue); + }); + }); + + describe("remove from localStorage -", function() { + beforeEach(function() { + spyOn(locker, "forget"); + }); + + it("it deletes the current track from local Storage", function() { + persistence.deleteTrackPosition(); + expect(locker.forget).toHaveBeenCalledWith('CurrentSong'); + }); + + it("it deletes the saved playing queue from local Storage", function() { + persistence.deleteQueue(); + expect(locker.forget).toHaveBeenCalledWith('CurrentQueue'); + }); + }); +}); diff --git a/app/common/player-service.js b/app/common/player-service.js deleted file mode 100644 index c0db6d7..0000000 --- a/app/common/player-service.js +++ /dev/null @@ -1,415 +0,0 @@ -angular.module('JamStash') - -.service('player', ['$rootScope', '$window', 'utils', 'globals', 'model', 'notifications', - function ($rootScope, $window, utils, globals, model, notifications) { - - 'use strict'; - var player1 = globals.Player1; - var player2 = '#playdeck_2'; - var scrobbled = false; - var timerid = 0; - $rootScope.defaultPlay = function (data, event) { - if (typeof $(player1).data("jPlayer") == 'undefined') { - $rootScope.nextTrack(); - } - if (typeof $(player1).data("jPlayer") != 'undefined' && globals.settings.Jukebox) { - if ($(player1).data("jPlayer").status.paused) { - $rootScope.sendToJukebox('start'); - } else { - $rootScope.sendToJukebox('stop'); - } - } - }; - $rootScope.nextTrack = function () { - var next = getNextSong(); - if (next) { - $rootScope.playSong(false, next); - } - }; - $rootScope.previousTrack = function () { - var next = getNextSong(true); - if (next) { - $rootScope.playSong(false, next); - } else { - $rootScope.restartSong(); - } - }; - function getNextSong (previous) { - var song; - if (globals.settings.Debug) { console.log('Getting Next Song > ' + 'Queue length: ' + $rootScope.queue.length); } - if ($rootScope.queue.length > 0) { - angular.forEach($rootScope.queue, function (item, key) { - if (item.playing === true) { - song = item; - } else { - item.playing = false; - } - }); - var index = $rootScope.queue.indexOf(song); - var next; - if (previous) { - next = $rootScope.queue[index - 1]; - } else { - next = $rootScope.queue[index + 1]; - } - if (typeof next != 'undefined') { - if (globals.settings.Debug) { console.log('Next Song: ' + next.id); } - return next; - } else { - return false; - } - } else { - return false; - } - } - function internalScrobbleSong(submission) { - if ($rootScope.loggedIn && submission) { - var id = $rootScope.playingSong.id; - if (globals.settings.Debug) { console.log('Scrobble Song: ' + id); } - $.ajax({ - url: globals.BaseURL() + '/scrobble.view?' + globals.BaseParams() + '&id=' + id + "&submission=" + submission, - method: 'GET', - dataType: globals.settings.Protocol, - timeout: 10000, - success: function () { - scrobbled = true; - } - }); - } - } - this.startSaveTrackPosition = function () { - if (globals.settings.SaveTrackPosition) { - if (timerid !== 0) { - clearInterval(timerid); - } - timerid = $window.setInterval(function () { - if (globals.settings.SaveTrackPosition) { - this.saveTrackPosition(); - } - }, 30000); - } - }; - this.toggleMute = function () { - //var audio = typeof $(player1).data("jPlayer") != 'undefined' ? true : false; - var audio = $(player1).data("jPlayer"); - if (typeof audio != 'undefined') { - if (audio.options.muted) { - audio.options.muted = false; - } else { - audio.options.muted = true; - } - } - } - this.saveTrackPosition = function () { - //var audio = typeof $(player1).data("jPlayer") != 'undefined' ? true : false; - var audio = $(player1).data("jPlayer"); - if (typeof audio != 'undefined') { - if (audio.status.currentTime > 0 && audio.status.paused === false) { - var song; - angular.forEach($rootScope.queue, function (item, key) { - if (item.playing === true) { - song = item; - } - }); - if (song) { - var position = audio.status.currentTime; - if (position !== null) { - $('#action_SaveProgress').fadeTo("slow", 0).delay(500).fadeTo("slow", 1).delay(500).fadeTo("slow", 0).delay(500).fadeTo("slow", 1); - song.position = position; - // Save Queue - if (utils.browserStorageCheck()) { - try { - var songStr = angular.toJson(song); - localStorage.setItem('CurrentSong', songStr); - if (globals.settings.Debug) { console.log('Saving Current Position: ' + songStr); } - var html = localStorage.getItem('CurrentQueue'); - if ($rootScope.queue.length > 0) { - var current = $rootScope.queue; - if (current != html) { - localStorage.setItem('CurrentQueue', angular.toJson(current)); - if (globals.settings.Debug) { console.log('Saving Queue: ' + current.length + ' characters'); } - } - } - } catch (e) { - if (e == QUOTA_EXCEEDED_ERR) { - alert('Quota exceeded!'); - } - } - } else { - if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); } - } - } - } - } - } else { - if (globals.settings.Debug) { console.log('Saving Queue: No Audio Loaded'); } - } - }; - this.loadTrackPosition = function () { - if (utils.browserStorageCheck()) { - // Load Saved Song - var song = angular.fromJson(localStorage.getItem('CurrentSong')); - if (song) { - $rootScope.playSong(true, song); - // Load Saved Queue - var items = angular.fromJson(localStorage.getItem('CurrentQueue')); - if (items) { - //$rootScope.queue = []; - $rootScope.queue = items; - if ($rootScope.queue.length > 0) { - notifications.updateMessage($rootScope.queue.length + ' Saved Song(s)', true); - } - if (globals.settings.Debug) { console.log('Play Queue Loaded From localStorage: ' + $rootScope.queue.length + ' song(s)'); } - } - } - } else { - if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser'); } - } - }; - this.deleteCurrentQueue = function (data) { - if (utils.browserStorageCheck()) { - localStorage.removeItem('CurrentQueue'); - utils.setValue('CurrentSong', null, false); - if (globals.settings.Debug) { console.log('Removing Play Queue'); } - } else { - if (globals.settings.Debug) { console.log('HTML5::loadStorage not supported on your browser, ' + html.length + ' characters'); } - } - }; - $rootScope.restartSong = function (loadonly, data) { - var audio = $(player1).data("jPlayer"); - audio.play(0); - }; - $rootScope.playSong = function (loadonly, data) { - if (globals.settings.Debug) { console.log('Play: ' + JSON.stringify(data, null, 2)); } - angular.forEach($rootScope.queue, function(item, key) { - item.playing = false; - }); - data.playing = true; - data.selected = false; - - if ($rootScope.playingSong != null && data.id != $rootScope.playingSong.id && $.fancybox.isOpen) { - utils.fancyboxOpenImage(data.coverartfull); - } - - $rootScope.playingSong = data; - - var id = data.id; - var url = data.url; - var position = data.position; - var title = data.name; - var album = data.album; - var artist = data.artist; - var suffix = data.suffix; - var specs = data.specs; - var coverartthumb = data.coverartthumb; - var coverartfull = data.coverartfull; - var starred = data.starred; - $('#playermiddle').css('visibility', 'visible'); - $('#songdetails').css('visibility', 'visible'); - - if (globals.settings.Jukebox) { - $rootScope.addToJukebox(id); - $rootScope.loadjPlayer(player1, url, suffix, true, position); - } else { - $rootScope.loadjPlayer(player1, url, suffix, loadonly, position); - } - - var spechtml = ''; - var data = $(player1).data().jPlayer; - for (var i = 0; i < data.solutions.length; i++) { - var solution = data.solutions[i]; - if (data[solution].used) { - spechtml += "" + solution + " is"; - spechtml += " currently being used with"; - angular.forEach(data[solution].support, function (format) { - if (data[solution].support[format]) { - spechtml += " " + format + ""; - } - }); - spechtml += " support"; - } - } - $('#SMStats').html(spechtml); - scrobbled = false; - - if ($rootScope.queue.length > 0) { - $('#queue').stop().scrollTo('#' + id, 400); - } - - if (globals.settings.NotificationSong && !loadonly) { - notifications.showNotification(coverartthumb, utils.toHTML.un(title), utils.toHTML.un(artist + ' - ' + album), 'text', '#NextTrack'); - } - if (globals.settings.ScrollTitle) { - utils.scrollTitle(utils.toHTML.un(artist) + ' - ' + utils.toHTML.un(title)); - } else { - utils.setTitle(utils.toHTML.un(artist) + ' - ' + utils.toHTML.un(title)); - } - //utils.safeApply(); - if(!$rootScope.$root.$$phase) { - $rootScope.$apply(); - } - }; - $rootScope.loadjPlayer = function (el, url, suffix, loadonly, position) { - // jPlayer Setup - var volume = 1; - if (utils.getValue('Volume')) { - volume = parseFloat(utils.getValue('Volume')); - } - var audioSolution = "html,flash"; - if (globals.settings.ForceFlash) { - audioSolution = "flash,html"; - } - if (globals.settings.Debug) { console.log('Setting Audio Solution: ' + audioSolution); } - //var salt = Math.floor(Math.random() * 100000); - //url += '&salt=' + salt; - var muted = false; - if (globals.settings.Jukebox) { muted = true;} - $(el).jPlayer("destroy"); - $.jPlayer.timeFormat.showHour = true; - $(el).jPlayer({ - swfPath: "js/plugins/jplayer", - wmode: "window", - solution: audioSolution, - supplied: suffix, - volume: volume, - muted: muted, - errorAlerts: false, - warningAlerts: false, - cssSelectorAncestor: "", - cssSelector: { - play: ".PlayTrack", - pause: ".PauseTrack", - seekBar: "#audiocontainer .scrubber", - playBar: "#audiocontainer .progress", - mute: "#action_Mute", - unmute: "#action_UnMute", - volumeMax: "#action_VolumeMax", - currentTime: "#played", - duration: "#duration" - }, - ready: function () { - console.log("File Suffix: " + suffix); - if (suffix == 'oga') { - $(this).jPlayer("setMedia", { - oga: url - }); - } else if (suffix == 'm4a') { - $(this).jPlayer("setMedia", { - m4a: url - }); - } else if (suffix == 'mp3') { - $(this).jPlayer("setMedia", { - mp3: url - }); - } - if (!loadonly) { // Start playing - $(this).jPlayer("play"); - } else { // Loadonly - //$('#' + songid).addClass('playing'); - $(this).jPlayer("pause", position); - } - if (globals.settings.Debug) { - console.log('[jPlayer Version Info]'); - utils.logObjectProperties($(el).data("jPlayer").version); - console.log('[HTML5 Debug Info]'); - utils.logObjectProperties($(el).data("jPlayer").html); - console.log('[Flash Debug Info]'); - utils.logObjectProperties($(el).data("jPlayer").flash); - console.log('[jPlayer Options Info]'); - utils.logObjectProperties($(el).data("jPlayer").options); - } - }, - timeupdate: function (event) { - // Scrobble song once percentage is reached - var p = event.jPlayer.status.currentPercentAbsolute; - if (!scrobbled && p > 30) { - if (globals.settings.Debug) { console.log('LAST.FM SCROBBLE - Percent Played: ' + p); } - internalScrobbleSong(true); - } - }, - volumechange: function (event) { - utils.setValue('Volume', event.jPlayer.options.volume, true); - }, - ended: function (event) { - if (globals.settings.Repeat) { // Repeat current track if enabled - $(this).jPlayer("play"); - } else { - if (!getNextSong()) { // Action if we are at the last song in queue - if (globals.settings.LoopQueue) { // Loop to first track in queue if enabled - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); - } else if (globals.settings.AutoPlay) { // Load more tracks if enabled - $rootScope.getRandomSongs('play', '', ''); - notifications.updateMessage('Auto Play Activated...', true); - } - } else { - $rootScope.nextTrack(); - } - } - }, - error: function (event) { - var time = $(player1).data("jPlayer").status.currentTime; - $(player1).jPlayer("play", time); - if (globals.settings.Debug) { - console.log("Error Type: " + event.jPlayer.error.type); - console.log("Error Context: " + event.jPlayer.error.context); - console.log("Error Message: " + event.jPlayer.error.message); - console.log("Stream interrupted, retrying from position: " + time); - } - } - }); - return; - }; - this.playPauseSong = function () { - if (typeof $(player1).data("jPlayer") != 'undefined') { - if ($(player1).data("jPlayer").status.paused) { - $(player1).jPlayer("play"); - } else { - $(player1).jPlayer("pause"); - } - } - }; - this.playVideo = function (id, bitrate) { - var w, h; - bitrate = parseInt(bitrate); - if (bitrate <= 600) { - w = 320; h = 240; - } else if (bitrate <= 1000) { - w = 480; h = 360; - } else { - w = 640; h = 480; - } - //$("#jPlayerSelector").jPlayer("option", "fullScreen", true); - $("#videodeck").jPlayer({ - ready: function () { - /* - $.fancybox({ - autoSize: false, - width: w + 10, - height: h + 10, - content: $('#videodeck') - }); - */ - $(this).jPlayer("setMedia", { - m4v: 'https://&id=' + id + '&salt=83132' - }).jPlayer("play"); - $('#videooverlay').show(); - }, - swfPath: "js/jplayer", - solution: "html, flash", - supplied: "m4v" - }); - }; - this.scrobbleSong = internalScrobbleSong; - this.rateSong = function (songid, rating) { - $.ajax({ - url: baseURL + '/setRating.view?' + baseParams + '&id=' + songid + "&rating=" + rating, - method: 'GET', - dataType: protocol, - timeout: 10000, - success: function () { - updateMessage('Rating Updated!', true); - } - }); - }; -}]); diff --git a/app/common/songs.html b/app/common/songs.html index c2ab466..37c442c 100644 --- a/app/common/songs.html +++ b/app/common/songs.html @@ -1,6 +1,6 @@
  • - + @@ -18,4 +18,4 @@
     
    {{o.time}}
    -
  • \ No newline at end of file + diff --git a/app/common/songs_lite.html b/app/common/songs_lite.html index e5f8ba8..9997004 100644 --- a/app/common/songs_lite.html +++ b/app/common/songs_lite.html @@ -1,10 +1,10 @@ -
  • +
  • -
    +
    {{o.time}}
    -
  • \ No newline at end of file + diff --git a/app/common/utils-service.js b/app/common/utils-service.js index c10de56..c2b9e01 100644 --- a/app/common/utils-service.js +++ b/app/common/utils-service.js @@ -8,22 +8,6 @@ angular.module('jamstash.utils', ['jamstash.settings']) .service('utils', ['$rootScope', 'globals', function ($rootScope, globals) { 'use strict'; - this.fancyboxOpenImage = function (url) { - $.fancybox.open({ - helpers : { - overlay : { - css : { - 'background' : 'rgba(0, 0, 0, 0.15)' - } - } - }, - hideOnContentClick: true, - type: 'image', - openEffect: 'none', - closeEffect: 'none', - href: url - }); - }; this.safeApply = function (fn) { var phase = $rootScope.$root.$$phase; if (phase === '$apply' || phase === '$digest') { @@ -100,14 +84,6 @@ angular.module('jamstash.utils', ['jamstash.settings']) break; } }; - // HTML5 - this.browserStorageCheck = function () { - if (typeof (localStorage) === 'undefined') { - return false; - } else { - return true; - } - }; this.timeToSeconds = function (time) { var a = time.split(':'); // split it at the colons var seconds; @@ -259,41 +235,6 @@ angular.module('jamstash.utils', ['jamstash.settings']) var u = strurl.substring(0, strurl.indexOf('?')); return u; }; - this.setTitle = function (text) { - if (text !== "") { - document.title = text; - } - }; - var timer = 0; - this.scrollTitle = function (text) { - var shift = { - "left": function (a) { - a.push(a.shift()); - }, - "right": function (a) { - a.unshift(a.pop()); - } - }; - var opts = { - text: text, - dir: "left", - speed: 1200 - }; - - var t = (opts.text || document.title).split(""); - if (!t) { - return; - } - t.push(" "); - clearInterval(timer); - timer = setInterval(function () { - var f = shift[opts.dir]; - if (f) { - f(t); - document.title = t.join(""); - } - }, opts.speed); - }; this.parseVersionString = function (str) { if (typeof (str) !== 'string') { return false; } var x = str.split('.'); @@ -353,4 +294,4 @@ angular.module('jamstash.utils', ['jamstash.settings']) var newDate = months[month] + " " + dateParts[2] + ", " + dateParts[0]; return newDate; }; -}]); \ No newline at end of file +}]); diff --git a/app/images/pause_alt_24x32.png b/app/images/pause_alt_24x24.png similarity index 100% rename from app/images/pause_alt_24x32.png rename to app/images/pause_alt_24x24.png diff --git a/app/images/play_alt_24x32.png b/app/images/play_alt_24x24.png similarity index 100% rename from app/images/play_alt_24x32.png rename to app/images/play_alt_24x24.png diff --git a/app/index.html b/app/index.html index c0f2e1e..c4322b9 100755 --- a/app/index.html +++ b/app/index.html @@ -1,175 +1,121 @@ - - - - - - - - - Jamstash - - - - - - - - - - - - - - - -
    - -
    - -
    - -
    - -
    -
    - - -
    -
    -
    -
    - - - - -
    -
    -
    -
      -
    • -
    • -
    -
    - - - - - - - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    00:00/00:00
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + Jamstash + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/player/player-directive.js b/app/player/player-directive.js new file mode 100644 index 0000000..0d2993a --- /dev/null +++ b/app/player/player-directive.js @@ -0,0 +1,205 @@ +/** + * 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', 'jamstash.notifications', 'jamstash.persistence', 'jamstash.page']) + +.directive('jplayer', ['$interval', 'player', 'globals', 'subsonic', 'notifications', 'persistence', 'Page', + function($interval, playerService, globals, subsonic, notifications, persistence, Page) { + 'use strict'; + return { + restrict: 'EA', + template: '
    ', + link: function(scope, element) { + + //TODO: Hyz: Move to another directive that delegates either to jPlayer or to jukebox + $('.PlayTrack').on('click', function(event) { + event.preventDefault(); + $(this).hide(); + $('.PauseTrack').show(); + }); + + //TODO: Hyz: Move to another directive that delegates either to jPlayer or to jukebox + $('.PauseTrack').on('click', function(event) { + event.preventDefault(); + $(this).hide(); + $('.PlayTrack').show(); + }); + + var timerid; + var $player = element.children('div'); + var audioSolution = 'html,flash'; + if (globals.settings.ForceFlash) { + audioSolution = 'flash,html'; + } + + var updatePlayer = function() { + $.jPlayer.timeFormat.showHour = true; + $player.jPlayer({ + // Flash fallback for outdated browser not supporting HTML5 audio/video tags + // TODO: Hyz: Replace in Grunt ! + swfPath: 'bower_components/jplayer/dist/jplayer/jquery.jplayer.swf', + wmode: 'window', + solution: audioSolution, + supplied: 'mp3, oga, m4a', + preload: 'auto', + errorAlerts: false, + warningAlerts: false, + cssSelectorAncestor: '#player', + cssSelector: { + play: '.PlayTrack', + pause: '.PauseTrack', + seekBar: '#audiocontainer .scrubber', + playBar: '#audiocontainer .progress', + mute: '#action_Mute', + unmute: '#action_UnMute', + volumeMax: '#action_VolumeMax', + currentTime: '#played', + duration: '#duration' + }, + setmedia: function() { + scope.scrobbled = false; + }, + play: function() { + scope.revealControls(); + }, + ended: function() { + // We do this here and not on the service because we cannot create + // a circular dependency between the player and subsonic services + // TODO: Hyz: Should be fixed when it's subsonic-controller instead of subsonic-service that uses player-service + if(playerService.isLastSongPlaying() && globals.settings.AutoPlay) { + // Load more random tracks + subsonic.getRandomSongs().then(function (songs) { + playerService.addSongs(songs).songEnded(); + notifications.updateMessage('Auto Play Activated...', true); + }); + } else { + playerService.songEnded(); + } + scope.$apply(); + }, + timeupdate: function (event) { + // Scrobble song once percentage is reached + var p = event.jPlayer.status.currentPercentAbsolute; + var isPlaying = !event.jPlayer.status.paused; + if (!scope.scrobbled && p > 30 && isPlaying) { + if (globals.settings.Debug) { console.log('LAST.FM SCROBBLE - Percent Played: ' + p); } + subsonic.scrobble(scope.currentSong); + scope.scrobbled = true; + } + }, + error: function (event) { + var position = event.jPlayer.status.currentTime; + if(position) { + $player.jPlayer('play', position); + } + if (globals.settings.Debug) { + console.log("jPlayer error: ", event.jPlayer.error); + console.log("Stream interrupted, retrying from position: ", position); + } + } + }); + }; + + scope.$watch(function () { + return playerService.getPlayingSong(); + }, function (newSong) { + if(newSong !== undefined) { + scope.currentSong = newSong; + Page.setTitleSong(newSong); + if($.fancybox.isOpen) { + scope.fancyboxOpenImage(newSong.coverartfull); + } + var media = {}; + if (newSong.suffix === 'oga') { + media= { oga: newSong.url }; + } else if (newSong.suffix === 'm4a') { + media= { m4a: newSong.url }; + } else if (newSong.suffix === 'mp3') { + media= { mp3: newSong.url }; + } + $player.jPlayer('setMedia', media); + if (globals.settings.Jukebox) { + $player.jPlayer('mute', true); + scope.addToJukebox(newSong.id); + } + if (playerService.loadSong === true || globals.settings.Jukebox) { + // Do not play, only load + playerService.loadSong = false; + scope.revealControls(); + $player.jPlayer('pause', newSong.position); + } else { + $player.jPlayer('play'); + if(globals.settings.NotificationSong) { + notifications.showNotification(newSong); + } + } + } + }); + + scope.$watch(function () { + return playerService.restartSong; + }, function (newVal) { + if(newVal === true) { + $player.jPlayer('play', 0); + scope.scrobbled = false; + playerService.restartSong = false; + } + }); + + scope.$watch(function () { + return globals.settings.SaveTrackPosition; + }, function (newVal) { + if (newVal === true) { + scope.startSavePosition(); + } + }); + + scope.revealControls = function () { + $('#playermiddle').css('visibility', 'visible'); + $('#songdetails').css('visibility', 'visible'); + }; + + scope.startSavePosition = function () { + if (globals.settings.SaveTrackPosition) { + if (timerid !== 0) { + $interval.cancel(timerid); + } + timerid = $interval(function () { + var audio = $player.data('jPlayer'); + if (globals.settings.SaveTrackPosition && scope.currentSong !== undefined && + audio !== undefined && audio.status.currentTime > 0 && audio.status.paused === false) { + $('#action_SaveProgress') + .fadeTo("slow", 0).delay(500) + .fadeTo("slow", 1).delay(500) + .fadeTo("slow", 0).delay(500) + .fadeTo("slow", 1); + scope.currentSong.position = audio.status.currentTime; + persistence.saveTrackPosition(scope.currentSong); + persistence.saveQueue(); + } + }, 30000); + } + }; + + // Startup + timerid = 0; + scope.currentSong = {}; + scope.scrobbled = false; + + updatePlayer(); + scope.startSavePosition(); + + //TODO: Hyz: Maybe move to another directive dedicated to the scrubber ? + $('#audiocontainer .scrubber').mouseover(function () { + $('.audiojs .scrubber').stop().animate({ height: '8px' }); + }); + $('#audiocontainer .scrubber').mouseout(function () { + $('.audiojs .scrubber').stop().animate({ height: '4px' }); + }); + + } //end link + }; +}]); diff --git a/app/player/player-directive_test.js b/app/player/player-directive_test.js new file mode 100644 index 0000000..a827887 --- /dev/null +++ b/app/player/player-directive_test.js @@ -0,0 +1,301 @@ +describe("jplayer directive", function() { + 'use strict'; + + var element, scope, $player, playingSong, deferred, + playerService, mockGlobals, subsonic, notifications, persistence, Page, $interval; + + beforeEach(function() { + // We redefine globals because in some tests we need to alter the settings + mockGlobals = { + settings: { + AutoPlay: false, + Jukebox: false, + NotificationSong: false, + SaveTrackPosition: false + } + }; + // Redefined to avoid firing 'play' with a previous test song + playingSong = undefined; + module('jamstash.player.directive', function($provide) { + // Mock the player service + $provide.decorator('player', function($delegate) { + $delegate.restartSong = false; + $delegate.loadSong = false; + $delegate.getPlayingSong = jasmine.createSpy('getPlayingSong').and.callFake(function() { + return playingSong; + }); + $delegate.nextTrack = jasmine.createSpy('nextTrack'); + $delegate.songEnded = jasmine.createSpy('songEnded'); + $delegate.isLastSongPlaying = jasmine.createSpy('isLastSongPlaying'); + return $delegate; + }); + $provide.value('globals', mockGlobals); + }); + + spyOn($.fn, "jPlayer").and.callThrough(); + inject(function($rootScope, $compile, _$interval_, $q, _player_, _subsonic_, _notifications_, _persistence_, _Page_) { + playerService = _player_; + subsonic = _subsonic_; + notifications = _notifications_; + persistence = _persistence_; + $interval = _$interval_; + Page = _Page_; + // Compile the directive + scope = $rootScope.$new(); + element = '
    '; + element = $compile(element)(scope); + scope.$digest(); + deferred = $q.defer(); + }); + spyOn(Page, "setTitleSong"); + $.fancybox.isOpen = false; + $player = element.children('div'); + }); + + describe("When the player service's current song changes,", function() { + beforeEach(function() { + // To avoid errors breaking the test, we stub jPlayer + $.fn.jPlayer.and.stub(); + playingSong = { + id: 659, + url: 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate', + suffix: 'mp3' + }; + }); + + it("it sets jPlayer's media, stores the song for future scrobbling and sets the page title with the song", function() { + scope.$apply(); + + expect($.fn.jPlayer).toHaveBeenCalledWith('setMedia', {'mp3': 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate'}); + expect(scope.currentSong).toEqual(playingSong); + expect(Page.setTitleSong).toHaveBeenCalledWith(playingSong); + }); + + it("if the global setting Jukebox is true, it mutes jPlayer and adds the song to subsonic's Jukebox", function() { + mockGlobals.settings.Jukebox = true; + scope.addToJukebox = jasmine.createSpy("addToJukebox"); + + scope.$apply(); + expect($player.jPlayer).toHaveBeenCalledWith('mute', true); + expect(scope.addToJukebox).toHaveBeenCalledWith(playingSong.id); + }); + + it("if the player service's loadSong flag is true, it does not play the song, it displays the player controls and sets the player to the song's supplied position", function() { + spyOn(scope, "revealControls"); + playerService.loadSong = true; + playingSong.position = 42.2784; + + scope.$apply(); + + expect($player.jPlayer).not.toHaveBeenCalledWith('play'); + expect($player.jPlayer).toHaveBeenCalledWith('pause', 42.2784); + expect(playerService.loadSong).toBeFalsy(); + expect(scope.revealControls).toHaveBeenCalled(); + }); + + describe("if the player service's loadSong flag is false,", function() { + it("it plays the song", function() { + playerService.loadSong = false; + scope.$apply(); + + expect($player.jPlayer).toHaveBeenCalledWith('play'); + expect(playerService.loadSong).toBeFalsy(); + }); + + it("if the global setting NotificationSong is true, it displays a notification", function() { + spyOn(notifications, "showNotification"); + mockGlobals.settings.NotificationSong = true; + + scope.$apply(); + + expect(notifications.showNotification).toHaveBeenCalledWith(playingSong); + }); + }); + + it("if fancybox is open, it sets it up with the new song's cover art", function() { + $.fancybox.isOpen = true; + scope.fancyboxOpenImage = jasmine.createSpy("fancyboxOpenImage"); + + scope.$apply(); + + expect(scope.fancyboxOpenImage).toHaveBeenCalledWith(playingSong.coverartfull); + }); + + it("if the song's suffix is 'm4a', it sets jPlayer up with this format", function() { + playingSong.suffix = 'm4a'; + scope.$apply(); + expect($.fn.jPlayer).toHaveBeenCalledWith('setMedia', {'m4a': 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate'}); + }); + + it("if the song's suffix is 'oga', it sets jPlayer up with this format", function() { + playingSong.suffix = 'oga'; + scope.$apply(); + expect($.fn.jPlayer).toHaveBeenCalledWith('setMedia', {'oga': 'https://gantry.com/antemarital/vigorless?a=oropharyngeal&b=killcrop#eviscerate'}); + }); + }); + + it("When the player service's restartSong flag is true, it restarts the current song, resets the restart flag to false and resets the scrobbled flag to false", function() { + $.fn.jPlayer.and.stub(); + playerService.restartSong = true; + scope.scrobbled = true; + scope.$apply(); + + expect($player.jPlayer).toHaveBeenCalledWith('play', 0); + expect(playerService.restartSong).toBeFalsy(); + expect(scope.scrobbled).toBeFalsy(); + }); + + describe("When jplayer has finished the current song,", function() { + var ended; + beforeEach(function() { + ended = $.jPlayer.event.ended; + }); + it("it notifies the player service that the song has ended", function() { + $player.trigger(ended); + expect(playerService.songEnded).toHaveBeenCalled(); + }); + + it("given that the last song of the queue is playing and that the global setting AutoPlay is true, it asks subsonic for random tracks, notifies the player service that the song has ended and notifies the user", function() { + mockGlobals.settings.AutoPlay = true; + spyOn(subsonic, "getRandomSongs").and.returnValue(deferred.promise); + spyOn(notifications, "updateMessage"); + playerService.isLastSongPlaying.and.returnValue(true); + + $player.trigger(ended); + deferred.resolve(); + scope.$apply(); + + expect(playerService.isLastSongPlaying).toHaveBeenCalled(); + expect(subsonic.getRandomSongs).toHaveBeenCalled(); + expect(notifications.updateMessage).toHaveBeenCalledWith('Auto Play Activated...', true); + }); + }); + + it("When jPlayer gets new media, it resets the scrobbled flag to false", function() { + scope.scrobbled = true; + + var e = $.jPlayer.event.setmedia; + $player.trigger(e); + + expect(scope.scrobbled).toBeFalsy(); + }); + + it("When jPlayer throws an error, it tries to restart playback at the last position", function() { + // Fake jPlayer's internal _trigger event because I can't trigger a manual error + var fakejPlayer = { + element: $player, + status: { currentTime: 10.4228 } + }; + var error = $.jPlayer.event.error; + + $.jPlayer.prototype._trigger.call(fakejPlayer, error); + + expect($player.jPlayer).toHaveBeenCalledWith('play', 10.4228); + }); + + it("When jPlayer starts to play the current song, it displays the player controls", function() { + spyOn(scope, "revealControls"); + + var e = $.jPlayer.event.play; + $player.trigger(e); + + expect(scope.revealControls).toHaveBeenCalled(); + }); + + it("revealControls - it displays the song details and the player controls", function() { + $.fn.jPlayer.and.stub(); + 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(); + }); + }); + + it("When the global setting SaveTrackPosition becomes true, it starts saving the current song's position", function() { + spyOn(scope, "startSavePosition"); + mockGlobals.settings.SaveTrackPosition = true; + + scope.$apply(); + + expect(scope.startSavePosition).toHaveBeenCalled(); + }); + + describe("Given that the global setting SaveTrackPosition is true,", function() { + beforeEach(function() { + mockGlobals.settings.SaveTrackPosition = true; + spyOn(persistence, "saveTrackPosition"); + spyOn(persistence, "saveQueue"); + }); + + it("every 30 seconds, it saves the current song's position and the playing queue", function() { + scope.currentSong = { id: 419 }; + $player.data('jPlayer').status.currentTime = 35.3877; + $player.data('jPlayer').status.paused = false; + + scope.startSavePosition(); + $interval.flush(30001); + + expect(scope.currentSong.position).toBe(35.3877); + expect(persistence.saveTrackPosition).toHaveBeenCalledWith(scope.currentSong); + expect(persistence.saveQueue).toHaveBeenCalled(); + }); + + it("if the song is not playing, it does not save anything", function() { + $player.data('jPlayer').status.currentTime = 0.0; + $player.data('jPlayer').status.paused = true; + + scope.startSavePosition(); + $interval.flush(30001); + + expect(persistence.saveTrackPosition).not.toHaveBeenCalled(); + expect(persistence.saveQueue).not.toHaveBeenCalled(); + }); + + it("if there was already a watcher, it clears it before adding a new one", function() { + spyOn($interval, "cancel"); + + scope.startSavePosition(); + scope.startSavePosition(); + expect($interval.cancel).toHaveBeenCalled(); + }); + }); +}); diff --git a/app/player/player-service.js b/app/player/player-service.js new file mode 100644 index 0000000..1cf1226 --- /dev/null +++ b/app/player/player-service.js @@ -0,0 +1,137 @@ +/** +* jamstash.player.service Module +* +* 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']) + +.factory('player', ['globals', function (globals) { + 'use strict'; + + var player = { + // playingIndex and playingSong aren't meant to be used, they only are public for unit-testing purposes + _playingIndex: -1, + _playingSong: undefined, + queue: [], + restartSong: false, + loadSong: false, + + play: function(song) { + // Find the song's index in the queue, if it's in there + var index = player.indexOfSong(song); + player._playingIndex = (index !== undefined) ? index : -1; + if(player._playingSong === song) { + // We call restart because the _playingSong hasn't changed and the directive won't + // play the song again + player.restart(); + } else { + player._playingSong = song; + } + }, + + playFirstSong: function() { + player._playingIndex = 0; + player.play(player.queue[0]); + }, + + load: function(song) { + player.loadSong = true; + player.play(song); + }, + + restart: function() { + player.restartSong = true; + }, + + // Called from the player directive at the end of the current song + songEnded: function() { + if (globals.settings.Repeat) { + // repeat current track + player.restart(); + } else if (player.isLastSongPlaying() === true) { + if (globals.settings.LoopQueue) { + // Loop to first track in queue + player.playFirstSong(); + } + } else { + player.nextTrack(); + } + }, + + nextTrack: function() { + // Find the song's index in the queue, in case it changed (with a drag & drop) + var index = player.indexOfSong(player._playingSong); + player._playingIndex = (index !== undefined) ? index : -1; + + if((player._playingIndex + 1) < player.queue.length) { + var nextTrack = player.queue[player._playingIndex + 1]; + player._playingIndex++; + player.play(nextTrack); + } + }, + + previousTrack: function() { + // Find the song's index in the queue, in case it changed (with a drag & drop) + var index = player.indexOfSong(player._playingSong); + player._playingIndex = (index !== undefined) ? index : -1; + + if((player._playingIndex - 1) > 0) { + var previousTrack = player.queue[player._playingIndex - 1]; + player._playingIndex--; + player.play(previousTrack); + } else if (player.queue.length > 0) { + player.playFirstSong(); + } + }, + + emptyQueue: function() { + player.queue = []; + return player; + }, + + shuffleQueue: function() { + player.queue = _(player.queue).shuffle(); + return player; + }, + + addSong: function(song) { + player.queue.push(song); + return player; + }, + + addSongs: function (songs) { + player.queue = player.queue.concat(songs); + return player; + }, + + removeSong: function(song) { + var index = player.queue.indexOf(song); + player.queue.splice(index, 1); + return player; + }, + + removeSongs: function (songs) { + player.queue = _(player.queue).difference(songs); + return player; + }, + + getPlayingSong: function() { + return player._playingSong; + }, + + isLastSongPlaying: function() { + return ((player._playingIndex +1) === player.queue.length); + }, + + indexOfSong: function(song) { + for (var i = player.queue.length - 1; i >= 0; i--) { + if (angular.equals(song, player.queue[i])) { + return i; + } + } + return undefined; + } + }; + + return player; +}]); diff --git a/app/player/player-service_test.js b/app/player/player-service_test.js new file mode 100644 index 0000000..7e30074 --- /dev/null +++ b/app/player/player-service_test.js @@ -0,0 +1,280 @@ +describe("Player service -", function() { + 'use strict'; + + var player, mockGlobals, firstSong, secondSong, thirdSong, newSong; + beforeEach(function() { + // We redefine globals because in some tests we need to alter the settings + mockGlobals = { + settings: { + Repeat: false, + LoopQueue: false + } + }; + module('jamstash.player.service', function ($provide) { + $provide.value('globals', mockGlobals); + }); + inject(function (_player_) { + player = _player_; + }); + }); + + describe("Given that I have 3 songs in my playing queue,", function() { + + beforeEach(function() { + firstSong = { + id: 6726, + name: 'Guarauno', + artist: 'Carlyn Pollack', + album: 'Arenig' + }; + secondSong = { + id: 2452, + name: 'Michoacan', + artist: 'Lura Jeppsen', + album: 'dioptrical' + }; + thirdSong = { + id: 574, + name: 'Celtidaceae', + artist: 'Willard Steury', + album: 'redux' + }; + player.queue = [firstSong, secondSong, thirdSong]; + newSong = { + id: 3573, + name: 'Tritopatores', + artist: 'Alysha Rocher', + album: 'uncombinably' + }; + }); + + describe("when I call nextTrack", function() { + beforeEach(function() { + spyOn(player, "play"); + }); + + it("and no song is playing, it plays the first song", function() { + player.nextTrack(); + + expect(player._playingIndex).toBe(0); + expect(player.play).toHaveBeenCalledWith(player.queue[0]); + }); + + it("and the first song is playing, it plays the second song", function() { + player._playingIndex = 0; + player._playingSong = firstSong; + + player.nextTrack(); + + expect(player._playingIndex).toBe(1); + expect(player.play).toHaveBeenCalledWith(player.queue[1]); + }); + + it("and the last song is playing, it does nothing", function() { + player._playingIndex = 2; + player._playingSong = thirdSong; + + player.nextTrack(); + + expect(player._playingIndex).toBe(2); + expect(player.play).not.toHaveBeenCalled(); + }); + }); + + describe("when I call previousTrack", function() { + beforeEach(function() { + spyOn(player, "play"); + }); + + it("and no song is playing, it plays the first song", function() { + player.previousTrack(); + + expect(player._playingIndex).toBe(0); + expect(player.play).toHaveBeenCalledWith(player.queue[0]); + }); + + it("and the first song is playing, it restarts the first song", function() { + player._playingIndex = 0; + player._playingSong = firstSong; + + player.previousTrack(); + + expect(player._playingIndex).toBe(0); + expect(player.play).toHaveBeenCalledWith(player.queue[0]); + }); + + it("and the last song is playing, it plays the second song", function() { + player._playingIndex = 2; + player._playingSong = thirdSong; + + player.previousTrack(); + + expect(player._playingIndex).toBe(1); + expect(player.play).toHaveBeenCalledWith(player.queue[1]); + }); + }); + + it("when I call playFirstSong, it plays the first song and updates the playing index", function() { + spyOn(player, "play"); + + player.playFirstSong(); + + expect(player._playingIndex).toBe(0); + expect(player.play).toHaveBeenCalledWith(player.queue[0]); + }); + + it("when I play the second song, it finds its index in the playing queue and updates the playing index", function() { + player.play(secondSong); + expect(player._playingIndex).toBe(1); + }); + + it("when I play a song that isn't in the playing queue, the next song will be the first song of the playing queue", function() { + player.play(newSong); + expect(player._playingIndex).toBe(-1); + }); + + it("when I call emptyQueue, it empties the playing queue", function() { + player.emptyQueue(); + expect(player.queue).toEqual([]); + }); + + it("when I get the index of the first song, it returns 0", function() { + expect(player.indexOfSong(firstSong)).toBe(0); + }); + + it("when I get the index of a song that isn't in the playing queue, it returns undefined", function() { + expect(player.indexOfSong(newSong)).toBeUndefined(); + }); + + it("when I add a song to the queue, it is appended to the end of the playing queue", function() { + player.addSong(newSong); + expect(player.queue).toEqual([firstSong, secondSong, thirdSong, newSong]); + }); + + it("when I add 3 songs to the queue, they are appended to the end of the playing queue", function() { + var secondNewSong = {id: 6338, name: 'Preconquest', artist: 'France Wisley', album: 'Unmix'}; + var thirdNewSong = {id: 3696, name: 'Cetene', artist: 'Hilario Masley', album: 'Gonapophysal'}; + player.addSongs([newSong, secondNewSong, thirdNewSong]); + expect(player.queue).toEqual([firstSong, secondSong, thirdSong, newSong, secondNewSong, thirdNewSong]); + }); + + it("when I remove the second song, the playing queue is now only the first and third song", function() { + player.removeSong(secondSong); + expect(player.queue).toEqual([firstSong, thirdSong]); + }); + + it("when I remove the first and third songs, the playing queue is now only the second song", function() { + player.removeSongs([firstSong, thirdSong]); + expect(player.queue).toEqual([secondSong]); + }); + + it("when the first song is playing, isLastSongPlaying returns false", function() { + player._playingIndex = 0; + expect(player.isLastSongPlaying()).toBeFalsy(); + }); + + it("when the third song is playing, isLastSongPlaying returns true", function() { + player._playingIndex = 2; + expect(player.isLastSongPlaying()).toBeTruthy(); + }); + + it("and the current song is not the last, when the current song ends, it plays the next song in queue", function() { + spyOn(player, "nextTrack"); + player._playingIndex = 0; + + player.songEnded(); + + expect(player.nextTrack).toHaveBeenCalled(); + }); + + it("and that the 'Repeat song' setting is true, when the current song ends, it restarts it", function() { + spyOn(player, "restart"); + mockGlobals.settings.Repeat = true; + + player.songEnded(); + + expect(player.restart).toHaveBeenCalled(); + }); + + describe("and the current song is the last of the queue, when the current song ends,", function() { + beforeEach(function() { + player._playingIndex = 2; + }); + + it("if the 'Repeat queue' setting is true, it plays the first song of the queue", function() { + spyOn(player, "playFirstSong"); + mockGlobals.settings.LoopQueue = true; + + player.songEnded(); + + expect(player.playFirstSong).toHaveBeenCalled(); + }); + + it("it does not play anything", function() { + spyOn(player, "nextTrack").and.callThrough(); + player.songEnded(); + + expect(player._playingIndex).toBe(2); + expect(player.nextTrack).not.toHaveBeenCalled(); + }); + }); + }); + + describe("Given a song", function() { + + var song; + beforeEach(function() { + song = { + id: 6726, + name: 'Guarauno', + artist: 'Carlyn Pollack', + album: 'Arenig', + playing: false + }; + }); + + it("when the song was playing and I play it again, it restarts the current song", function() { + spyOn(player, "restart"); + + player.play(song); + player.play(song); + + expect(player.restart).toHaveBeenCalled(); + }); + + it("when I restart the current song, the flag for the directive is set", 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 my playing queue is empty", function() { + + beforeEach(function() { + player.queue = []; + player._playingIndex = -1; + spyOn(player, "play"); + }); + + it("when I call nextTrack, it does nothing", function() { + player.nextTrack(); + expect(player.play).not.toHaveBeenCalled(); + expect(player._playingIndex).toBe(-1); + }); + + it("when I call previousTrack, it does nothing", function() { + player.previousTrack(); + expect(player.play).not.toHaveBeenCalled(); + expect(player._playingIndex).toBe(-1); + }); + }); +}); diff --git a/app/player/player.html b/app/player/player.html new file mode 100644 index 0000000..6c7d9f3 --- /dev/null +++ b/app/player/player.html @@ -0,0 +1,59 @@ +
    +
    +
    + +
    +
    + + + +
    +
      +
    • +
    • +
    +
    + + + + + + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    00:00/00:00
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/app/player/player.js b/app/player/player.js new file mode 100644 index 0000000..5bd8ca3 --- /dev/null +++ b/app/player/player.js @@ -0,0 +1,39 @@ +/** +* jamstash.player Module +* +* Enables basic control of the player : play, pause, previous track, next track. +* Also provides the currently playing song's info through the scope so it can be displayed next to +* the player controls. +*/ +angular.module('jamstash.player.controller', ['jamstash.player.service', 'jamstash.player.directive']) + +.controller('PlayerController', ['$scope', 'player', 'globals', + function($scope, player, globals){ + 'use strict'; + + $scope.getPlayingSong = function () { + return player.getPlayingSong(); + }; + + $scope.play = function () { + if (globals.settings.Jukebox) { + $scope.sendToJukebox('start'); + } + }; + + $scope.pause = function () { + if (globals.settings.Jukebox) { + $scope.sendToJukebox('stop'); + } + }; + + $scope.previousTrack = function () { + player.previousTrack(); + }; + + $scope.nextTrack = function () { + player.nextTrack(); + }; + + //TODO: Hyz: updateFavorite - leave in rootScope ? +}]); diff --git a/app/player/player_test.js b/app/player/player_test.js new file mode 100644 index 0000000..7991b5a --- /dev/null +++ b/app/player/player_test.js @@ -0,0 +1,40 @@ +describe("Player controller", function() { + 'use strict'; + + var player, scope; + + beforeEach(function() { + module('jamstash.player.controller'); + + inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + + player = jasmine.createSpyObj("player", ["getPlayingSong", "previousTrack", "nextTrack"]); + + $controller('PlayerController', { + $scope: scope, + player: player + }); + }); + }); + + it("When I get the currently playing song, it asks the player service", function() { + scope.getPlayingSong(); + + expect(player.getPlayingSong).toHaveBeenCalled(); + }); + + it("When I get the previous track, it uses the player service", function() { + scope.previousTrack(); + + expect(player.previousTrack).toHaveBeenCalled(); + }); + + it("When I get the next track, it uses the player service", function() { + scope.nextTrack(); + + expect(player.nextTrack).toHaveBeenCalled(); + }); + + // TODO: updateFavorite +}); diff --git a/app/podcasts/podcasts.js b/app/podcasts/podcasts.js index 5c9eb7b..0f7dca3 100644 --- a/app/podcasts/podcasts.js +++ b/app/podcasts/podcasts.js @@ -1,11 +1,11 @@ - + angular.module('JamStash') -.controller('PodcastCtrl', ['$scope', '$rootScope', function PodcastCtrl($scope, $rootScope) { +.controller('PodcastController', ['$scope', '$rootScope', function ($scope, $rootScope) { 'use strict'; $rootScope.song = []; /* Launch on Startup */ $scope.getPodcasts(); /* End Startup */ -}]); \ No newline at end of file +}]); diff --git a/app/queue/queue.html b/app/queue/queue.html index da65316..0ee61b1 100644 --- a/app/queue/queue.html +++ b/app/queue/queue.html @@ -1,5 +1,24 @@ -
    -
    - -
    -
    \ No newline at end of file +
    + + + +
    +
    Queue
    +
    + +
    +
    diff --git a/app/queue/queue.js b/app/queue/queue.js index 2a8521f..eba8315 100644 --- a/app/queue/queue.js +++ b/app/queue/queue.js @@ -1,10 +1,68 @@ -angular.module('JamStash') +/** +* jamstash.queue.controller Module +* +* Manages the playing queue. Gives access to the player service's queue-related functions, +* like adding, removing and shuffling the queue. +*/ +angular.module('jamstash.queue.controller', ['jamstash.player.service']) -.controller('QueueCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'utils', 'globals', - function QueueCtrl($scope, $rootScope, $routeParams, $location, utils, globals) { +.controller('QueueController', ['$scope', 'globals', 'player', + function ($scope, globals, player) { 'use strict'; $scope.settings = globals.settings; - $scope.song = $rootScope.queue; - //angular.copy($rootScope.queue, $scope.song); - $scope.itemType = 'pl'; + $scope.player = player; + + $scope.playSong = function (song) { + player.play(song); + }; + + $scope.emptyQueue = function() { + player.emptyQueue(); + //TODO: Hyz: Shouldn't it be in a directive ? + $.fancybox.close(); + }; + + $scope.shuffleQueue = function() { + player.shuffleQueue(); + }; + + $scope.addSongToQueue = function(song) { + player.addSong(song); + }; + + $scope.removeSongFromQueue = function(song) { + player.removeSong(song); + }; + + $scope.removeSelectedSongsFromQueue = function () { + player.removeSongs($scope.selectedSongs); + }; + + $scope.isPlayingSong = function (song) { + return angular.equals(song, player.getPlayingSong()); + }; + + $scope.$watch(function () { + return player.getPlayingSong(); + }, function (newSong) { + if(newSong !== undefined) { + //TODO: Hyz: Shouldn't it be in a directive ? + $('#SideBar').stop().scrollTo('.song.id' + newSong.id, 400); + } + }); + + /** + * Change the queue's order through jQuery UI's sortable + */ + $scope.dragStart = function (e, ui) { + ui.item.data('start', ui.item.index()); + }; + + $scope.dragEnd = function (e, ui) { + var start = ui.item.data('start'), + end = ui.item.index(); + player.queue.splice(end, 0, player.queue.splice(start, 1)[0]); + }; + + //TODO: Hyz: updateFavorite - leave in rootScope ? }]); diff --git a/app/queue/queue_test.js b/app/queue/queue_test.js new file mode 100644 index 0000000..859ee12 --- /dev/null +++ b/app/queue/queue_test.js @@ -0,0 +1,134 @@ +describe("Queue controller", function() { + 'use strict'; + + var player, scope; + var song; + + beforeEach(function() { + module('jamstash.queue.controller'); + + inject(function ($controller, $rootScope, globals, _player_) { + scope = $rootScope.$new(); + player = _player_; + + $controller('QueueController', { + $scope: scope, + globals: globals, + player: player + }); + }); + song = { id: 7310 }; + player.queue = []; + }); + + it("When I play a song, it calls play in the player service", function() { + spyOn(player, "play"); + scope.playSong(song); + expect(player.play).toHaveBeenCalledWith(song); + }); + + it("When I empty the queue, it calls emptyQueue in the player service and closes fancybox", function() { + spyOn(player, "emptyQueue"); + spyOn($.fancybox, "close"); + + scope.emptyQueue(); + expect(player.emptyQueue).toHaveBeenCalled(); + expect($.fancybox.close).toHaveBeenCalled(); + }); + + it("When I shuffle the queue, it calls shuffleQueue in the player service", function() { + spyOn(player, "shuffleQueue"); + scope.shuffleQueue(); + expect(player.shuffleQueue).toHaveBeenCalled(); + }); + + it("When I add one song to the queue, it calls addSong in the player service", function() { + spyOn(player, "addSong"); + scope.addSongToQueue(song); + expect(player.addSong).toHaveBeenCalledWith(song); + }); + + it("When I remove a song from the queue, it calls removeSong in the player service", function() { + spyOn(player, "removeSong"); + scope.removeSongFromQueue(song); + expect(player.removeSong).toHaveBeenCalledWith(song); + }); + + it("When I remove all the selected songs from the queue, it calls removeSongs in the player service", function() { + spyOn(player, "removeSongs"); + var secondSong = { id: 6791 }; + scope.selectedSongs = [song, secondSong]; + scope.removeSelectedSongsFromQueue(); + expect(player.removeSongs).toHaveBeenCalledWith([song, secondSong]); + }); + + it("asks the player service if a given song is the currently playing song", function() { + spyOn(player, "getPlayingSong").and.returnValue(song); + expect(scope.isPlayingSong(song)).toBeTruthy(); + expect(player.getPlayingSong).toHaveBeenCalled(); + }); + + it("when the player service's current song changes, it scrolls the queue to display it", function() { + spyOn(player, "getPlayingSong").and.callFake(function() { + return song; + }); + spyOn($.fn, "scrollTo"); + + scope.$apply(); + + expect($.fn.scrollTo).toHaveBeenCalled(); + }); + + describe("reorders the queue by drag and drop - ", function() { + var mockUI; + beforeEach(function() { + player.queue = [ + {id: 2246}, + {id: 8869}, + {id: 285} + ]; + mockUI = { + item: {} + }; + }); + + it("given a song in the queue, when I start dragging it, it records what its starting position in the queue was", function() { + mockUI.item.index = jasmine.createSpy("index").and.returnValue('1'); + mockUI.item.data = jasmine.createSpy("data"); + + scope.dragStart({}, mockUI); + + expect(mockUI.item.index).toHaveBeenCalled(); + expect(mockUI.item.data).toHaveBeenCalledWith('start', '1'); + }); + + it("given a song in the queue that I started dragging, when I drop it, its position in the queue has changed", function() { + mockUI.item.index = jasmine.createSpy("index").and.returnValue('0'); + mockUI.item.data = jasmine.createSpy("data").and.returnValue('1'); + + scope.dragEnd({}, mockUI); + + expect(mockUI.item.index).toHaveBeenCalled(); + expect(mockUI.item.data).toHaveBeenCalledWith('start'); + // The second song should now be first + expect(player.queue).toEqual([ + {id: 8869}, + {id: 2246}, + {id: 285} + ]); + }); + + // TODO: Hyz: Maybe it should be an end-to-end test + it("given that the player is playing the second song (B), when I swap the first (A) and the second song (B), the player's next song should be A", function() { + player.play({id: 8869}); + mockUI.item.index = jasmine.createSpy("index").and.returnValue('0'); + mockUI.item.data = jasmine.createSpy("data").and.returnValue('1'); + + scope.dragEnd({}, mockUI); + + player.nextTrack(); + expect(player._playingIndex).toBe(1); + expect(player._playingSong).toEqual({id: 2246}); + }); + }); +}); diff --git a/app/settings/settings.js b/app/settings/settings.js index 30b7039..6ce8755 100644 --- a/app/settings/settings.js +++ b/app/settings/settings.js @@ -1,11 +1,10 @@ angular.module('JamStash') -.controller('SettingsCtrl', ['$rootScope', '$scope', '$routeParams', '$location', '$http', '$q', 'utils', 'globals', 'json', 'notifications', 'player', - function ($rootScope, $scope, $routeParams, $location, $http, $q, utils, globals, json, notifications, player) { +.controller('SettingsController', ['$rootScope', '$scope', '$routeParams', '$location', 'utils', 'globals', 'json', 'notifications', 'persistence', 'subsonic', + function ($rootScope, $scope, $routeParams, $location, utils, globals, json, notifications, persistence, subsonic) { 'use strict'; $rootScope.hideQueue(); $scope.settings = globals.settings; /* See service.js */ - $scope.ApiVersion = globals.settings.ApiVersion; $scope.Timeouts = [ { id: 10000, name: 10 }, { id: 20000, name: 20 }, @@ -28,34 +27,6 @@ $('#AZIndex').show(); } }); - $scope.ping = function () { - var exception = {reason: 'Error when contacting the Subsonic server.'}; - var deferred = $q.defer(); - var httpPromise; - httpPromise = $http({ - method: 'GET', - timeout: globals.settings.Timeout, - // 2015-1-5: This API call only works with json as of SS v5.0?!? - //url: globals.BaseURL() + '/ping.view?' + globals.BaseParams(), - url: globals.BaseURL() + '/ping.view?' + globals.BaseJSONParams() - }); - httpPromise.success(function(data, status) { - var subsonicResponse = (data['subsonic-response'] !== undefined) ? data['subsonic-response'] : {status: 'failed'}; - if (subsonicResponse.status === 'ok') { - $scope.ApiVersion = subsonicResponse.version; - globals.settings.ApiVersion = $scope.ApiVersion; - deferred.resolve(data); - } else { - if(subsonicResponse.status === 'failed' && subsonicResponse.error !== undefined) { - notifications.updateMessage(subsonicResponse.error.message); - } - } - }).error(function(data, status) { - exception.httpError = status; - deferred.reject(exception); - }); - return deferred.promise; - }; $scope.reset = function () { utils.setValue('Settings', null, true); $scope.loadSettings(); @@ -65,20 +36,21 @@ if ($scope.settings.Server.indexOf('http://') != 0 && $scope.settings.Server.indexOf('https://') != 0) { $scope.settings.Server = 'http://' + $scope.settings.Server; } if ($scope.settings.NotificationSong) { notifications.requestPermissionIfRequired(); - if (!notifications.hasNotificationSupport()) { + if (!notifications.isSupported()) { alert('HTML5 Notifications are not available for your current browser, Sorry :('); } } if ($scope.settings.NotificationNowPlaying) { notifications.requestPermissionIfRequired(); - if (!notifications.hasNotificationSupport()) { + if (!notifications.isSupported()) { alert('HTML5 Notifications are not available for your current browser, Sorry :('); } } if ($scope.settings.SaveTrackPosition) { - player.saveTrackPosition(); + persistence.saveQueue(); } else { - player.deleteCurrentQueue(); + persistence.deleteTrackPosition(); + persistence.deleteQueue(); } if ($scope.settings.Theme) { utils.switchTheme(globals.settings.Theme); @@ -92,9 +64,21 @@ notifications.updateMessage('Settings Updated!', true); $scope.loadSettings(); if (globals.settings.Server !== '' && globals.settings.Username !== '' && globals.settings.Password !== '') { - $scope.ping().then(function() { + subsonic.ping().then(function (subsonicResponse) { + globals.settings.ApiVersion = subsonicResponse.version; $location.path('/library').replace(); $rootScope.showIndex = true; + }, function (error) { + //TODO: Hyz: Duplicate from subsonic.js - requestSongs. Find a way to handle this only once. + var errorNotif; + if (error.subsonicError !== undefined) { + errorNotif = error.reason + ' ' + error.subsonicError.message; + } else if (error.httpError !== undefined) { + errorNotif = error.reason + ' HTTP error ' + error.httpError; + } else { + errorNotif = error.reason; + } + notifications.updateMessage(errorNotif, true); }); } }; diff --git a/app/subsonic/subsonic-service.js b/app/subsonic/subsonic-service.js index 2dda7cf..057b90b 100644 --- a/app/subsonic/subsonic-service.js +++ b/app/subsonic/subsonic-service.js @@ -5,10 +5,10 @@ * Also offers more fine-grained functionality that is not part of Subsonic's API. */ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.utils', 'jamstash.model', - 'jamstash.notifications', 'angular-underscore/utils']) + 'jamstash.notifications', 'jamstash.player.service', 'angular-underscore/utils']) -.factory('subsonic', ['$rootScope', '$http', '$q', 'globals', 'utils', 'map', 'notifications', - function ($rootScope, $http, $q, globals, utils, map, notifications) { +.factory('subsonic', ['$rootScope', '$http', '$q', 'globals', 'utils', 'map', 'notifications', 'player', + function ($rootScope, $http, $q, globals, utils, map, notifications, player) { 'use strict'; var index = { shortcuts: [], artists: [] }; @@ -23,7 +23,7 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util selectedArtist: null, selectedAlbum: null, selectedPlaylist: null, - selectedAutoPlaylist: null, + selectedAutoPlaylist: null, selectedGenre: null, selectedPodcast: null }; @@ -68,7 +68,7 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util } } }; - + return { showIndex: $rootScope.showIndex, showPlaylist: showPlaylist, @@ -82,7 +82,7 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util dataType: globals.settings.Protocol, timeout: globals.settings.Timeout, success: function (data) { - + } }); */ @@ -104,6 +104,60 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util } }); }, + + /** + * Handles building the URL with the correct parameters and error-handling while communicating with + * a Subsonic server + * @param {String} partialUrl the last part of the Subsonic URL you want, e.g. 'getStarred.view'. If it does not start with a '/', it will be prefixed + * @param {Object} config optional $http config object. The base settings expected by Subsonic (username, password, etc.) will be overwritten. + * @return {Promise} a Promise that will be resolved if we receive the 'ok' status from Subsonic. Will be rejected otherwise with an object : {'reason': a message that can be displayed to a user, 'httpError': the HTTP error code, 'subsonicError': the error Object sent by Subsonic} + */ + subsonicRequest: function (partialUrl, config) { + var exception = { reason: 'Error when contacting the Subsonic server.' }; + var deferred = $q.defer(); + var actualUrl = (partialUrl.charAt(0) === '/') ? partialUrl : '/' + partialUrl; + var url = globals.BaseURL() + actualUrl; + + // Extend the provided config (if it exists) with our params + // Otherwise we create a config object + var actualConfig = config || {}; + var params = actualConfig.params || {}; + params.u = globals.settings.Username; + params.p = globals.settings.Password; + params.f = globals.settings.Protocol; + params.v = globals.settings.ApiVersion; + params.c = globals.settings.ApplicationName; + actualConfig.params = params; + actualConfig.timeout = globals.settings.Timeout; + + var httpPromise; + if(globals.settings.Protocol === 'jsonp') { + actualConfig.params.callback = 'JSON_CALLBACK'; + httpPromise = $http.jsonp(url, actualConfig); + } else { + httpPromise = $http.get(url, actualConfig); + } + httpPromise.success(function(data) { + var subsonicResponse = (data['subsonic-response'] !== undefined) ? data['subsonic-response'] : {status: 'failed'}; + if (subsonicResponse.status === 'ok') { + deferred.resolve(subsonicResponse); + } else { + if(subsonicResponse.status === 'failed' && subsonicResponse.error !== undefined) { + exception.subsonicError = subsonicResponse.error; + } + deferred.reject(exception); + } + }).error(function(data, status) { + exception.httpError = status; + deferred.reject(exception); + }); + return deferred.promise; + }, + + ping: function () { + return this.subsonicRequest('ping.view'); + }, + getArtists: function (id, refresh) { var deferred = $q.defer(); if (refresh || index.artists.length == 0) { @@ -309,16 +363,16 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util } if (action == 'add') { angular.forEach(items, function (item, key) { - $rootScope.queue.push(map.mapSong(item)); + player.queue.push(map.mapSong(item)); }); notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); } else if (action == 'play') { - $rootScope.queue = []; + player.queue = []; angular.forEach(items, function (item, key) { - $rootScope.queue.push(map.mapSong(item)); + player.queue.push(map.mapSong(item)); }); - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); + var next = player.queue[0]; + player.play(next); notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); } else { if (typeof data["subsonic-response"].directory.id != 'undefined') { @@ -418,69 +472,35 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util } return deferred.promise; }, - getRandomSongs: function (action, genre, folder) { - var deferred = $q.defer(); - if (globals.settings.Debug) { console.log('action:' + action + ', genre:' + genre + ', folder:' + folder); } - var size = globals.settings.AutoPlaylistSize; - content.selectedPlaylist = null; - if (typeof folder == 'number') { - content.selectedAutoPlaylist = folder; - } else if (genre !== '') { - content.selectedAutoPlaylist = genre; - } else { - content.selectedAutoPlaylist = 'random'; + + getRandomSongs: function (genre, folder) { + var exception = {reason: 'No songs found on the Subsonic server.'}; + var params = { + size: globals.settings.AutoPlaylistSize + }; + if (genre !== undefined && genre !== '' && genre !== 'Random') { + params.genre = genre; } - var genreParams = ''; - if (genre !== '' && genre != 'Random') { - genreParams = '&genre=' + genre; + if (!isNaN(folder)) { + params.musicFolderId = folder; } - var folderParams = ''; - if (typeof folder == 'number' && folder !== '' && folder != 'all') { - //alert(folder); - folderParams = '&musicFolderId=' + folder; - } else if (typeof $rootScope.SelectedMusicFolder.id != 'undefined' && $rootScope.SelectedMusicFolder.id >= 0) { - //alert($rootScope.SelectedMusicFolder.id); - folderParams = '&musicFolderId=' + $rootScope.SelectedMusicFolder.id; - } - $.ajax({ - url: globals.BaseURL() + '/getRandomSongs.view?' + globals.BaseParams() + '&size=' + size + genreParams + folderParams, - method: 'GET', - dataType: globals.settings.Protocol, - timeout: globals.settings.Timeout, - success: function (data) { - if (typeof data["subsonic-response"].randomSongs.song != 'undefined') { - var items = []; - if (data["subsonic-response"].randomSongs.song.length > 0) { - items = data["subsonic-response"].randomSongs.song; - } else { - items[0] = data["subsonic-response"].randomSongs.song; - } - if (action == 'add') { - angular.forEach(items, function (item, key) { - $rootScope.queue.push(map.mapSong(item)); - }); - notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); - } else if (action == 'play') { - $rootScope.queue = []; - angular.forEach(items, function (item, key) { - $rootScope.queue.push(map.mapSong(item)); - }); - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); - notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); - } else { - content.album = []; - content.song = []; - angular.forEach(items, function (item, key) { - content.song.push(map.mapSong(item)); - }); - } - } - deferred.resolve(content); + var deferred = this.subsonicRequest('getRandomSongs.view', { + params: params + }).then(function (subsonicResponse) { + if(subsonicResponse.randomSongs !== undefined && subsonicResponse.randomSongs.song.length > 0) { + var songs = []; + // TODO: Hyz: Add mapSongs to map service + angular.forEach(subsonicResponse.randomSongs.song, function (item) { + songs.push(map.mapSong(item)); + }); + return songs; + } else { + return $q.reject(exception); } }); - return deferred.promise; + return deferred; }, + getPlaylists: function (refresh) { var deferred = $q.defer(); if (globals.settings.Debug) { console.log("LOAD PLAYLISTS"); } @@ -517,6 +537,7 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util return deferred.promise; }, getPlaylist: function (id, action) { + //TODO: Hyz: Test this var deferred = $q.defer(); content.selectedAutoPlaylist = null; content.selectedPlaylist = id; @@ -536,16 +557,16 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util } if (action == 'add') { angular.forEach(items, function (item, key) { - $rootScope.queue.push(map.mapSong(item)); + player.queue.push(map.mapSong(item)); }); notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); } else if (action == 'play') { - $rootScope.queue = []; + player.queue = []; angular.forEach(items, function (item, key) { - $rootScope.queue.push(map.mapSong(item)); + player.queue.push(map.mapSong(item)); }); - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); + var next = player.queue[0]; + player.play(next); notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); } else { content.album = []; @@ -563,61 +584,38 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util }); return deferred.promise; }, - getStarred: function (action, type) { - var exception = {reason: 'Error when contacting the Subsonic server.'}; - var deferred = $q.defer(); - var httpPromise; - if(globals.settings.Protocol === 'jsonp') { - httpPromise = $http.jsonp(globals.BaseURL() + '/getStarred.view?callback=JSON_CALLBACK&' + globals.BaseParams(), - { - timeout: globals.settings.Timeout, - cache: true - }); - } else { - httpPromise = $http.get(globals.BaseURL() + '/getStarred.view?' + globals.BaseParams(), - { - timeout: globals.settings.Timeout, - cache: true - }); - } - httpPromise.success(function(data, status) { - var subsonicResponse = (data['subsonic-response'] !== undefined) ? data['subsonic-response'] : {status: 'failed'}; - if (subsonicResponse.status === 'ok') { + + getStarred: function () { + var deferred = this.subsonicRequest('getStarred.view', { cache: true }) + .then(function (subsonicResponse) { if(angular.equals(subsonicResponse.starred, {})) { - deferred.reject({reason: 'Nothing is starred on the Subsonic server.'}); + return $q.reject({reason: 'Nothing is starred on the Subsonic server.'}); } else { - deferred.resolve(subsonicResponse.starred); + return subsonicResponse.starred; } - } else { - if(subsonicResponse.status === 'failed' && subsonicResponse.error !== undefined) { - exception.subsonicError = subsonicResponse.error; - } - deferred.reject(exception); - } - }).error(function(data, status) { - exception.httpError = status; - deferred.reject(exception); - }); - return deferred.promise; + }); + return deferred; }, + getRandomStarredSongs: function() { - var exception = {reason: 'No starred songs found on the Subsonic server.'}; - var deferred = $q.defer(); - - this.getStarred().then(function (data) { - if(data.song !== undefined && data.song.length > 0) { - // Return random subarray of songs - var randomSongs = [].concat(_.sample(data.song, globals.settings.AutoPlaylistSize)); - deferred.resolve(randomSongs); - } else { - deferred.reject(exception); - } - }, function (reason) { - deferred.reject(reason); - }); - - return deferred.promise; + var deferred = this.getStarred() + .then(function (starred) { + if(starred.song !== undefined && starred.song.length > 0) { + // Return random subarray of songs + var songs = [].concat(_(starred.song).sample(globals.settings.AutoPlaylistSize)); + var mappedSongs = []; + // TODO: Hyz: Add mapSongs to map service + angular.forEach(songs, function (item) { + mappedSongs.push(map.mapSong(item)); + }); + return mappedSongs; + } else { + return $q.reject({reason: 'No starred songs found on the Subsonic server.'}); + } + }); + return deferred; }, + newPlaylist: function (data, event) { var deferred = $q.defer(); var reply = prompt("Choose a name for your new playlist.", ""); @@ -703,7 +701,7 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util } else { items[0] = data["subsonic-response"].genres; } - + $rootScope.Genres = items; $scope.$apply(); } @@ -763,19 +761,19 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util if (action == 'add') { angular.forEach(items, function (item, key) { if (item.status != "skipped") { - $rootScope.queue.push(map.mapPodcast(item)); + player.queue.push(map.mapPodcast(item)); } }); notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); } else if (action == 'play') { - $rootScope.queue = []; + player.queue = []; angular.forEach(items, function (item, key) { if (item.status != "skipped") { - $rootScope.queue.push(map.mapPodcast(item)); + player.queue.push(map.mapPodcast(item)); } }); - var next = $rootScope.queue[0]; - $rootScope.playSong(false, next); + var next = player.queue[0]; + player.play(next); notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); } else { content.album = []; @@ -792,7 +790,20 @@ angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.util } }); return deferred.promise; + }, + + scrobble: function (song) { + var deferred = this.subsonicRequest('scrobble.view', { + params: { + id: song.id, + submisssion: true + } + }).then(function () { + if(globals.settings.Debug) { console.log('Successfully scrobbled song: ' + song.id); } + return true; + }); + return deferred; } // End subsonic }; -}]); \ No newline at end of file +}]); diff --git a/app/subsonic/subsonic-service_test.js b/app/subsonic/subsonic-service_test.js index 8540fdc..553e53e 100644 --- a/app/subsonic/subsonic-service_test.js +++ b/app/subsonic/subsonic-service_test.js @@ -1,160 +1,300 @@ describe("Subsonic service -", function() { - 'use strict'; + 'use strict'; - var subsonic, mockBackend, mockGlobals, response; + var subsonic, mockBackend, mockGlobals; + var response; + beforeEach(function() { + // We redefine it because in some tests we need to alter the settings + mockGlobals = { + settings: { + AutoPlaylistSize: 3, + Username: "Hyzual", + Password: "enc:cGFzc3dvcmQ=", + Protocol: "jsonp", + ApiVersion: "1.10.2", + ApplicationName: "Jamstash", + Timeout: 20000 + }, + BaseURL: function () { + return 'http://demo.subsonic.com/rest'; + }, + }; - var url = 'http://demo.subsonic.com/rest/getStarred.view?'+ - 'callback=JSON_CALLBACK&u=Hyzual&p=enc:cGFzc3dvcmQ=&v=1.10.2&c=Jamstash&f=jsonp'; + module('jamstash.subsonic.service', function ($provide) { + $provide.value('globals', mockGlobals); + // Mock the model service + $provide.decorator('map', function ($delegate) { + $delegate.mapSong = function (argument) { + return argument; + }; + return $delegate; + }); + }); - beforeEach(function() { - // We redefine it because in some tests we need to alter the settings - mockGlobals = { - settings: { - AutoPlaylistSize: 3, - Protocol: 'jsonp' - }, - BaseURL: function () { - return 'http://demo.subsonic.com/rest'; - }, - BaseParams: function () { - return 'u=Hyzual&p=enc:cGFzc3dvcmQ=&v=1.10.2&c=Jamstash&f=jsonp'; - } - }; + inject(function (_subsonic_, $httpBackend) { + subsonic = _subsonic_; + mockBackend = $httpBackend; + }); + response = {"subsonic-response": {status: "ok", version: "1.10.2"}}; + }); - module('jamstash.subsonic.service', function ($provide) { - $provide.value('globals', mockGlobals); - }); + afterEach(function() { + mockBackend.verifyNoOutstandingExpectation(); + mockBackend.verifyNoOutstandingRequest(); + }); - inject(function (_subsonic_, $httpBackend) { - subsonic = _subsonic_; - mockBackend = $httpBackend; - }); - response = {"subsonic-response": {status: "ok", version: "1.10.2"}}; - }); + describe("subsonicRequest -", function() { + var partialUrl, url; + beforeEach(function() { + partialUrl = '/getStarred.view'; + url ='http://demo.subsonic.com/rest/getStarred.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; + }); - afterEach(function() { - mockBackend.verifyNoOutstandingExpectation(); - mockBackend.verifyNoOutstandingRequest(); - }); + it("Given that the Subsonic server is not responding, when I make a request to Subsonic it returns an error object with a message", function() { + mockBackend.expectJSONP(url).respond(503, 'Service Unavailable'); - describe("getStarred -", function() { + var promise = subsonic.subsonicRequest(partialUrl); + mockBackend.flush(); - it("Given that I have 2 starred albums, 1 starred artist and 3 starred songs in my library, when getting everything starred, it returns them all", function() { - response["subsonic-response"].starred = {artist: [{id: 2245}], album: [{id: 1799},{id: 20987}], song: [{id: 2478},{id: 14726},{id: 742}]}; - mockBackend.whenJSONP(url).respond(200, JSON.stringify(response)); + expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', httpError: 503}); + }); - var promise = subsonic.getStarred(); - mockBackend.flush(); + it("Given a missing parameter, when I make a request to Subsonic it returns an error object with a message", function() { + delete mockGlobals.settings.Password; + var missingPasswordUrl = 'http://demo.subsonic.com/rest/getStarred.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&u=Hyzual&v=1.10.2'; + var errorResponse = {"subsonic-response" : { + "status" : "failed", + "version" : "1.10.2", + "error" : {"code" : 10,"message" : "Required parameter is missing."} + }}; + mockBackend.expectJSONP(missingPasswordUrl).respond(200, JSON.stringify(errorResponse)); - expect(promise).toBeResolvedWith({artist: [ - {id: 2245} - ], album: [ - {id: 1799},{id: 20987} - ], song: [ - {id: 2478},{id: 14726},{id: 742} - ] - }); - }); + var promise = subsonic.subsonicRequest(partialUrl); + mockBackend.flush(); - it("Given that the global protocol setting is 'json' and given that I have 3 starred songs in my library, when getting everything starred, it uses GET and returns 3 starred songs", function() { - mockGlobals.settings.Protocol = 'json'; - mockGlobals.BaseParams = function() { return 'u=Hyzual&p=enc:cGFzc3dvcmQ=&v=1.10.2&c=Jamstash&f=json'; }; - var getUrl = 'http://demo.subsonic.com/rest/getStarred.view?' + - 'u=Hyzual&p=enc:cGFzc3dvcmQ=&v=1.10.2&c=Jamstash&f=json'; - response["subsonic-response"].starred = {song: [ - {id: "2147"},{id:"9847"},{id:"214"} - ]}; - mockBackend.expectGET(getUrl).respond(200, JSON.stringify(response)); + expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', subsonicError: {code: 10, message:'Required parameter is missing.'}}); + }); - var promise = subsonic.getStarred(); - mockBackend.flush(); + it("Given a partialUrl that does not start with '/', it adds '/' before it and makes a correct request", function() { + partialUrl = 'getStarred.view'; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); - expect(promise).toBeResolvedWith({song: [ - {id: "2147"},{id:"9847"},{id:"214"}] - }); - }); + subsonic.subsonicRequest(partialUrl); + mockBackend.flush(); + }); - it("Given that there is absolutely nothing starred in my library, when getting everything starred, it returns an error object with a message", function() { - response["subsonic-response"].starred = {}; - mockBackend.whenJSONP(url).respond(200, JSON.stringify(response)); + it("Given $http config params, it does not overwrite them", function() { + partialUrl = 'scrobble.view'; + url ='http://demo.subsonic.com/rest/scrobble.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&id=75&p=enc:cGFzc3dvcmQ%3D&submission=false&u=Hyzual&v=1.10.2'; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); - var promise = subsonic.getStarred(); - mockBackend.flush(); + subsonic.subsonicRequest(partialUrl, { + params: { + id: 75, + submission: false + } + }); + mockBackend.flush(); + }); - expect(promise).toBeRejectedWith({reason: 'Nothing is starred on the Subsonic server.'}); - }); + it("Given that the global protocol setting is 'json', when I make a request to Subsonic it uses GET and does not use the JSON_CALLBACK parameter", function() { + mockGlobals.settings.Protocol = 'json'; + var getUrl = 'http://demo.subsonic.com/rest/getStarred.view?'+ + 'c=Jamstash&f=json&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; + mockBackend.expectGET(getUrl).respond(200, JSON.stringify(response)); - it("Given that the Subsonic server is not responding, when getting everything starred, it returns an error object with a message", function() { - mockBackend.whenJSONP(url).respond(503, 'Service Unavailable'); + var promise = subsonic.subsonicRequest(partialUrl); + mockBackend.flush(); - var promise = subsonic.getStarred(); - mockBackend.flush(); + expect(promise).toBeResolvedWith({status: "ok", version: "1.10.2"}); + }); + }); - expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', httpError: 503}); - }); + it("ping - when I ping Subsonic, it returns Subsonic's response, containing its REST API version", function() { + var url = 'http://demo.subsonic.com/rest/ping.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); - it("Given a missing parameter, when getting the starred songs, it returns an error object with a message", function() { - mockGlobals.BaseParams = function() { return 'u=Hyzual&v=1.10.2&c=Jamstash&f=jsonp';}; - var missingPasswordUrl = 'http://demo.subsonic.com/rest/getStarred.view?'+ - 'callback=JSON_CALLBACK&u=Hyzual&v=1.10.2&c=Jamstash&f=jsonp'; - var errorResponse = {"subsonic-response" : { - "status" : "failed", - "version" : "1.10.2", - "error" : {"code" : 10,"message" : "Required parameter is missing."} - }}; - mockBackend.whenJSONP(missingPasswordUrl).respond(200, errorResponse); + var promise = subsonic.ping(); + mockBackend.flush(); - var promise = subsonic.getStarred(); - mockBackend.flush(); + expect(promise).toBeResolvedWith({status: "ok", version: "1.10.2"}); + }); - expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', subsonicError: {code: 10, message:'Required parameter is missing.'}}); - }); - }); //end getStarred + it("scrobble - Given a song, when I scrobble it, it returns true if there was no error", function() { + var song = { id: 45872 }; + var url = 'http://demo.subsonic.com/rest/scrobble.view?' + + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&id=45872&p=enc:cGFzc3dvcmQ%3D&submisssion=true&u=Hyzual&v=1.10.2'; - describe("getRandomStarredSongs -", function() { - describe("Given that the global setting AutoPlaylist Size is 3", function() { + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); - it("and given that I have more than 3 starred songs in my library, when getting random starred songs, the result should be limited to 3 starred songs", function() { - var library = [ - {id: "11841"},{id: "12061"},{id: "17322"},{id: "1547"},{id: "14785"} - ]; - response["subsonic-response"].starred = {song: library}; - mockBackend.whenJSONP(url).respond(200, JSON.stringify(response)); + var promise = subsonic.scrobble(song); + mockBackend.flush(); - var promise = subsonic.getRandomStarredSongs(); - // We create a spy in order to get the results of the promise - var success = jasmine.createSpy("success"); - promise.then(success); + expect(promise).toBeResolvedWith(true); + }); - mockBackend.flush(); + describe("getStarred -", function() { + var url = 'http://demo.subsonic.com/rest/getStarred.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; - expect(promise).toBeResolved(); - expect(success).toHaveBeenCalled(); - var randomlyPickedSongs = success.calls.mostRecent().args[0]; - for (var i = 0; i < randomlyPickedSongs.length; i++) { - expect(library).toContain(randomlyPickedSongs[i]); - } - }); + it("Given that I have 2 starred albums, 1 starred artist and 3 starred songs in my library, when getting everything starred, it returns them all", function() { + response["subsonic-response"].starred = { + artist: [{id: 2245}], + album: [{id: 1799},{id: 20987}], + song: [{id: 2478},{id: 14726},{id: 742}] + }; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); - it("and given that I have only 1 starred song in my library, when getting random starred songs, it returns my starred song", function() { - response["subsonic-response"].starred = {song: [{id: "11841"}]}; - mockBackend.whenJSONP(url).respond(200, JSON.stringify(response)); + var promise = subsonic.getStarred(); + mockBackend.flush(); - var promise = subsonic.getRandomStarredSongs(); - mockBackend.flush(); + expect(promise).toBeResolvedWith({ + artist: [{id: 2245}], + album: [{id: 1799},{id: 20987}], + song: [{id: 2478},{id: 14726},{id: 742}] + }); + }); - expect(promise).toBeResolvedWith([{id: "11841"}]); - }); + it("Given that there is absolutely nothing starred in my library, when getting everything starred, it returns an error object with a message", function() { + response["subsonic-response"].starred = {}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); - it("and given that I don't have any starred song in my library, when getting random starred songs, it returns an error object with a message", function() { - response["subsonic-response"].starred = {song: []}; - mockBackend.whenJSONP(url).respond(200, JSON.stringify(response)); + var promise = subsonic.getStarred(); + mockBackend.flush(); - var promise = subsonic.getRandomStarredSongs(); - mockBackend.flush(); + expect(promise).toBeRejectedWith({reason: 'Nothing is starred on the Subsonic server.'}); + }); + }); - expect(promise).toBeRejectedWith({reason: 'No starred songs found on the Subsonic server.'}); - }); - }); - }); -}); \ No newline at end of file + describe("getRandomStarredSongs -", function() { + var url = 'http://demo.subsonic.com/rest/getStarred.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; + + describe("Given that the global setting AutoPlaylist Size is 3", function() { + it("and given that I have more than 3 starred songs in my library, when getting random starred songs, it returns 3 starred songs", function() { + var library = [ + {id: 11841},{id: 12061},{id: 17322},{id: 1547},{id: 14785} + ]; + response["subsonic-response"].starred = {song: library}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomStarredSongs(); + // We create a spy in order to get the results of the promise + var success = jasmine.createSpy("success"); + promise.then(success); + + mockBackend.flush(); + + expect(promise).toBeResolved(); + expect(success).toHaveBeenCalled(); + var randomlyPickedSongs = success.calls.mostRecent().args[0]; + for (var i = 0; i < randomlyPickedSongs.length; i++) { + expect(library).toContain(randomlyPickedSongs[i]); + } + }); + + it("and given that I have only 1 starred song in my library, when getting random starred songs, it returns my starred song", function() { + response["subsonic-response"].starred = {song: [{id: 11841}]}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomStarredSongs(); + mockBackend.flush(); + + expect(promise).toBeResolvedWith([{id: 11841}]); + }); + + it("and given that I don't have any starred song in my library, when getting random starred songs, it returns an error object with a message", function() { + response["subsonic-response"].starred = {song: []}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomStarredSongs(); + mockBackend.flush(); + + expect(promise).toBeRejectedWith({reason: 'No starred songs found on the Subsonic server.'}); + }); + }); + }); + + describe("getRandomSongs -", function() { + var url = 'http://demo.subsonic.com/rest/getRandomSongs.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&size=3&u=Hyzual&v=1.10.2'; + + describe("Given that the global setting AutoPlaylist Size is 3", function() { + it("and given that I have more than 3 songs in my library, when getting random songs, it returns 3 songs", function() { + var library = [ + {id: 1143},{id: 5864},{id: 7407},{id: 6471},{id: 59} + ]; + response["subsonic-response"].randomSongs = {song: library}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomSongs(); + // We create a spy in order to get the results of the promise + var success = jasmine.createSpy("success"); + promise.then(success); + + mockBackend.flush(); + + expect(promise).toBeResolved(); + expect(success).toHaveBeenCalled(); + var randomlyPickedSongs = success.calls.mostRecent().args[0]; + for (var i = 0; i < randomlyPickedSongs.length; i++) { + expect(library).toContain(randomlyPickedSongs[i]); + } + }); + + it("and given that I have only 1 song in my library, when getting random songs, it returns that song", function() { + response["subsonic-response"].randomSongs = {song: [{id: 7793}]}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomSongs(); + mockBackend.flush(); + + expect(promise).toBeResolvedWith([{id: 7793}]); + }); + + it("and given that I don't have any song in my library, when getting random songs, it returns an error object with a message", function() { + response["subsonic-response"].randomSongs = {song: []}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomSongs(); + mockBackend.flush(); + + expect(promise).toBeRejectedWith({reason: 'No songs found on the Subsonic server.'}); + }); + + it("and given a genre, when getting random songs, it returns 3 songs from the given genre", function() { + url = 'http://demo.subsonic.com/rest/getRandomSongs.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&genre=Rock&p=enc:cGFzc3dvcmQ%3D&size=3&u=Hyzual&v=1.10.2'; + var library = [ + {id: 9408},{id: 9470},{id: 6932} + ]; + response["subsonic-response"].randomSongs = {song: library}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomSongs('Rock'); + mockBackend.flush(); + + expect(promise).toBeResolvedWith([{id: 9408},{id: 9470},{id: 6932}]); + }); + + it("and given a folder id, when getting random songs, it returns 3 songs from the given folder", function() { + url = 'http://demo.subsonic.com/rest/getRandomSongs.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&musicFolderId=2&p=enc:cGFzc3dvcmQ%3D&size=3&u=Hyzual&v=1.10.2'; + var library = [ + {id: 9232},{id: 3720},{id: 8139} + ]; + response["subsonic-response"].randomSongs = {song: library}; + mockBackend.expectJSONP(url).respond(200, JSON.stringify(response)); + + var promise = subsonic.getRandomSongs('', 2); + mockBackend.flush(); + + expect(promise).toBeResolvedWith([{id: 9232},{id: 3720},{id: 8139}]); + }); + }); + }); +}); diff --git a/app/subsonic/subsonic.html b/app/subsonic/subsonic.html index 3cf4216..b9d13d9 100644 --- a/app/subsonic/subsonic.html +++ b/app/subsonic/subsonic.html @@ -6,7 +6,7 @@ - - \ No newline at end of file + diff --git a/app/subsonic/subsonic.js b/app/subsonic/subsonic.js index 83f0271..1cf2f1c 100644 --- a/app/subsonic/subsonic.js +++ b/app/subsonic/subsonic.js @@ -1,13 +1,13 @@ -/** -* jamstash.subsonic.ctrl Module +/** +* jamstash.subsonic.controller Module * * Access and use the Subsonic Server. The Controller is in charge of relaying the Service's messages to the user through the * notifications. */ -angular.module('jamstash.subsonic.ctrl', ['jamstash.subsonic.service']) +angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'jamstash.player.service']) -.controller('SubsonicCtrl', ['$scope', '$rootScope', '$routeParams', 'utils', 'globals', 'map', 'subsonic', 'notifications', - function SubsonicCtrl($scope, $rootScope, $routeParams, utils, globals, map, subsonic, notifications) { +.controller('SubsonicController', ['$scope', '$rootScope', '$routeParams', 'utils', 'globals', 'map', 'subsonic', 'notifications', 'player', + function ($scope, $rootScope, $routeParams, utils, globals, map, subsonic, notifications, player) { 'use strict'; $scope.settings = globals.settings; @@ -143,15 +143,19 @@ angular.module('jamstash.subsonic.ctrl', ['jamstash.subsonic.service']) $scope.selectNone = function () { $rootScope.selectNone($scope.song); }; + // TODO: Hyz: Replace $scope.playAll = function () { $rootScope.playAll($scope.song); }; + // TODO: Hyz: Replace $scope.playFrom = function (index) { $rootScope.playFrom(index, $scope.song); }; + // TODO: Hyz: Replace $scope.removeSong = function (item) { $rootScope.removeSong(item, $scope.song); }; + // TODO: Hyz: Replace $scope.songsRemoveSelected = function () { subsonic.songsRemoveSelected($scope.selectedSongs).then(function (data) { $scope.album = data.album; @@ -190,12 +194,70 @@ angular.module('jamstash.subsonic.ctrl', ['jamstash.subsonic.service']) $scope.selectedPlaylist = data.selectedPlaylist; }); }; + + /** + * Handles common actions with songs such as displaying them on the scope, adding them to the playing queue + * and playing the first song after adding them to the queue. Displays notifications for songs added to the playing queue + * Also handles error notifications in case of: a service error, a subsonic error or an HTTP error + * @param {Promise} promise a Promise that must be resolved with an array of songs or must be rejected with an object : {'reason': a message that can be displayed to a user, 'httpError': the HTTP error code, 'subsonicError': the error Object sent by Subsonic} + * @param {String} action the action to be taken with the songs. Must be 'add', 'play' or 'display' + * @return {Promise} the original promise passed in first param. That way we can chain it further ! + */ + $scope.requestSongs = function (promise, action) { + promise.then(function (songs) { + if(action === 'play') { + player.emptyQueue().addSongs(songs).playFirstSong(); + notifications.updateMessage(songs.length + ' Song(s) Added to Queue', true); + } else if (action === 'add') { + player.addSongs(songs); + notifications.updateMessage(songs.length + ' Song(s) Added to Queue', true); + } else if (action === 'display') { + $scope.album = []; + $scope.song = songs; + } + }, function (error) { + var errorNotif; + if (error.subsonicError !== undefined) { + errorNotif = error.reason + ' ' + error.subsonicError.message; + } else if (error.httpError !== undefined) { + errorNotif = error.reason + ' HTTP error ' + error.httpError; + } else { + errorNotif = error.reason; + } + notifications.updateMessage(errorNotif, true); + }); + return promise; + }; + $scope.getSongs = function (id, action) { subsonic.getSongs(id, action).then(function (data) { $scope.album = data.album; $scope.song = data.song; }); }; + + $scope.getRandomStarredSongs = function (action) { + var promise = subsonic.getRandomStarredSongs(); + $scope.requestSongs(promise, action); + + $scope.selectedPlaylist = null; + $scope.selectedAutoPlaylist = 'starred'; + }; + + $scope.getRandomSongs = function (action, genre, folder) { + var promise = subsonic.getRandomSongs(genre, folder); + $scope.requestSongs(promise, action); + + $scope.selectedPlaylist = null; + if (!isNaN(folder)) { + $scope.selectedAutoPlaylist = folder; + } else if (genre !== undefined && genre !== '' && genre !== 'Random') { + $scope.selectedAutoPlaylist = genre; + } else { + $scope.selectedAutoPlaylist = 'random'; + } + }; + $scope.getArtistByTag = function (id) { // Gets Artist by ID3 tag $scope.selectedAutoAlbum = null; $scope.selectedArtist = id; @@ -264,6 +326,7 @@ angular.module('jamstash.subsonic.ctrl', ['jamstash.subsonic.service']) $scope.selectedPlaylist = data.selectedPlaylist; }); }; + $scope.getStarred = function (action, type) { subsonic.getStarred(action, type).then(function (data) { $scope.album = data.album; @@ -272,45 +335,7 @@ angular.module('jamstash.subsonic.ctrl', ['jamstash.subsonic.service']) $scope.selectedPlaylist = data.selectedPlaylist; }); }; - $scope.getRandomStarredSongs = function (action) { - subsonic.getRandomStarredSongs() - .then(function (randomStarredSongs) { - var mappedSongs = []; - // Map regardless of the action - for (var i = 0; i < randomStarredSongs.length; i++) { - mappedSongs.push(map.mapSong(randomStarredSongs[i])); - } - if(action === 'play') { - $rootScope.queue = [].concat(mappedSongs); - notifications.updateMessage(mappedSongs.length + ' Song(s) Added to Queue', true); - $rootScope.playSong(false, $rootScope.queue[0]); - } else if (action === 'add') { - $rootScope.queue = $rootScope.queue.concat(mappedSongs); - notifications.updateMessage(mappedSongs.length + ' Song(s) Added to Queue', true); - } else if (action === 'display') { - $scope.album = []; - $scope.song = mappedSongs; - } - }).catch(function (error) { - var errorNotif; - if (error.subsonicError !== undefined) { - errorNotif = error.reason + ' ' + error.subsonicError.message; - } else if (error.httpError !== undefined) { - errorNotif = error.reason + ' HTTP error ' + error.httpError; - } else { - errorNotif = error.reason; - } - notifications.updateMessage(errorNotif, true); - }); - }; - $rootScope.getRandomSongs = function (action, genre, folder) { - subsonic.getRandomSongs(action, genre, folder).then(function (data) { - $scope.album = data.album; - $scope.song = data.song; - $scope.selectedAutoPlaylist = data.selectedAutoPlaylist; - $scope.selectedPlaylist = data.selectedPlaylist; - }); - }; + $scope.newPlaylist = function (data, event) { subsonic.newPlaylist(data, event).then(function (data) { $scope.getPlaylists(true); @@ -448,6 +473,12 @@ angular.module('jamstash.subsonic.ctrl', ['jamstash.subsonic.service']) end = ui.item.index(); $scope.song.splice(end, 0, $scope.song.splice(start, 1)[0]); }; + $scope.playSong = function (song) { + player.play(song); + }; + $scope.addSongToQueue = function(song) { + player.addSong(song); + }; /* Launch on Startup */ $scope.getArtists(); diff --git a/app/subsonic/subsonic_test.js b/app/subsonic/subsonic_test.js index ec2b78e..4691c2d 100644 --- a/app/subsonic/subsonic_test.js +++ b/app/subsonic/subsonic_test.js @@ -1,160 +1,237 @@ describe("Subsonic controller", function() { - 'use strict'; + 'use strict'; - var scope, $rootScope, subsonic, notifications, deferred; + var scope, $rootScope, subsonic, notifications, player, + deferred; - beforeEach(function() { - jasmine.addCustomEqualityTester(angular.equals); + beforeEach(function() { + jasmine.addCustomEqualityTester(angular.equals); - module('jamstash.subsonic.ctrl'); + module('jamstash.subsonic.controller', function ($provide) { + // Mock the player service + $provide.decorator('player', function($delegate) { - inject(function ($controller, _$rootScope_, utils, globals, map, _subsonic_, _notifications_, $q) { - $rootScope = _$rootScope_; - scope = $rootScope.$new(); - subsonic = _subsonic_; - notifications = _notifications_; + $delegate.queue = []; + $delegate.play = jasmine.createSpy("play"); + $delegate.playFirstSong = jasmine.createSpy("playFirstSong"); + return $delegate; + }); - // Mock the functions of the services and the rootscope - deferred = $q.defer(); - spyOn(subsonic, 'getRandomStarredSongs').and.returnValue(deferred.promise); - spyOn(map, 'mapSong').and.callFake(function (song) { - return {id: song.id}; - }); - spyOn(notifications, 'updateMessage'); - $rootScope.playSong = jasmine.createSpy('playSong'); - $rootScope.queue = []; + $provide.decorator('subsonic', function($delegate, $q) { + deferred = $q.defer(); + $delegate.getRandomStarredSongs = jasmine.createSpy("getRandomStarredSongs").and.returnValue(deferred.promise); + $delegate.getRandomSongs = jasmine.createSpy("getRandomSongs").and.returnValue(deferred.promise); + return $delegate; + }); - $controller('SubsonicCtrl', { - $scope: scope, - $rootScope: $rootScope, - $routeParams: {}, - utils: utils, - globals: globals, - map: map, - subsonic: subsonic, - notifications: notifications - }); - }); - }); + $provide.decorator('notifications', function ($delegate) { + $delegate.updateMessage = jasmine.createSpy("updateMessage"); + return $delegate; + }); + }); - //TODO: JMA: It should be the exact same test when getting songs from an album. We aren't testing that it's randomized, that's the service's job. - describe("getRandomStarred -", function() { + inject(function ($controller, _$rootScope_, utils, globals, map, _subsonic_, _notifications_, $q, _player_) { + $rootScope = _$rootScope_; + scope = $rootScope.$new(); + subsonic = _subsonic_; + notifications = _notifications_; + player = _player_; - describe("given that my library contains 3 starred songs, ", function() { - var response = [ - {id:"2548"}, {id:"8986"}, {id:"2986"} - ]; + $controller('SubsonicController', { + $scope: scope, + $rootScope: $rootScope, + $routeParams: {}, + utils: utils, + globals: globals, + map: map, + subsonic: subsonic, + notifications: notifications + }); + }); + }); - it("when displaying random starred songs, it sets the scope with the selected songs", function() { - scope.getRandomStarredSongs('display'); - deferred.resolve(response); - $rootScope.$apply(); + describe("given that my library contains 3 songs, ", function() { + var response; + beforeEach(function() { + response = [ + {id:"2548"}, {id:"8986"}, {id:"2986"} + ]; + }); - expect(subsonic.getRandomStarredSongs).toHaveBeenCalled(); - expect(scope.song).toEqual([ - {id: "2548"}, {id: "8986"}, {id: "2986"} - ]); - }); + describe("get songs -", function() { + beforeEach(function() { + spyOn(scope, "requestSongs"); + }); - it("when adding random starred songs, it adds the selected songs to the queue and notifies the user", function() { - scope.getRandomStarredSongs('add'); - deferred.resolve(response); - $rootScope.$apply(); + it("it can get random starred songs from the subsonic service", function() { + scope.getRandomStarredSongs('whatever action'); + deferred.resolve(response); - expect(subsonic.getRandomStarredSongs).toHaveBeenCalled(); - expect($rootScope.queue).toEqual([ - {id: "2548"}, {id: "8986"}, {id: "2986"} - ]); - expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true); - }); + expect(subsonic.getRandomStarredSongs).toHaveBeenCalled(); + expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'whatever action'); + expect(scope.selectedPlaylist).toBeNull(); + expect(scope.selectedAutoPlaylist).toBe('starred'); + }); - it("when playing random starred songs, it plays the first selected song, empties the queue and fills it with the selected songs, and notifies the user", function() { - $rootScope.queue = [{id: "7666"}]; + it("it can get random songs from all folders or genres from the subsonic service", function() { + scope.getRandomSongs('whatever action'); + deferred.resolve(response); - scope.getRandomStarredSongs('play'); - deferred.resolve(response); - $rootScope.$apply(); + expect(subsonic.getRandomSongs).toHaveBeenCalled(); + expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'whatever action'); + expect(scope.selectedPlaylist).toBeNull(); + expect(scope.selectedAutoPlaylist).toBe('random'); + }); - expect(subsonic.getRandomStarredSongs).toHaveBeenCalled(); - expect($rootScope.playSong).toHaveBeenCalledWith(false, {id: "2548"}); - expect($rootScope.queue).toEqual([ - {id: "2548"}, {id: "8986"}, {id: "2986"} - ]); - expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true); - }); + it("it can get random songs from a given genre from the subsonic service", function() { + scope.getRandomSongs('whatever action', 'Rock'); + deferred.resolve(response); - }); + expect(subsonic.getRandomSongs).toHaveBeenCalledWith('Rock', undefined); + expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'whatever action'); + expect(scope.selectedPlaylist).toBeNull(); + expect(scope.selectedAutoPlaylist).toBe('Rock'); + }); - it("given that I don't have any starred song in my library, when getting random starred songs, it notifies the user with an error message, does not play a song and does not touch the queue", function() { - $rootScope.queue = [{id: "7666"}]; + it("it can get random songs from a given folder id from the subsonic service", function() { + scope.getRandomSongs('whatever action', '', 1); + deferred.resolve(response); - scope.getRandomStarredSongs('whatever action'); - deferred.reject({reason: 'No starred songs found on the Subsonic server.'}); - $rootScope.$apply(); + expect(subsonic.getRandomSongs).toHaveBeenCalledWith('', 1); + expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'whatever action'); + expect(scope.selectedPlaylist).toBeNull(); + expect(scope.selectedAutoPlaylist).toBe(1); + }); + }); - expect(subsonic.getRandomStarredSongs).toHaveBeenCalled(); - expect($rootScope.playSong).not.toHaveBeenCalled(); - expect($rootScope.queue).toEqual([{id: "7666"}]); - expect(notifications.updateMessage).toHaveBeenCalledWith('No starred songs found on the Subsonic server.', true); - }); + describe("requestSongs -", function() { + it("when I display songs, it sets the scope with the selected songs", function() { + scope.requestSongs(deferred.promise, 'display'); + deferred.resolve(response); + scope.$apply(); - it("given that the Subsonic server returns an error, when getting random starred songs, it notifies the user with the error message", function() { - scope.getRandomStarredSongs('whatever action'); - deferred.reject({reason: 'Error when contacting the Subsonic server.', - subsonicError: {code: 10, message:'Required parameter is missing.'} - }); - $rootScope.$apply(); + expect(scope.song).toEqual([ + {id: "2548"}, {id: "8986"}, {id: "2986"} + ]); + }); - expect(subsonic.getRandomStarredSongs).toHaveBeenCalled(); - expect(notifications.updateMessage).toHaveBeenCalledWith('Error when contacting the Subsonic server. Required parameter is missing.', true); - }); + it("when I add songs, it adds the selected songs to the playing queue and notifies the user", function() { + scope.requestSongs(deferred.promise, 'add'); + deferred.resolve(response); + scope.$apply(); - it("given that the Subsonic server is unreachable, when getting random starred songs, it notifies the user with the HTTP error code", function() { - scope.getRandomStarredSongs('whatever action'); - deferred.reject({reason: 'Error when contacting the Subsonic server.', - httpError: 404 - }); - $rootScope.$apply(); + expect(player.queue).toEqual([ + {id: "2548"}, {id: "8986"}, {id: "2986"} + ]); + expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true); + }); - expect(subsonic.getRandomStarredSongs).toHaveBeenCalled(); - expect(notifications.updateMessage).toHaveBeenCalledWith('Error when contacting the Subsonic server. HTTP error 404', true); - }); - }); + it("when I play songs, it plays the first selected song, empties the queue and fills it with the selected songs and it notifies the user", function() { + player.queue = [{id: "7666"}]; - describe("reorders playlists by drag and drop - ", function() { - var mockUI; - beforeEach(function() { - scope.song = [{id: "1084"}, {id: "6810"}, {id: "214"}]; - mockUI = { - item: {} - }; - }); + scope.requestSongs(deferred.promise, 'play'); + deferred.resolve(response); + scope.$apply(); - it("given a song in a list of songs, when I start dragging it, it records what its starting position in the list was", function() { - mockUI.item.index = jasmine.createSpy('index').and.returnValue('1'); - mockUI.item.data = jasmine.createSpy('data'); + expect(player.playFirstSong).toHaveBeenCalled(); + expect(player.queue).toEqual([ + {id: "2548"}, {id: "8986"}, {id: "2986"} + ]); + expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true); + }); - scope.dragStart({}, mockUI); + it("when I request songs, it returns a promise so that I can chain it further", function() { + var success = jasmine.createSpy("success"); - expect(mockUI.item.index).toHaveBeenCalled(); - expect(mockUI.item.data).toHaveBeenCalledWith('start', '1'); - }); + scope.requestSongs(deferred.promise, 'whatever action').then(success); + deferred.resolve(response); + scope.$apply(); - it("given a song in a list of songs that I started dragging, when I drop it, its position in the list of songs has changed", function() { - mockUI.item.index = jasmine.createSpy('index').and.returnValue('0'); - mockUI.item.data = jasmine.createSpy('data').and.returnValue('1'); + expect(success).toHaveBeenCalled(); + }); - scope.dragEnd({}, mockUI); + it("given that I don't have any song in my library, when I request songs, it notifies the user with an error message, does not play a song and does not change the queue", function() { + player.queue = [{id: "7666"}]; - expect(mockUI.item.index).toHaveBeenCalled(); - expect(mockUI.item.data).toHaveBeenCalledWith('start'); - // The second song should now be first - expect(scope.song).toEqual([ - {id: "6810"}, {id: "1084"}, {id: "214"} - ]); - }); - }); + scope.requestSongs(deferred.promise, 'whatever action'); + deferred.reject({reason: 'No songs found on the Subsonic server.'}); + scope.$apply(); - //TODO: JMA: all starred -}); \ No newline at end of file + expect(player.playFirstSong).not.toHaveBeenCalled(); + expect(player.queue).toEqual([{id: "7666"}]); + expect(notifications.updateMessage).toHaveBeenCalledWith('No songs found on the Subsonic server.', true); + }); + + it("given that the Subsonic server returns an error, when I request songs, it notifies the user with the error message", function() { + scope.requestSongs(deferred.promise, 'whatever action'); + deferred.reject({reason: 'Error when contacting the Subsonic server.', + subsonicError: {code: 10, message:'Required parameter is missing.'} + }); + scope.$apply(); + + expect(notifications.updateMessage).toHaveBeenCalledWith('Error when contacting the Subsonic server. Required parameter is missing.', true); + }); + + it("given that the Subsonic server is unreachable, when I request songs, it notifies the user with the HTTP error code", function() { + scope.requestSongs(deferred.promise, 'whatever action'); + deferred.reject({reason: 'Error when contacting the Subsonic server.', + httpError: 404 + }); + scope.$apply(); + + expect(notifications.updateMessage).toHaveBeenCalledWith('Error when contacting the Subsonic server. HTTP error 404', true); + }); + }); + }); + + describe("reorders playlists by drag and drop - ", function() { + var mockUI; + beforeEach(function() { + scope.song = [ + {id: 1084}, + {id: 6810}, + {id: 214} + ]; + mockUI = { + item: {} + }; + }); + + it("given a song in a list of songs, when I start dragging it, it records what its starting position in the list was", function() { + mockUI.item.index = jasmine.createSpy('index').and.returnValue('1'); + mockUI.item.data = jasmine.createSpy('data'); + + scope.dragStart({}, mockUI); + + expect(mockUI.item.index).toHaveBeenCalled(); + expect(mockUI.item.data).toHaveBeenCalledWith('start', '1'); + }); + + it("given a song in a list of songs that I started dragging, when I drop it, its position in the list of songs has changed", function() { + mockUI.item.index = jasmine.createSpy('index').and.returnValue('0'); + mockUI.item.data = jasmine.createSpy('data').and.returnValue('1'); + + scope.dragEnd({}, mockUI); + + expect(mockUI.item.index).toHaveBeenCalled(); + expect(mockUI.item.data).toHaveBeenCalledWith('start'); + // The second song should now be first + expect(scope.song).toEqual([ + {id: 6810}, + {id: 1084}, + {id: 214} + ]); + }); + }); + + it("When I call playSong, it calls play in the player service", function() { + var fakeSong = {"id": 3572}; + + scope.playSong(fakeSong); + + expect(player.play).toHaveBeenCalledWith(fakeSong); + }); + + //TODO: JMA: all starred +}); diff --git a/bower.json b/bower.json index 4601985..95c9223 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "jamstash", - "version": "4.3", + "version": "4.4.0", "description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming", "authors": [ "tsquillario (https://github.com/tsquillario)", @@ -31,14 +31,15 @@ "angular-sanitize": "~1.2.0", "angular-cookies": "~1.2.0", "angular-resource": "~1.2.0", - "jquery": "~2.0.3", - "jquery-ui": "~1.10.3", - "jplayer": "~2.8.4", + "jquery": "~2.0.0", + "jquery-ui": "~1.10.0", + "jplayer": "~2.9.0", "fancybox": "~2.1.4", "notify.js": "<=1.2.2", "jquery.scrollTo": "~1.4.5", "underscore": "~1.7.0", - "angular-underscore": "~0.5.0" + "angular-underscore": "~0.5.0", + "angular-locker": "~1.0.2" }, "overrides": { "fancybox": { @@ -50,7 +51,8 @@ }, "devDependencies": { "angular-mocks": "~1.2.0", - "jasmine-promise-matchers": "~0.0.3" + "jasmine-promise-matchers": "~0.0.3", + "jasmine-fixture": "~1.2.2" }, "ignore": [ "bower_components", @@ -58,4 +60,4 @@ ], "private": true, "appPath": "app" -} \ No newline at end of file +} diff --git a/karma.conf.js b/karma.conf.js index 85c3f11..9ce4127 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -32,8 +32,10 @@ module.exports = function(config) { 'bower_components/jquery.scrollTo/jquery.scrollTo.js', 'bower_components/underscore/underscore.js', 'bower_components/angular-underscore/angular-underscore.js', + 'bower_components/angular-locker/dist/angular-locker.min.js', 'bower_components/angular-mocks/angular-mocks.js', 'bower_components/jasmine-promise-matchers/dist/jasmine-promise-matchers.js', + 'bower_components/jasmine-fixture/dist/jasmine-fixture.js', // endbower 'app/**/*.js', 'app/**/*_test.js' diff --git a/manifest.json b/manifest.json index 6e2f9de..33b1724 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Jamstash", "description": "HTML5 Player for Subsonic & Archive.org", - "version": "4.2.3", + "version": "4.4.0", "app": { "launch": { "web_url": "http://jamstash.com" diff --git a/package.json b/package.json index 98e7dd6..b2ed8a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jamstash", - "version": "4.3", + "version": "4.4.0", "description": "HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming", "author": "Trevor Squillario (https://github.com/tsquillario)", "contributors": [