diff --git a/app/common/main-controller.js b/app/common/main-controller.js index 55315f5..0aa3bbe 100644 --- a/app/common/main-controller.js +++ b/app/common/main-controller.js @@ -10,7 +10,6 @@ angular.module('JamStash') $rootScope.Genres = []; $rootScope.Messages = []; - $rootScope.SelectedMusicFolder = ""; $rootScope.unity = null; $scope.Page = Page; $rootScope.loggedIn = function () { diff --git a/app/common/persistence-service.js b/app/common/persistence-service.js index 02c145f..85236f2 100644 --- a/app/common/persistence-service.js +++ b/app/common/persistence-service.js @@ -77,6 +77,19 @@ angular.module('jamstash.persistence', ['angular-locker', locker.forget('Volume'); }; + /* Manage selected music folder */ + this.getSelectedMusicFolder = function () { + return locker.get('MusicFolders'); + }; + + this.saveSelectedMusicFolder = function (selectedMusicFolder) { + locker.put('MusicFolders', selectedMusicFolder); + }; + + this.deleteSelectedMusicFolder = function () { + locker.forget('MusicFolders'); + }; + /* Manage user settings */ this.getSettings = function () { // If the latest version from changelog.json is newer than the version stored in local storage, diff --git a/app/common/persistence-service_test.js b/app/common/persistence-service_test.js index 5512dee..93e32b0 100644 --- a/app/common/persistence-service_test.js +++ b/app/common/persistence-service_test.js @@ -154,6 +154,48 @@ describe("Persistence service", function() { expect(locker.forget).toHaveBeenCalledWith('Volume'); }); + describe("getSelectedMusicFolder() -", function() { + it("Given a previously saved selected music folder in local storage, when I get the saved music folder, then an object containing the id and name of the selected music folder will be returned", function() { + fakeStorage = { + 'MusicFolders': { + 'id': 74, + 'name': 'kooliman unhurled' + } + }; + + var selectedMusicFolder = persistence.getSelectedMusicFolder(); + + expect(locker.get).toHaveBeenCalledWith('MusicFolders'); + expect(selectedMusicFolder).toEqual({ + 'id': 74, + 'name': 'kooliman unhurled' + }); + }); + + it("Given that no selected music folder was previously saved in local storage, when I get the saved music folder, then undefined will be returned", function() { + var selectedMusicFolder = persistence.getSelectedMusicFolder(); + + expect(locker.get).toHaveBeenCalledWith('MusicFolders'); + expect(selectedMusicFolder).toBeUndefined(); + }); + }); + + it("saveSelectedMusicFolder() - given an object containing the id and name of the selected music folder, when I save the music folder, then it will be set in local storage", function() { + persistence.saveSelectedMusicFolder({ + id: 41, + name: 'parlormaid carcinolytic' + }); + expect(locker.put).toHaveBeenCalledWith('MusicFolders', { + id: 41, + name: 'parlormaid carcinolytic' + }); + }); + + it("deleteSelectedMusicFolder() - when I delete the selected music folder, then it will be erased from local storage", function() { + persistence.deleteSelectedMusicFolder(); + expect(locker.forget).toHaveBeenCalledWith('MusicFolders'); + }); + describe("getSettings() -", function() { beforeEach(function() { spyOn(persistence, 'upgradeVersion'); diff --git a/app/subsonic/subsonic-service.js b/app/subsonic/subsonic-service.js index 09387e3..54d33d7 100644 --- a/app/subsonic/subsonic-service.js +++ b/app/subsonic/subsonic-service.js @@ -34,6 +34,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', var subsonicService = { showIndex: $rootScope.showIndex, showPlaylist: showPlaylist, + //TODO: Hyz: Do we still need this ? it's only used in the songpreview directive getSongTemplate: function (callback) { var id = '16608'; var url = globals.BaseURL() + '/getMusicDirectory.view?' + globals.BaseParams() + '&id=' + id; @@ -121,13 +122,21 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', return subsonicService.subsonicRequest('ping.view'); }, + getMusicFolders: function () { + var exception = {reason: 'No music folder found on the Subsonic server.'}; + var promise = subsonicService.subsonicRequest('getMusicFolders.view') + .then(function (subsonicResponse) { + if (subsonicResponse.musicFolders !== undefined && subsonicResponse.musicFolders.musicFolder !== undefined) { + return [].concat(subsonicResponse.musicFolders.musicFolder); + } else { + return $q.reject(exception); + } + }); + return promise; + }, + getArtists: function (folder) { var exception = {reason: 'No artist found on the Subsonic server.'}; - // TODO: Hyz: Move loading / saving the music folder to persistence-service - if (isNaN(folder) && utils.getValue('MusicFolders')) { - var musicFolder = angular.fromJson(utils.getValue('MusicFolders')); - folder = musicFolder.id; - } var params; if (!isNaN(folder)) { params = { diff --git a/app/subsonic/subsonic-service_test.js b/app/subsonic/subsonic-service_test.js index 965f458..fa2b412 100644 --- a/app/subsonic/subsonic-service_test.js +++ b/app/subsonic/subsonic-service_test.js @@ -1,7 +1,7 @@ describe("Subsonic service -", function() { 'use strict'; - var subsonic, mockBackend, mockGlobals, $q, + var $q, mockBackend, subsonic, mockGlobals, response, url; beforeEach(function() { // We redefine it because in some tests we need to alter the settings @@ -624,6 +624,55 @@ describe("Subsonic service -", function() { }); }); + describe("getMusicFolders() -", function() { + beforeEach(function() { + url = 'http://demo.subsonic.com/rest/getMusicFolders.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; + }); + + it("Given that there were 2 music folders in my library, when I get the music folders, then a promise will be resolved with an array of 2 music folders", function() { + response["subsonic-response"].musicFolders = { + musicFolder: [ + { id: 80, name: "languageless" }, + { id: 38, name: "mala" } + ] + }; + mockBackend.expectJSONP(url).respond(JSON.stringify(response)); + + var promise = subsonic.getMusicFolders(); + mockBackend.flush(); + + expect(promise).toBeResolvedWith([ + { id: 80, name: "languageless" }, + { id: 38, name: "mala" } + ]); + }); + + it("Given that there was 1 music folder in my Madsonic library, when I get the music folders, then a promise will be resolved with an array of 1 music folder", function() { + response["subsonic-response"].musicFolders = { + musicFolder: {id: 56, name: "dite"} + }; + mockBackend.expectJSONP(url).respond(JSON.stringify(response)); + + var promise = subsonic.getMusicFolders(); + mockBackend.flush(); + + expect(promise).toBeResolvedWith([ + { id: 56, name: "dite"} + ]); + }); + + it("Given that there wasn't any music folder in my library, when I get the music folders, then a promise will be rejected with an error message", function() { + response["subsonic-response"].musicFolders = {}; + mockBackend.expectJSONP(url).respond(JSON.stringify(response)); + + var promise = subsonic.getMusicFolders(); + mockBackend.flush(); + + expect(promise).toBeRejectedWith({reason: 'No music folder found on the Subsonic server.'}); + }); + }); + describe("getArtists() -", function() { beforeEach(function() { url = 'http://demo.subsonic.com/rest/getIndexes.view?'+ @@ -658,6 +707,15 @@ describe("Subsonic service -", function() { expect(promise).toBeResolvedWith(response["subsonic-response"].indexes); }); + it("Given a folder id, when I get the artists, then it will be used as parameter in the request", function() { + url = 'http://demo.subsonic.com/rest/getIndexes.view?'+ + 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&musicFolderId=54'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; + mockBackend.expectJSONP(url).respond(JSON.stringify(response)); + + subsonic.getArtists(54); + mockBackend.flush(); + }); + it("Given that there were 2 artist at the top level of my Madsonic library, when I get the artists, then a promise will be resolved with an array of two artist", function() { response["subsonic-response"].indexes = { shortcut: { id: 433, name: "Podcast" }, diff --git a/app/subsonic/subsonic.html b/app/subsonic/subsonic.html index 329bfef..e8a9597 100644 --- a/app/subsonic/subsonic.html +++ b/app/subsonic/subsonic.html @@ -86,7 +86,9 @@
- +
A-Z
diff --git a/app/subsonic/subsonic.js b/app/subsonic/subsonic.js index e5cfb57..d13c190 100644 --- a/app/subsonic/subsonic.js +++ b/app/subsonic/subsonic.js @@ -4,10 +4,10 @@ * 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.controller', ['jamstash.subsonic.service', 'jamstash.player.service']) +angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'jamstash.player.service', 'jamstash.persistence']) -.controller('SubsonicController', ['$scope', '$rootScope', '$routeParams', '$window', 'utils', 'globals', 'map', 'subsonic', 'notifications', 'player', - function ($scope, $rootScope, $routeParams, $window, utils, globals, map, subsonic, notifications, player) { +.controller('SubsonicController', ['$scope', '$rootScope', '$routeParams', '$window', 'utils', 'globals', 'map', 'subsonic', 'notifications', 'player', 'persistence', + function ($scope, $rootScope, $routeParams, $window, utils, globals, map, subsonic, notifications, player, persistence) { 'use strict'; $scope.settings = globals.settings; @@ -141,13 +141,20 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja } } }); - $rootScope.$watch("SelectedMusicFolder", function (newValue, oldValue) { + + $scope.$watch("SelectedMusicFolder", function (newValue, oldValue) { if (newValue !== oldValue) { - // TODO: Hyz: Move loading / saving the music folder to persistence-service - utils.setValue('MusicFolders', angular.toJson(newValue), true); - $scope.getArtists(newValue.id); + var folderId; + if (newValue) { + folderId = newValue.id; + persistence.saveSelectedMusicFolder(newValue); + } else { + persistence.deleteSelectedMusicFolder(); + } + $scope.getArtists(folderId); } }); + $scope.searching = { query: "", typeId: globals.settings.DefaultSearchType, @@ -201,7 +208,12 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja $scope.song = data.song; }); }; + $scope.getArtists = function (folder) { + var savedFolder = $scope.SelectedMusicFolder; + if (isNaN(folder) && savedFolder) { + folder = savedFolder.id; + } var promise = subsonic.getArtists(folder); $scope.handleErrors(promise).then(function (data) { $scope.index = data.index; @@ -214,8 +226,9 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja } }); }; + $scope.refreshArtists = function () { - utils.setValue('MusicFolders', null, true); + $scope.SelectedMusicFolder = undefined; $scope.getArtists(); $scope.getPlaylists(); }; @@ -594,43 +607,19 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja }; $scope.getMusicFolders = function () { - $.ajax({ - url: globals.BaseURL() + '/getMusicFolders.view?' + globals.BaseParams(), - method: 'GET', - dataType: globals.settings.Protocol, - timeout: globals.settings.Timeout, - success: function (data) { - if (data["subsonic-response"].musicFolders.musicFolder !== undefined) { - var folders = []; - if (data["subsonic-response"].musicFolders.musicFolder.length > 0) { - folders = data["subsonic-response"].musicFolders.musicFolder; - } else { - folders[0] = data["subsonic-response"].musicFolders.musicFolder; - } - - folders.unshift({ - "id": -1, - "name": "All Folders" - }); - $rootScope.MusicFolders = folders; - if (utils.getValue('MusicFolders')) { - var folder = angular.fromJson(utils.getValue('MusicFolders')); - var i = 0, index = ""; - angular.forEach($rootScope.MusicFolders, function (item, key) { - if (item.id == folder.id) { - index = i; - } - i++; - }); - $rootScope.SelectedMusicFolder = $rootScope.MusicFolders[index]; - } else { - $rootScope.SelectedMusicFolder = $rootScope.MusicFolders[0]; - } - $scope.$apply(); + var promise = subsonic.getMusicFolders(); + $scope.handleErrors(promise).then(function (musicFolders) { + var folders = musicFolders; + $scope.MusicFolders = folders; + var savedFolder = persistence.getSelectedMusicFolder(); + if (savedFolder) { + if (_.findIndex(folders, {id: savedFolder.id}) !== -1) { + $scope.SelectedMusicFolder = savedFolder; } } }); }; + /** * Change the order of playlists through jQuery UI's sortable */ @@ -650,12 +639,12 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja }; /* Launch on Startup */ + $scope.getMusicFolders(); $scope.getArtists(); $scope.getPlaylists(); $scope.getGenres(); $scope.getPodcasts(); $scope.openDefaultSection(); - $scope.getMusicFolders(); if ($routeParams.artistId && $routeParams.albumId) { $scope.getAlbumByTag($routeParams.albumId); } else if ($routeParams.artistId) { diff --git a/app/subsonic/subsonic_test.js b/app/subsonic/subsonic_test.js index 75c1941..856f819 100644 --- a/app/subsonic/subsonic_test.js +++ b/app/subsonic/subsonic_test.js @@ -2,14 +2,14 @@ describe("Subsonic controller", function() { 'use strict'; var scope, $rootScope, $controller, $window, - subsonic, notifications, player, controllerParams, deferred; + subsonic, notifications, player, utils, persistence, controllerParams, deferred; beforeEach(function() { jasmine.addCustomEqualityTester(angular.equals); module('jamstash.subsonic.controller'); - inject(function (_$controller_, _$rootScope_, utils, globals, map, $q) { + inject(function (_$controller_, _$rootScope_, globals, map, $q) { $rootScope = _$rootScope_; scope = $rootScope.$new(); deferred = $q.defer(); @@ -19,34 +19,42 @@ describe("Subsonic controller", function() { "confirm" ]); notifications = jasmine.createSpyObj("notifications", ["updateMessage"]); + utils = jasmine.createSpyObj("utils", ["getValue"]); + persistence = jasmine.createSpyObj("persistence", [ + "getSelectedMusicFolder", + "saveSelectedMusicFolder", + "deleteSelectedMusicFolder" + ]); // Mock the subsonic service subsonic = jasmine.createSpyObj("subsonic", [ - "getSongs", - "recursiveGetSongs", + "deletePlaylist", "getAlbums", "getArtists", "getGenres", - "getPlaylists", - "getPodcasts", - "getRandomStarredSongs", - "getRandomSongs", + "getMusicFolders", "getPlaylist", - "newPlaylist", - "deletePlaylist", - "savePlaylist", + "getPlaylists", "getPodcast", - "search" + "getPodcasts", + "getRandomSongs", + "getRandomStarredSongs", + "getSongs", + "newPlaylist", + "recursiveGetSongs", + "savePlaylist", + "search", ]); // We make them return different promises and use our deferred variable only when testing // a particular function, so that they stay isolated - subsonic.getSongs.and.returnValue($q.defer().promise); - subsonic.recursiveGetSongs.and.returnValue($q.defer().promise); subsonic.getAlbums.and.returnValue($q.defer().promise); subsonic.getArtists.and.returnValue($q.defer().promise); subsonic.getGenres.and.returnValue($q.defer().promise); + subsonic.getMusicFolders.and.returnValue($q.defer().promise); subsonic.getPlaylists.and.returnValue($q.defer().promise); subsonic.getPodcasts.and.returnValue($q.defer().promise); + subsonic.getSongs.and.returnValue($q.defer().promise); + subsonic.recursiveGetSongs.and.returnValue($q.defer().promise); subsonic.showIndex = false; // Mock the player service @@ -73,7 +81,8 @@ describe("Subsonic controller", function() { map: map, subsonic: subsonic, notifications: notifications, - player: player + player: player, + persistence: persistence }; }); }); @@ -82,6 +91,7 @@ describe("Subsonic controller", function() { beforeEach(function() { $controller('SubsonicController', controllerParams); scope.selectedPlaylist = null; + scope.$apply(); }); describe("getSongs -", function() { @@ -513,12 +523,36 @@ describe("Subsonic controller", function() { //TODO: Hyz: all starred - describe("When I load the artists,", function() { + describe("", function() { + beforeEach(function() { + spyOn(scope, "getArtists"); + }); + + it("Given no previously selected music folder, when I select a music folder, then it will be stored in persistence and the artists will be loaded from subsonic", function() { + scope.SelectedMusicFolder = { id: 22, name: "Cascadia" }; + scope.$apply(); + + expect(persistence.saveSelectedMusicFolder).toHaveBeenCalledWith({ id: 22, name: "Cascadia" }); + expect(scope.getArtists).toHaveBeenCalledWith(22); + }); + + it("Given a previously selected music folder, when I select the 'All Folders' (undefined) music folder, then the stored value will be deleted from persistence and all the artists will be loaded from subsonic", function() { + scope.SelectedMusicFolder = { id: 23, name: "grantable" }; + scope.$apply(); + scope.SelectedMusicFolder = undefined; + scope.$apply(); + + expect(persistence.deleteSelectedMusicFolder).toHaveBeenCalled(); + expect(scope.getArtists).not.toHaveBeenCalledWith(jasmine.any(Number)); + }); + }); + + describe("getArtists() -", function() { beforeEach(function() { subsonic.getArtists.and.returnValue(deferred.promise); }); - it("Given that there are songs in the library, it loads the artists and publishes them to the scope", function() { + it("Given that there were artists in the library, when I load the artists, then subsonic will be queried an index array containing the artists and a shortcut array containing the shortcuts (such as Podcasts) will be publisehd to the scope", function() { scope.getArtists(); deferred.resolve({ index: [ @@ -539,7 +573,15 @@ describe("Subsonic controller", function() { ]); }); - it("Given that there aren't any songs in the library, when loading indexes, it notifies the user with an error message", function() { + it("Given no folder id and given a selected music folder had been set in the scope, when I get the artists, then the selected music folder's id will be used as parameter to subsonic service", function() { + scope.SelectedMusicFolder = { id: 62, name: "dollardee" }; + + scope.getArtists(); + + expect(subsonic.getArtists).toHaveBeenCalledWith(62); + }); + + it("Given that there weren't any artist in the library, when I load the artists, then a notification with an error message will be displayed", function() { scope.getArtists(); deferred.reject({reason: 'No artist found on the Subsonic server.'}); scope.$apply(); @@ -549,13 +591,24 @@ describe("Subsonic controller", function() { expect(notifications.updateMessage).toHaveBeenCalledWith('No artist found on the Subsonic server.', true); }); - it("it lets handleErrors handle HTTP and Subsonic errors", function() { + it("Given that the server was unreachable, when I get the music folders, then handleErrors() will deal with the error", function() { spyOn(scope, 'handleErrors').and.returnValue(deferred.promise); scope.getArtists(); expect(scope.handleErrors).toHaveBeenCalledWith(deferred.promise); }); }); + it("refreshArtists() - When I refresh the artists, then the selected music folder will be reset to undefined and the artists and playlists will be loaded", function() { + spyOn(scope, "getArtists"); + spyOn(scope, "getPlaylists"); + + scope.refreshArtists(); + + expect(scope.SelectedMusicFolder).toBeUndefined(); + expect(scope.getArtists).toHaveBeenCalled(); + expect(scope.getPlaylists).toHaveBeenCalled(); + }); + describe("When I load the playlists,", function() { beforeEach(function() { subsonic.getPlaylists.and.returnValue(deferred.promise); @@ -631,7 +684,7 @@ describe("Subsonic controller", function() { expect(scope.getPlaylists).toHaveBeenCalled(); }); - it("Given no selected playlist, when I try to delete a playlist, an error message will be notified", function() { + it("Given no selected playlist, when I try to delete a playlist, an error notification will be displayed", function() { scope.selectedPlaylist = null; scope.deletePlaylist(); @@ -663,7 +716,7 @@ describe("Subsonic controller", function() { expect(notifications.updateMessage).toHaveBeenCalledWith('Playlist Updated!', true); }); - it("Given no selected playlist, when I try to save a playlist, an error message will be notified", function() { + it("Given no selected playlist, when I try to save a playlist, an error notification will be displayed", function() { scope.selectedPlaylist = null; scope.savePlaylist(); @@ -705,6 +758,64 @@ describe("Subsonic controller", function() { }); }); + describe("getMusicFolders", function() { + beforeEach(function() { + subsonic.getMusicFolders.and.returnValue(deferred.promise); + }); + + it("Given that there were music folders in the library, when I get the music folders, then the folders will be published to the scope", function() { + scope.getMusicFolders(); + deferred.resolve([ + { id: 74, name: "scirrhosis"}, + { id: 81, name: "drooper"} + ]); + scope.$apply(); + + expect(subsonic.getMusicFolders).toHaveBeenCalled(); + expect(scope.MusicFolders).toEqual([ + { id: 74, name: "scirrhosis"}, + { id: 81, name: "drooper"} + ]); + }); + + describe("Given that there was a selected music folder previously saved in persistence", function() { + it("and that it matched one of the music folders returned by subsonic, when I get the music folders, then the scope's selected music folder will be set", function() { + persistence.getSelectedMusicFolder.and.returnValue({ id: 79, name: "dismember" }); + + scope.getMusicFolders(); + deferred.resolve([ + { id: 93, name: "illish" }, + { id: 79, name: "dismember" } + ]); + scope.$apply(); + + expect(scope.SelectedMusicFolder).toEqual({ id: 79, name: "dismember" }); + }); + + it("and that it didn't match one of the music folders returned by subsonic, when I get the music folders, then the scope's selected music folder will be undefined", function() { + persistence.getSelectedMusicFolder.and.returnValue({ id: 49, name: "metafulminuric" }); + + scope.getMusicFolders(); + deferred.resolve([ + { id: 94, name: "dorsiflexor" } + ]); + scope.$apply(); + + expect(scope.SelectedMusicFolder).toBeUndefined(); + }); + }); + + it("Given that there weren't any music folder in the library, when I get the music folders, then handleErrors() will deal with the error", function() { + spyOn(scope, 'handleErrors').and.returnValue(deferred.promise); + + scope.getMusicFolders(); + deferred.reject({reason: 'No music folder found on the Subsonic server.'}); + scope.$apply(); + + expect(scope.handleErrors).toHaveBeenCalledWith(deferred.promise); + }); + }); + describe("search() -", function() { beforeEach(function() { subsonic.search.and.returnValue(deferred.promise);