diff --git a/app/subsonic/subsonic-service.js b/app/subsonic/subsonic-service.js index b2bc338..077a232 100644 --- a/app/subsonic/subsonic-service.js +++ b/app/subsonic/subsonic-service.js @@ -138,7 +138,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', }).then(function (subsonicResponse) { if(subsonicResponse.indexes !== undefined && (subsonicResponse.indexes.index !== undefined || subsonicResponse.indexes.shortcut !== undefined)) { // Make sure shortcut, index and each index's artist are arrays - // because Madsonic will return objects and not arrays if there is only 1 artist + // because Madsonic will return an object when there's only one element var formattedResponse = {}; formattedResponse.shortcut = [].concat(subsonicResponse.indexes.shortcut); formattedResponse.index = [].concat(subsonicResponse.indexes.index); @@ -164,6 +164,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', params: params }).then(function (subsonicResponse) { if(subsonicResponse.directory.child !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var childArray = [].concat(subsonicResponse.directory.child); if (childArray.length > 0) { content.song = []; @@ -203,6 +204,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', params: params }).then(function (subsonicResponse) { if(subsonicResponse.albumList.album !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var albumArray = [].concat(subsonicResponse.albumList.album); if (albumArray.length > 0) { content.song = []; @@ -253,52 +255,25 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', }); return deferred.promise; }, - getSongs: function (id, action) { - var exception = {reason: 'No songs found on the Subsonic server.'}; + + getSongs: function (id) { + var exception = {reason: 'This directory is empty.'}; var promise = subsonicService.subsonicRequest('getMusicDirectory.view', { params: { id: id } }).then(function (subsonicResponse) { if(subsonicResponse.directory.child !== undefined) { - var items = [].concat(subsonicResponse.directory.child); - if (items.length > 0) { - content.selectedAlbum = id; - if (action == 'add') { - angular.forEach(items, function (item, key) { - player.queue.push(map.mapSong(item)); - }); - notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); - } else if (action == 'play') { - player.queue = []; - angular.forEach(items, function (item, key) { - player.queue.push(map.mapSong(item)); - }); - var next = player.queue[0]; - player.play(next); - notifications.updateMessage(items.length + ' Song(s) Added to Queue', true); - } else { - if (subsonicResponse.directory.id != 'undefined') { - var albumId = subsonicResponse.directory.id; - var albumName = subsonicResponse.directory.name; - if (content.breadcrumb.length > 0) { content.breadcrumb.splice(1, (content.breadcrumb.length - 1)); } - content.breadcrumb.push({ 'type': 'album', 'id': albumId, 'name': albumName }); - } - content.song = []; - content.album = []; - var albums = []; - angular.forEach(items, function (item, key) { - if (item.isDir) { - albums.push(map.mapAlbum(item)); - } else { - content.song.push(map.mapSong(item)); - } - }); - if (albums.length > 0) { - content.album = albums; - } - } - return content; + // Make sure this is an array using concat because Madsonic will return an object when there's only one element + var children = [].concat(subsonicResponse.directory.child); + if (children.length > 0) { + var allChildren = _(children).partition(function (item) { + return item.isDir; + }); + return { + directories: map.mapAlbums(allChildren[0]), + songs: map.mapSongs(allChildren[1]) + }; } } // We end up here for every else @@ -307,6 +282,41 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', return promise; }, + // This is used when we add or play a directory, so we recursively get all its contents + recursiveGetSongs: function (id) { + var deferred = $q.defer(); + // We first use getSongs() to get the contents of the root directory + subsonicService.getSongs(id).then(function (data) { + var directories = data.directories; + var songs = data.songs; + // If there are only songs, we return them immediately: this is a leaf directory and the end of the recursion + if (directories.length === 0) { + deferred.resolve(songs); + } else { + // otherwise, for each directory, we call ourselves + var promises = []; + angular.forEach(directories, function (dir) { + var subdirectoryRequest = subsonicService.recursiveGetSongs(dir.id).then(function (data) { + // This is where we join all the songs together in a single array + return songs.concat(data); + }); + promises.push(subdirectoryRequest); + }); + // since all of this is asynchronous, we need to wait for all the requests to finish by using $q.all() + var allRequestsFinished = $q.all(promises).then(function (data) { + // and since $q.all() wraps everything in another array, we use flatten() to end up with only one array of songs + return _(data).flatten(); + }); + deferred.resolve(allRequestsFinished); + } + }, function () { + // Even if getSongs returns an error, we resolve with an empty array. Otherwise one empty directory somewhere + // would keep us from playing all the songs of a directory recursively + deferred.resolve([]); + }); + return deferred.promise; + }, + search: function (query, type) { if(_([0, 1, 2]).contains(type)) { var promise = subsonicService.subsonicRequest('search2.view', { @@ -315,6 +325,8 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', } }).then(function (subsonicResponse) { if (!_.isEmpty(subsonicResponse.searchResult2)) { + // Make sure that song, album and artist are arrays using concat + // because Madsonic will return an object when there's only one element switch (type) { case 0: if (subsonicResponse.searchResult2.song !== undefined) { @@ -357,6 +369,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', params: params }).then(function (subsonicResponse) { if(subsonicResponse.randomSongs !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var songArray = [].concat(subsonicResponse.randomSongs.song); if (songArray.length > 0) { return map.mapSongs(songArray); @@ -384,6 +397,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', var promise = subsonicService.getStarred() .then(function (starred) { if(starred.song !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var songArray = [].concat(starred.song); if (songArray.length > 0) { // Return random subarray of songs @@ -402,6 +416,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', var promise = subsonicService.subsonicRequest('getPlaylists.view') .then(function (subsonicResponse) { if(subsonicResponse.playlists.playlist !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var playlistArray = [].concat(subsonicResponse.playlists.playlist); if (playlistArray.length > 0) { var allPlaylists = _(playlistArray).partition(function (item) { @@ -424,6 +439,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', } }).then(function (subsonicResponse) { if (subsonicResponse.playlist.entry !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var entryArray = [].concat(subsonicResponse.playlist.entry); if (entryArray.length > 0) { return map.mapSongs(entryArray); @@ -514,6 +530,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', }) .then(function (subsonicResponse) { if (subsonicResponse.podcasts !== undefined && subsonicResponse.podcasts.channel !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var channelArray = [].concat(subsonicResponse.podcasts.channel); if (channelArray.length > 0) { return channelArray; @@ -535,10 +552,12 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils', }).then(function (subsonicResponse) { var episodes = []; if (subsonicResponse.podcasts.channel !== undefined) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var channelArray = [].concat(subsonicResponse.podcasts.channel); if (channelArray.length > 0) { var channel = channelArray[0]; if (channel !== null && channel.id === id) { + // Make sure this is an array using concat because Madsonic will return an object when there's only one element var episodesArray = [].concat(channel.episode); episodes = _(episodesArray).filter(function (episode) { return episode.status === "completed"; diff --git a/app/subsonic/subsonic-service_test.js b/app/subsonic/subsonic-service_test.js index 3f5603d..a576c3d 100644 --- a/app/subsonic/subsonic-service_test.js +++ b/app/subsonic/subsonic-service_test.js @@ -1,8 +1,8 @@ describe("Subsonic service -", function() { 'use strict'; - var subsonic, mockBackend, mockGlobals, - response; + var subsonic, mockBackend, mockGlobals, $q, + response, url; beforeEach(function() { // We redefine it because in some tests we need to alter the settings mockGlobals = { @@ -46,9 +46,10 @@ describe("Subsonic service -", function() { }); }); - inject(function (_subsonic_, $httpBackend) { + inject(function (_subsonic_, $httpBackend, _$q_) { subsonic = _subsonic_; mockBackend = $httpBackend; + $q = _$q_; }); response = {"subsonic-response": {status: "ok", version: "1.10.2"}}; }); @@ -59,7 +60,7 @@ describe("Subsonic service -", function() { }); describe("subsonicRequest() -", function() { - var partialUrl, url; + var partialUrl; beforeEach(function() { partialUrl = '/getStarred.view'; url ='http://demo.subsonic.com/rest/getStarred.view?'+ @@ -129,7 +130,6 @@ describe("Subsonic service -", function() { }); describe("getAlbums() -", function() { - var url; beforeEach(function() { url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=21'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; @@ -182,62 +182,127 @@ describe("Subsonic service -", function() { }); }); + //TODO: Hyz: Rename into getDirectory(), because we don't know if there will be songs or other directories in it describe("getSongs() -", function() { - var url; beforeEach(function() { - var url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+ + url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=209'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; }); - it("Given that there were 2 songs in the given directory id in my library, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs", function() { + it("Given a directory containing 2 songs and 1 subdirectory and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs and an array of 1 subdirectory", function() { response["subsonic-response"].directory = { child: [ { id: 778 }, - { id: 614 } + { id: 614 }, + { id: 205, isDir: true} ] }; mockBackend.expectJSONP(url).respond(JSON.stringify(response)); var promise = subsonic.getSongs(209); - //TODO: Hyz: Replace with toBeResolvedWith() when getSongs() is refactored - var success = function (data) { - expect(data.album).toEqual([]); - expect(data.song).toEqual([ - { id: 778 }, - { id: 614 } - ]); - }; - promise.then(success); - mockBackend.flush(); - expect(promise).toBeResolved(); + expect(promise).toBeResolvedWith({ + directories: [{ id: 205, isDir: true}], + songs: [{id: 778}, {id: 614}] + }); }); - it("Given that there was only 1 song in the given directory id in my Madsonic library, when I get the songs from that directory, then a promise will be resolved with an array of 1 song", function() { + it("Given a directory containing 1 song in my Madsonic library and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 1 song and an empty array of subdirectories", function() { response["subsonic-response"].directory = { child: { id: 402 } }; mockBackend.expectJSONP(url).respond(JSON.stringify(response)); var promise = subsonic.getSongs(209); - //TODO: Hyz: Replace with toBeResolvedWith() when getSongs() is refactored - var success = function (data) { - expect(data.album).toEqual([]); - expect(data.song).toEqual([ - { id: 402 } - ]); - }; - promise.then(success); - mockBackend.flush(); - expect(promise).toBeResolved(); + expect(promise).toBeResolvedWith({ + directories: [], + songs: [{id: 402}] + }); + }); + + it("Given a directory that didn't contain anything and given its id, when I get the songs from that directory, then a promise will be rejected with an error message", function() { + response["subsonic-response"].directory = {}; + mockBackend.expectJSONP(url).respond(JSON.stringify(response)); + + var promise = subsonic.getSongs(209); + mockBackend.flush(); + + expect(promise).toBeRejectedWith({reason: 'This directory is empty.'}); + }); + }); + + describe("recursiveGetSongs() -", function() { + var deferred; + beforeEach(function() { + deferred = $q.defer(); + spyOn(subsonic, 'getSongs').and.returnValue(deferred.promise); + }); + it("Given a directory containing 2 songs and a subdirectory itself containing 2 songs and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 4 songs", function() { + // Mock getSongs so we are only testing the recursivity + var firstDeferred = $q.defer(); + var secondDeferred = $q.defer(); + subsonic.getSongs.and.callFake(function (id) { + // First call to getSongs + if (id === 499) { + return firstDeferred.promise; + // Second call to getSongs + } else if (id === 553) { + return secondDeferred.promise; + } + }); + + var promise = subsonic.recursiveGetSongs(499); + // On the first call to getSongs, we expect 2 songs and a subdirectory + firstDeferred.resolve({ + directories: [{ id: 553, type: 'byfolder' }], + songs: [ + { id: 695 }, + { id: 227 } + ] + }); + // On the second call, we expect 2 songs + secondDeferred.resolve({ + directories: [], + songs: [ + { id: 422 }, + { id: 171 } + ] + }); + + expect(promise).toBeResolvedWith([ + { id: 695 }, + { id: 227 }, + { id: 422 }, + { id: 171 }, + ]); + }); + + it("Given a directory containing only 2 songs and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs", function() { + var promise = subsonic.recursiveGetSongs(14); + deferred.resolve({ + directories: [], + songs: [ + { id: 33 }, { id: 595 } + ] + }); + + expect(promise).toBeResolvedWith([ + { id: 33 }, { id: 595 } + ]); + }); + + it("Given a directory that didn't contain anything and given its id, when I get the songs from that directory, then a promise will be resolved with an empty array", function() { + var promise = subsonic.recursiveGetSongs(710); + deferred.reject({reason: 'This directory is empty.'}); + + expect(promise).toBeResolvedWith([]); }); }); describe("getAlbumListBy() -", function() { - var url; beforeEach(function() { url = 'http://demo.subsonic.com/rest/getAlbumList.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&offset=0'+'&p=enc:cGFzc3dvcmQ%3D'+'&size=3&type=newest'+'&u=Hyzual&v=1.10.2'; @@ -331,7 +396,6 @@ describe("Subsonic service -", function() { }); describe("getRandomStarredSongs() -", function() { - var url; beforeEach(function() { 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'; @@ -393,7 +457,6 @@ describe("Subsonic service -", function() { }); describe("getRandomSongs() -", function() { - var url; beforeEach(function() { 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'; @@ -508,7 +571,6 @@ describe("Subsonic service -", function() { }); describe("getArtists() -", function() { - var url; beforeEach(function() { url = 'http://demo.subsonic.com/rest/getIndexes.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; @@ -689,7 +751,6 @@ describe("Subsonic service -", function() { }); describe("getPlaylist() -", function() { - var url; beforeEach(function() { url = 'http://demo.subsonic.com/rest/getPlaylist.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=9123'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; @@ -781,7 +842,6 @@ describe("Subsonic service -", function() { }); describe("getPodcasts() -", function() { - var url; beforeEach(function() { url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&includeEpisodes=false'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; @@ -833,7 +893,6 @@ describe("Subsonic service -", function() { }); describe("getPodcast() -", function() { - var url; beforeEach(function() { url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=2695&includeEpisodes=true'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2'; @@ -933,7 +992,6 @@ describe("Subsonic service -", function() { }); describe("search() -", function() { - var url; beforeEach(function() { url = 'http://demo.subsonic.com/rest/search2.view?'+ 'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D'+'&query=unintersetingly'+'&u=Hyzual&v=1.10.2'; diff --git a/app/subsonic/subsonic.html b/app/subsonic/subsonic.html index e9a65d5..d42dd54 100644 --- a/app/subsonic/subsonic.html +++ b/app/subsonic/subsonic.html @@ -41,7 +41,7 @@ @@ -49,10 +49,10 @@