Merge branch 'rewrite_player' into develop
This commit is contained in:
commit
b51e033c25
40 changed files with 2770 additions and 1350 deletions
|
@ -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
|
||||
}
|
||||
|
|
25
app/app.js
25
app/app.js
|
@ -1,28 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
/* Declare app level module */
|
||||
angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize',
|
||||
'jamstash.subsonic.ctrl', 'jamstash.archive.ctrl'])
|
||||
'jamstash.subsonic.controller', 'jamstash.archive.controller', 'jamstash.player.controller', 'jamstash.queue.controller', 'jamstash.persistence'])
|
||||
|
||||
.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' })
|
||||
.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) {
|
||||
'use strict';
|
||||
|
||||
$httpProvider.interceptors.push(['$rootScope', '$location', '$q', 'globals', function ($rootScope, $location, $q, globals) {
|
||||
return {
|
||||
'request': function (request) {
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
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 = [];
|
||||
|
@ -13,6 +12,7 @@
|
|||
|
||||
$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,22 +165,12 @@
|
|||
// 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() {
|
||||
$(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;
|
||||
|
@ -442,7 +391,6 @@
|
|||
return $sce.trustAsHtml(html);
|
||||
};
|
||||
|
||||
|
||||
/* Launch on Startup */
|
||||
$scope.loadSettings();
|
||||
utils.switchTheme(globals.settings.Theme);
|
||||
|
@ -454,8 +402,8 @@
|
|||
if ($scope.loggedIn()) {
|
||||
//$scope.ping();
|
||||
if (globals.settings.SaveTrackPosition) {
|
||||
player.loadTrackPosition();
|
||||
player.startSaveTrackPosition();
|
||||
persistence.loadQueue();
|
||||
persistence.loadTrackPosition();
|
||||
}
|
||||
}
|
||||
/* End Startup */
|
||||
|
|
81
app/common/main-controller_test.js
Normal file
81
app/common/main-controller_test.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
$rootScope.$apply();
|
||||
}
|
||||
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);
|
||||
});
|
||||
$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();
|
||||
}
|
||||
};
|
||||
}]);
|
108
app/common/notification-service_test.js
Normal file
108
app/common/notification-service_test.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
64
app/common/page-service.js
Normal file
64
app/common/page-service.js
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
||||
}]);
|
54
app/common/page-service_test.js
Normal file
54
app/common/page-service_test.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
58
app/common/persistence-service.js
Normal file
58
app/common/persistence-service.js
Normal file
|
@ -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'); }
|
||||
};
|
||||
}]);
|
||||
|
126
app/common/persistence-service_test.js
Normal file
126
app/common/persistence-service_test.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 += "<strong class=\"codesyntax\">" + solution + "</strong> is";
|
||||
spechtml += " currently being used with<strong>";
|
||||
angular.forEach(data[solution].support, function (format) {
|
||||
if (data[solution].support[format]) {
|
||||
spechtml += " <strong class=\"codesyntax\">" + format + "</strong>";
|
||||
}
|
||||
});
|
||||
spechtml += "</strong> 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
|
@ -1,6 +1,6 @@
|
|||
<li class="row song" ng-repeat="o in song" ng-click="selectSong(o)" ng-dblclick="playFrom($index)" ng-class="{'selected': o.selected, 'playing': o.playing}">
|
||||
<div class="itemactions">
|
||||
<!--<a class="add" href="" title="Add To Queue" ng-click="addSongToQueue(o)" stop-event="click"></a>-->
|
||||
<a class="add" href="" title="Add To Queue" ng-click="addSongToQueue(o)" stop-event="click"></a>
|
||||
<!--<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click"></a>-->
|
||||
<!--<a class="play" href="" title="Start Playing From This Song" ng-click="playFrom($index)" stop-event="click"></a>-->
|
||||
<!--<a class="download" href="" title="Download Song" ng-click="download(o.id)"></a>-->
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<li class="row song" ng-repeat="o in song" ng-click="selectSong(o)" ng-dblclick="playSong(false, o)" ng-class="{'selected': o.selected, 'playing': o.playing}">
|
||||
<li class="row song" ng-repeat="o in song" ng-click="selectSong(o)" ng-dblclick="playSong(o)" ng-class="{'selected': o.selected, 'playing': o.playing}">
|
||||
<div class="itemactions">
|
||||
<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click"></a>
|
||||
<a href="" title="Favorite" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(o)" stop-event="click"></a>
|
||||
|
|
|
@ -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('.');
|
||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
@ -1,27 +1,26 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en" ng-app="JamStash">
|
||||
<html lang="en" ng-app="JamStash" ng-controller="AppController">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=UTF-8">
|
||||
<meta name="description" content="HTML5 Audio Streamer for Subsonic, Archive.org browsing and streaming">
|
||||
<meta name="keywords" content="Subsonic, Archive.org, Live Music Archive, HTML5 Audio, Music Streaming, Live Music">
|
||||
<meta property="og:image" content="http://jamstash.com/images/fbpreview.png"/>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<title>Jamstash</title>
|
||||
<title ng-bind="Page.title()">Jamstash</title>
|
||||
<link href="images/favicon_32x32.ico" rel="shortcut icon" />
|
||||
<link rel="icon" href="images/favicon_48x48.png" sizes="48x48"/>
|
||||
<link rel="icon" href="images/favicon_32x32.png" sizes="32x32"/>
|
||||
<!-- build:css(.) styles/vendor.min.css -->
|
||||
<!-- bower:css -->
|
||||
<link rel="stylesheet" href="bower_components/jplayer/skin/pink.flag/jplayer.pink.flag.css" />
|
||||
<link rel="stylesheet" href="bower_components/jplayer/dist/skin/pink.flag/jplayer.pink.flag.css" />
|
||||
<link rel="stylesheet" href="bower_components/fancybox/source/jquery.fancybox.css" />
|
||||
<!-- endbower -->
|
||||
<!-- endbuild -->
|
||||
<!--<link href="vendor/jquery-split-pane.css" rel="stylesheet" />-->
|
||||
<link href="styles/Style.css" rel="stylesheet" type="text/css" data-name="main" />
|
||||
<link href="styles/Mobile.css" rel="stylesheet" type="text/css" data-name="main" />
|
||||
<link href="" rel="stylesheet" type="text/css" data-name="theme" />
|
||||
</head>
|
||||
<body ng-controller="AppCtrl">
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<div id="messages">
|
||||
|
@ -49,19 +48,7 @@
|
|||
|
||||
<div class="clear"></div>
|
||||
</div><!-- end #content -->
|
||||
<div id="SideBar">
|
||||
<div class="headeractions">
|
||||
<a class="buttonimg" title="Shuffle Queue" ng-click="queueShuffle()"><img src="images/fork_gd_11x12.png"></a>
|
||||
<a class="buttonimg" id="action_Empty" title="Delete Queue" ng-click="queueEmpty()"><img src="images/trash_fill_gd_12x12.png"></a>
|
||||
<a class="buttonimg" id="action_DeleteSelected" title="Remove Selected From Queue" ng-click="queueRemoveSelected()"><img src="images/x_11x11.png"></a>
|
||||
</div>
|
||||
<div class="header">Queue</div>
|
||||
<div id="SideQueue">
|
||||
<ul class="simplelist songlist noselect">
|
||||
<div ng-repeat="song in [queue] track by $index" class="songs" ng-include src="'common/songs_lite.html'" sortable></div>
|
||||
</ul>
|
||||
<div class="colspacer"></div>
|
||||
</div>
|
||||
<div id="SideBar" ng-include src="'queue/queue.html'" ng-controller="QueueController">
|
||||
<!--
|
||||
<div id="NowPlaying">
|
||||
<div class="header"><img src="images/rss_12x12.png" /> Now Playing</div>
|
||||
|
@ -75,53 +62,7 @@
|
|||
-->
|
||||
</div>
|
||||
<!-- Player -->
|
||||
<div id="player">
|
||||
<div id="playercontainer">
|
||||
<div id="playerleft" class="floatleft">
|
||||
<div class="playeractions floatleft">
|
||||
<a class="hover" id="PreviousTrack" title="Previous Track" ng-click="previousTrack()"><img src="images/first_alt_24x24.png" /></a>
|
||||
<a class="hover PlayTrack" title="Play/Pause" ng-click="defaultPlay()"><img src="images/play_alt_24x32.png" /></a>
|
||||
<a class="hover PauseTrack" title="Play/Pause" ng-click="defaultPlay()" style="display: none;"><img src="images/pause_alt_24x32.png" /></a>
|
||||
<a class="hover" id="NextTrack" title="Next Track" ng-click="nextTrack()"><img src="images/last_alt_24x24.png" /></a>
|
||||
</div>
|
||||
<div id="songdetails">
|
||||
<div id="coverart"><a ng-click="fancyboxOpenImage(playingSong.coverartfull)"><img ng-src="{{playingSong.coverartthumb}}" src="images/albumdefault_60.jpg" alt="" /></a></div>
|
||||
<ul>
|
||||
<li class="song" id="{{playingSong.id}}" ng-bind-html="playingSong.name" title="{{playingSong.specs}}"></li>
|
||||
<li class="album" ng-bind-html="playingSong.album"></li>
|
||||
</ul>
|
||||
<div id="songdetails_controls">
|
||||
<a href="" class="jukebox" title="Jukebox Mode [Beta]" ng-click="toggleSetting('Jukebox')" ng-class="{'hoverSelected': !settings.Jukebox }"></a>
|
||||
<a href="" class="loop" title="Repeat" ng-click="toggleSetting('Repeat')" ng-class="{'hoverSelected': !settings.Repeat }"></a>
|
||||
<a href="" id="action_SaveProgress" class="lock" title="Save Track Position: On" ng-show="settings.SaveTrackPosition"></a>
|
||||
<a title="Favorite" href="" ng-class="{'favorite': playingSong.starred, 'rate': !playingSong.starred}" ng-click="updateFavorite(playingSong)" stop-event="click"></a>
|
||||
<a href="" id="action_Mute" class="mute" title="Mute"></a>
|
||||
<a href="" id="action_UnMute" class="unmute" title="Unmute" style="display: none;"></a>
|
||||
<!--<div class="jp-volume-bar"><div class="jp-volume-bar-value"></div></div><a href="" id="action_VolumeMax" class="volume" title="Max Volume"></a>-->
|
||||
</div>
|
||||
</div>
|
||||
<div id="playdeck_1"></div>
|
||||
<div id="playdeck_2"></div>
|
||||
<div id="submenu_CurrentPlaylist" class="submenu shadow" style="display: none;">
|
||||
<table id="CurrentPlaylistPreviewContainer" class="simplelist songlist">
|
||||
<thead></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="playermiddle">
|
||||
<div id="audiocontainer">
|
||||
<div class="audiojs" id="audio_wrapper0">
|
||||
<div class="scrubber"><div class="progress"></div><div class="loaded"></div></div>
|
||||
<div class="time"><em id="played">00:00</em>/<strong id="duration">00:00</strong></div>
|
||||
<div class="error-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview"></div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-include src="'player/player.html'" ng-controller="PlayerController"></div>
|
||||
</div> <!-- End container -->
|
||||
<script>
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
|
@ -149,6 +90,7 @@
|
|||
<script src="bower_components/jquery.scrollTo/jquery.scrollTo.js"></script>
|
||||
<script src="bower_components/underscore/underscore.js"></script>
|
||||
<script src="bower_components/angular-underscore/angular-underscore.js"></script>
|
||||
<script src="bower_components/angular-locker/dist/angular-locker.min.js"></script>
|
||||
<!-- endbower -->
|
||||
<script src="vendor/jquery.base64.js"></script>
|
||||
<script src="vendor/jquery.dateFormat-1.0.js"></script>
|
||||
|
@ -156,17 +98,21 @@
|
|||
<!-- our scripts -->
|
||||
<!-- build:js(app) scripts/scripts.min.js -->
|
||||
<script src="app.js"></script>
|
||||
<script src="settings/settings.js"></script>
|
||||
<script src="settings/settings-service.js"></script>
|
||||
<script src="common/model-service.js"></script>
|
||||
<script src="common/utils-service.js"></script>
|
||||
<script src="common/page-service.js"></script>
|
||||
<script src="common/notification-service.js"></script>
|
||||
<script src="subsonic/subsonic-service.js"></script>
|
||||
<script src="archive/archive-service.js"></script>
|
||||
<script src="common/player-service.js"></script>
|
||||
<script src="common/persistence-service.js"></script>
|
||||
<script src="common/main-controller.js"></script>
|
||||
<script src="settings/settings.js"></script>
|
||||
<script src="subsonic/subsonic.js"></script>
|
||||
<script src="subsonic/subsonic-service.js"></script>
|
||||
<script src="archive/archive.js"></script>
|
||||
<script src="archive/archive-service.js"></script>
|
||||
<script src="player/player.js"></script>
|
||||
<script src="player/player-directive.js"></script>
|
||||
<script src="player/player-service.js"></script>
|
||||
<script src="queue/queue.js"></script>
|
||||
<script src="common/filters.js"></script>
|
||||
<script src="common/directives.js"></script>
|
||||
|
|
205
app/player/player-directive.js
Normal file
205
app/player/player-directive.js
Normal file
|
@ -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: '<div></div>',
|
||||
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
|
||||
};
|
||||
}]);
|
301
app/player/player-directive_test.js
Normal file
301
app/player/player-directive_test.js
Normal file
|
@ -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 = '<div id="playdeck_1" jplayer></div>';
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
137
app/player/player-service.js
Normal file
137
app/player/player-service.js
Normal file
|
@ -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;
|
||||
}]);
|
280
app/player/player-service_test.js
Normal file
280
app/player/player-service_test.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
59
app/player/player.html
Normal file
59
app/player/player.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
<div id="player">
|
||||
<div id="playercontainer">
|
||||
<div id="playerleft" class="floatleft">
|
||||
<div class="playeractions floatleft">
|
||||
<a class="hover" id="PreviousTrack" title="Previous Track" ng-click="previousTrack()">
|
||||
<img src="images/first_alt_24x24.png" height="24" width="24" alt="Previous track" />
|
||||
</a>
|
||||
<a class="hover PlayTrack" title="Play/Pause" ng-click="play()">
|
||||
<img src="images/play_alt_24x24.png" height="24" width="24" alt="Play" />
|
||||
</a>
|
||||
<a class="hover PauseTrack" title="Play/Pause" style="display: none;" ng-click="pause()">
|
||||
<img src="images/pause_alt_24x24.png" height="24" width="24" alt="Pause" />
|
||||
</a>
|
||||
<a class="hover" id="NextTrack" title="Next Track" ng-click="nextTrack()">
|
||||
<img src="images/last_alt_24x24.png" height="24" width="24" alt="Next track" />
|
||||
</a>
|
||||
</div>
|
||||
<div id="songdetails">
|
||||
<div id="coverart">
|
||||
<a ng-click="fancyboxOpenImage(getPlayingSong().coverartfull)">
|
||||
<img ng-src="{{getPlayingSong().coverartthumb}}" src="images/albumdefault_60.jpg" height="30" width="30" />
|
||||
</a>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="song" id="{{getPlayingSong().id}}" ng-bind-html="getPlayingSong().name" title="{{getPlayingSong().specs}}"></li>
|
||||
<li class="album" ng-bind-html="getPlayingSong().album"></li>
|
||||
</ul>
|
||||
<div id="songdetails_controls">
|
||||
<a href="" class="jukebox" title="Jukebox Mode [Beta]" ng-click="toggleSetting('Jukebox')" ng-class="{'hoverSelected': !settings.Jukebox }"></a>
|
||||
<a href="" class="loop" title="Repeat" ng-click="toggleSetting('Repeat')" ng-class="{'hoverSelected': !settings.Repeat }"></a>
|
||||
<a href="" id="action_SaveProgress" class="lock" title="Save Track Position: On" ng-show="settings.SaveTrackPosition"></a>
|
||||
<a title="Favorite" href="" ng-class="{'favorite': getPlayingSong().starred, 'rate': !getPlayingSong().starred}" ng-click="updateFavorite(getPlayingSong())" stop-event="click"></a>
|
||||
<a href="" id="action_Mute" class="mute" title="Mute"></a>
|
||||
<a href="" id="action_UnMute" class="unmute" title="Unmute" style="display: none;"></a>
|
||||
<!--<div class="jp-volume-bar"><div class="jp-volume-bar-value"></div></div><a href="" id="action_VolumeMax" class="volume" title="Max Volume"></a>-->
|
||||
</div>
|
||||
</div>
|
||||
<div id="playdeck_1" jplayer></div>
|
||||
<div id="playdeck_2"></div>
|
||||
<div id="submenu_CurrentPlaylist" class="submenu shadow" style="display: none;">
|
||||
<table id="CurrentPlaylistPreviewContainer" class="simplelist songlist">
|
||||
<thead></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="playermiddle">
|
||||
<div id="audiocontainer">
|
||||
<div class="audiojs" id="audio_wrapper0">
|
||||
<div class="scrubber"><div class="progress"></div><div class="loaded"></div></div>
|
||||
<div class="time"><em id="played">00:00</em>/<strong id="duration">00:00</strong></div>
|
||||
<div class="error-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview"></div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
39
app/player/player.js
Normal file
39
app/player/player.js
Normal file
|
@ -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 ?
|
||||
}]);
|
40
app/player/player_test.js
Normal file
40
app/player/player_test.js
Normal file
|
@ -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
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
|
||||
angular.module('JamStash')
|
||||
|
||||
.controller('PodcastCtrl', ['$scope', '$rootScope', function PodcastCtrl($scope, $rootScope) {
|
||||
.controller('PodcastController', ['$scope', '$rootScope', function ($scope, $rootScope) {
|
||||
'use strict';
|
||||
$rootScope.song = [];
|
||||
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
<div id="queue" class="tabcontent">
|
||||
<div class="section fullsection floatleft">
|
||||
<ul class="songlist simplelist noselect" ng-if="song.length > 0" ng-include src="'common/songs.html'" sortable></ul>
|
||||
</div>
|
||||
<div class="headeractions">
|
||||
<a class="buttonimg" title="Shuffle Queue" ng-click="shuffleQueue()"><img src="images/fork_gd_11x12.png"></a>
|
||||
<a class="buttonimg" title="Delete Queue" ng-click="emptyQueue()"><img src="images/trash_fill_gd_12x12.png"></a>
|
||||
<a class="buttonimg" title="Remove Selected From Queue" ng-click="removeSelectedSongsFromQueue()"><img src="images/x_11x11.png"></a>
|
||||
</div>
|
||||
<div class="header">Queue</div>
|
||||
<div id="SideQueue">
|
||||
<ul class="simplelist songlist noselect">
|
||||
<div ng-repeat="song in [player.queue] track by $index" class="songs" sortable>
|
||||
<li class="row song id{{o.id}}" ng-repeat="o in song" ng-click="selectSong(o)" ng-dblclick="playSong(o)" ng-class="{'selected': o.selected, 'playing': isPlayingSong(o)}">
|
||||
<div class="itemactions">
|
||||
<a class="remove" href="" title="Remove Song" ng-click="removeSongFromQueue(o)" stop-event="click"></a>
|
||||
<a href="" title="Favorite" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(o)" stop-event="click"></a>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
<div class="title floatleft" title="{{o.description}}" ng-bind-html="o.name"></div>
|
||||
<div class="time floatleft">{{o.time}}</div>
|
||||
<div class="clear"></div>
|
||||
</li>
|
||||
|
||||
</div>
|
||||
</ul>
|
||||
<div class="colspacer"></div>
|
||||
</div>
|
|
@ -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 ?
|
||||
}]);
|
||||
|
|
134
app/queue/queue_test.js
Normal file
134
app/queue/queue_test.js
Normal file
|
@ -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});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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: [] };
|
||||
|
@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
if (!isNaN(folder)) {
|
||||
params.musicFolderId = folder;
|
||||
}
|
||||
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 {
|
||||
content.selectedAutoPlaylist = 'random';
|
||||
}
|
||||
var genreParams = '';
|
||||
if (genre !== '' && genre != 'Random') {
|
||||
genreParams = '&genre=' + genre;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
var deferred = this.getStarred()
|
||||
.then(function (starred) {
|
||||
if(starred.song !== undefined && starred.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);
|
||||
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 deferred.promise;
|
||||
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.", "");
|
||||
|
@ -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,6 +790,19 @@ 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
|
||||
};
|
||||
|
|
|
@ -1,28 +1,34 @@
|
|||
describe("Subsonic service -", function() {
|
||||
'use strict';
|
||||
|
||||
var subsonic, mockBackend, mockGlobals, response;
|
||||
|
||||
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';
|
||||
|
||||
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,
|
||||
Protocol: 'jsonp'
|
||||
Username: "Hyzual",
|
||||
Password: "enc:cGFzc3dvcmQ=",
|
||||
Protocol: "jsonp",
|
||||
ApiVersion: "1.10.2",
|
||||
ApplicationName: "Jamstash",
|
||||
Timeout: 20000
|
||||
},
|
||||
BaseURL: function () {
|
||||
return 'http://demo.subsonic.com/rest';
|
||||
},
|
||||
BaseParams: function () {
|
||||
return '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;
|
||||
});
|
||||
});
|
||||
|
||||
inject(function (_subsonic_, $httpBackend) {
|
||||
|
@ -37,89 +43,144 @@ describe("Subsonic service -", function() {
|
|||
mockBackend.verifyNoOutstandingRequest();
|
||||
});
|
||||
|
||||
describe("getStarred -", function() {
|
||||
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';
|
||||
});
|
||||
|
||||
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));
|
||||
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');
|
||||
|
||||
var promise = subsonic.getStarred();
|
||||
var promise = subsonic.subsonicRequest(partialUrl);
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeResolvedWith({artist: [
|
||||
{id: 2245}
|
||||
], album: [
|
||||
{id: 1799},{id: 20987}
|
||||
], song: [
|
||||
{id: 2478},{id: 14726},{id: 742}
|
||||
]
|
||||
});
|
||||
expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', httpError: 503});
|
||||
});
|
||||
|
||||
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() {
|
||||
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));
|
||||
|
||||
var promise = subsonic.subsonicRequest(partialUrl);
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', subsonicError: {code: 10, message:'Required parameter is missing.'}});
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
subsonic.subsonicRequest(partialUrl);
|
||||
mockBackend.flush();
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
subsonic.subsonicRequest(partialUrl, {
|
||||
params: {
|
||||
id: 75,
|
||||
submission: false
|
||||
}
|
||||
});
|
||||
mockBackend.flush();
|
||||
});
|
||||
|
||||
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';
|
||||
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"}
|
||||
]};
|
||||
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));
|
||||
|
||||
var promise = subsonic.subsonicRequest(partialUrl);
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeResolvedWith({status: "ok", version: "1.10.2"});
|
||||
});
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
var promise = subsonic.ping();
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeResolvedWith({status: "ok", version: "1.10.2"});
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
mockBackend.expectJSONP(url).respond(200, JSON.stringify(response));
|
||||
|
||||
var promise = subsonic.scrobble(song);
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeResolvedWith(true);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
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));
|
||||
|
||||
var promise = subsonic.getStarred();
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeResolvedWith({song: [
|
||||
{id: "2147"},{id:"9847"},{id:"214"}]
|
||||
expect(promise).toBeResolvedWith({
|
||||
artist: [{id: 2245}],
|
||||
album: [{id: 1799},{id: 20987}],
|
||||
song: [{id: 2478},{id: 14726},{id: 742}]
|
||||
});
|
||||
});
|
||||
|
||||
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));
|
||||
mockBackend.expectJSONP(url).respond(200, JSON.stringify(response));
|
||||
|
||||
var promise = subsonic.getStarred();
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeRejectedWith({reason: 'Nothing is starred on the Subsonic server.'});
|
||||
});
|
||||
|
||||
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.getStarred();
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', httpError: 503});
|
||||
});
|
||||
|
||||
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.getStarred();
|
||||
mockBackend.flush();
|
||||
|
||||
expect(promise).toBeRejectedWith({reason: 'Error when contacting the Subsonic server.', subsonicError: {code: 10, message:'Required parameter is missing.'}});
|
||||
});
|
||||
}); //end getStarred
|
||||
|
||||
describe("getRandomStarredSongs -", function() {
|
||||
describe("Given that the global setting AutoPlaylist Size is 3", 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';
|
||||
|
||||
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() {
|
||||
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"}
|
||||
{id: 11841},{id: 12061},{id: 17322},{id: 1547},{id: 14785}
|
||||
];
|
||||
response["subsonic-response"].starred = {song: library};
|
||||
mockBackend.whenJSONP(url).respond(200, JSON.stringify(response));
|
||||
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
|
||||
|
@ -137,18 +198,18 @@ describe("Subsonic service -", function() {
|
|||
});
|
||||
|
||||
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));
|
||||
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"}]);
|
||||
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.whenJSONP(url).respond(200, JSON.stringify(response));
|
||||
mockBackend.expectJSONP(url).respond(200, JSON.stringify(response));
|
||||
|
||||
var promise = subsonic.getRandomStarredSongs();
|
||||
mockBackend.flush();
|
||||
|
@ -157,4 +218,83 @@ describe("Subsonic service -", function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
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}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -122,13 +122,15 @@
|
|||
<li class="index" id="auto">Auto Playlists</li>
|
||||
<li class="item" ng-click="getRandomStarredSongs('display')" ng-class="{'selected': selectedAutoPlaylist == 'starred'}">
|
||||
<div class="itemactions">
|
||||
<a class="add" href="" ng-click="getRandomStarredSongs('add')" title="Add To Play Queue" stop-event="click"></a><a class="play" href="" ng-click="getRandomStarredSongs('play')" title="Play" stop-event="click"></a>
|
||||
<a class="add" href="" ng-click="getRandomStarredSongs('add')" title="Add To Play Queue" stop-event="click"></a>
|
||||
<a class="play" href="" ng-click="getRandomStarredSongs('play')" title="Play" stop-event="click"></a>
|
||||
</div>
|
||||
<div class="title">Starred</div>
|
||||
</li>
|
||||
<li class="item" ng-click="getRandomSongs('', '', '')" ng-class="{'selected': selectedAutoPlaylist == 'random'}">
|
||||
<li class="item" ng-click="getRandomSongs('display')" ng-class="{'selected': selectedAutoPlaylist == 'random'}">
|
||||
<div class="itemactions">
|
||||
<a class="add" href="" title="Add To Play Queue" ng-click="getRandomSongs('add', '', '')" stop-event="click"></a><a class="play" href="" title="Play" ng-click="getRandomSongs('play', '', '')" stop-event="click"></a>
|
||||
<a class="add" href="" title="Add To Play Queue" ng-click="getRandomSongs('add')" stop-event="click"></a>
|
||||
<a class="play" href="" title="Play" ng-click="getRandomSongs('play')" stop-event="click"></a>
|
||||
</div>
|
||||
<div class="title">Random</div>
|
||||
</li>
|
||||
|
@ -136,18 +138,20 @@
|
|||
<select id="Genres" name="Genres" class="large" ng-model="selectedGenre" ng-options="o for o in Genres">
|
||||
<option value="">[Select Genre]</option>
|
||||
</select>
|
||||
<li class="item" ng-repeat="o in playlistsGenre" ng-click="getRandomSongs('', o, '')" ng-class="{'selected': selectedAutoPlaylist == o}">
|
||||
<li class="item" ng-repeat="o in playlistsGenre" ng-click="getRandomSongs('display', o)" ng-class="{'selected': selectedAutoPlaylist == o}">
|
||||
<div class="itemactions">
|
||||
<a class="add" href="" title="Add To Play Queue" ng-click="getRandomSongs('add', o, '')" stop-event="click"></a><a class="play" href="" title="Play" ng-click="getRandomSongs('play', o, '')" stop-event="click"></a>
|
||||
<a class="add" href="" title="Add To Play Queue" ng-click="getRandomSongs('add', o)" stop-event="click"></a>
|
||||
<a class="play" href="" title="Play" ng-click="getRandomSongs('play', o)" stop-event="click"></a>
|
||||
</div>
|
||||
<div class="title">{{o}}</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="simplelist mainlist noselect">
|
||||
<li class="index" id="folder">Folder Playlists</li>
|
||||
<li class="item" ng-repeat="o in MusicFolders | musicfolder" ng-click=" getRandomSongs('', '' , o.id)" ng-class="{'selected': o.id == selectedAutoPlaylist}">
|
||||
<li class="item" ng-repeat="o in MusicFolders | musicfolder" ng-click=" getRandomSongs('display', '' , o.id)" ng-class="{'selected': o.id == selectedAutoPlaylist}">
|
||||
<div class="itemactions">
|
||||
<a class="add" href="" title="Add To Play Queue" ng-click="getRandomSongs('add', '', o.id)" stop-event="click"></a><a class="play" href="" title="Play" ng-click="getRandomSongs('play', '', o.id)" stop-event="click"></a>
|
||||
<a class="add" href="" title="Add To Play Queue" ng-click="getRandomSongs('add', '', o.id)" stop-event="click"></a>
|
||||
<a class="play" href="" title="Play" ng-click="getRandomSongs('play', '', o.id)" stop-event="click"></a>
|
||||
</div>
|
||||
<div class="title">{{o.name}}</div>
|
||||
</li>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -2,30 +2,43 @@ describe("Subsonic controller", function() {
|
|||
'use strict';
|
||||
|
||||
|
||||
var scope, $rootScope, subsonic, notifications, deferred;
|
||||
var scope, $rootScope, subsonic, notifications, player,
|
||||
deferred;
|
||||
|
||||
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) {
|
||||
$delegate.queue = [];
|
||||
$delegate.play = jasmine.createSpy("play");
|
||||
$delegate.playFirstSong = jasmine.createSpy("playFirstSong");
|
||||
return $delegate;
|
||||
});
|
||||
|
||||
$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;
|
||||
});
|
||||
|
||||
$provide.decorator('notifications', function ($delegate) {
|
||||
$delegate.updateMessage = jasmine.createSpy("updateMessage");
|
||||
return $delegate;
|
||||
});
|
||||
});
|
||||
|
||||
inject(function ($controller, _$rootScope_, utils, globals, map, _subsonic_, _notifications_, $q, _player_) {
|
||||
$rootScope = _$rootScope_;
|
||||
scope = $rootScope.$new();
|
||||
subsonic = _subsonic_;
|
||||
notifications = _notifications_;
|
||||
player = _player_;
|
||||
|
||||
// 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 = [];
|
||||
|
||||
$controller('SubsonicCtrl', {
|
||||
$controller('SubsonicController', {
|
||||
$scope: scope,
|
||||
$rootScope: $rootScope,
|
||||
$routeParams: {},
|
||||
|
@ -38,94 +51,148 @@ describe("Subsonic controller", function() {
|
|||
});
|
||||
});
|
||||
|
||||
//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() {
|
||||
|
||||
describe("given that my library contains 3 starred songs, ", function() {
|
||||
var response = [
|
||||
describe("given that my library contains 3 songs, ", function() {
|
||||
var response;
|
||||
beforeEach(function() {
|
||||
response = [
|
||||
{id:"2548"}, {id:"8986"}, {id:"2986"}
|
||||
];
|
||||
});
|
||||
|
||||
it("when displaying random starred songs, it sets the scope with the selected songs", function() {
|
||||
scope.getRandomStarredSongs('display');
|
||||
describe("get songs -", function() {
|
||||
beforeEach(function() {
|
||||
spyOn(scope, "requestSongs");
|
||||
});
|
||||
|
||||
it("it can get random starred songs from the subsonic service", function() {
|
||||
scope.getRandomStarredSongs('whatever action');
|
||||
deferred.resolve(response);
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||
expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'whatever action');
|
||||
expect(scope.selectedPlaylist).toBeNull();
|
||||
expect(scope.selectedAutoPlaylist).toBe('starred');
|
||||
});
|
||||
|
||||
it("it can get random songs from all folders or genres from the subsonic service", function() {
|
||||
scope.getRandomSongs('whatever action');
|
||||
deferred.resolve(response);
|
||||
|
||||
expect(subsonic.getRandomSongs).toHaveBeenCalled();
|
||||
expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'whatever action');
|
||||
expect(scope.selectedPlaylist).toBeNull();
|
||||
expect(scope.selectedAutoPlaylist).toBe('random');
|
||||
});
|
||||
|
||||
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("it can get random songs from a given folder id from the subsonic service", function() {
|
||||
scope.getRandomSongs('whatever action', '', 1);
|
||||
deferred.resolve(response);
|
||||
|
||||
expect(subsonic.getRandomSongs).toHaveBeenCalledWith('', 1);
|
||||
expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'whatever action');
|
||||
expect(scope.selectedPlaylist).toBeNull();
|
||||
expect(scope.selectedAutoPlaylist).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(scope.song).toEqual([
|
||||
{id: "2548"}, {id: "8986"}, {id: "2986"}
|
||||
]);
|
||||
});
|
||||
|
||||
it("when adding random starred songs, it adds the selected songs to the queue and notifies the user", function() {
|
||||
scope.getRandomStarredSongs('add');
|
||||
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);
|
||||
$rootScope.$apply();
|
||||
scope.$apply();
|
||||
|
||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||
expect($rootScope.queue).toEqual([
|
||||
expect(player.queue).toEqual([
|
||||
{id: "2548"}, {id: "8986"}, {id: "2986"}
|
||||
]);
|
||||
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Song(s) Added to Queue', true);
|
||||
});
|
||||
|
||||
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("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"}];
|
||||
|
||||
scope.getRandomStarredSongs('play');
|
||||
scope.requestSongs(deferred.promise, 'play');
|
||||
deferred.resolve(response);
|
||||
$rootScope.$apply();
|
||||
scope.$apply();
|
||||
|
||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||
expect($rootScope.playSong).toHaveBeenCalledWith(false, {id: "2548"});
|
||||
expect($rootScope.queue).toEqual([
|
||||
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);
|
||||
});
|
||||
|
||||
it("when I request songs, it returns a promise so that I can chain it further", function() {
|
||||
var success = jasmine.createSpy("success");
|
||||
|
||||
scope.requestSongs(deferred.promise, 'whatever action').then(success);
|
||||
deferred.resolve(response);
|
||||
scope.$apply();
|
||||
|
||||
expect(success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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("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"}];
|
||||
|
||||
scope.getRandomStarredSongs('whatever action');
|
||||
deferred.reject({reason: 'No starred songs found on the Subsonic server.'});
|
||||
$rootScope.$apply();
|
||||
scope.requestSongs(deferred.promise, 'whatever action');
|
||||
deferred.reject({reason: 'No songs found on the Subsonic server.'});
|
||||
scope.$apply();
|
||||
|
||||
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);
|
||||
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 getting random starred songs, it notifies the user with the error message", function() {
|
||||
scope.getRandomStarredSongs('whatever action');
|
||||
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.'}
|
||||
});
|
||||
$rootScope.$apply();
|
||||
scope.$apply();
|
||||
|
||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||
expect(notifications.updateMessage).toHaveBeenCalledWith('Error when contacting the Subsonic server. Required parameter is missing.', true);
|
||||
});
|
||||
|
||||
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');
|
||||
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
|
||||
});
|
||||
$rootScope.$apply();
|
||||
scope.$apply();
|
||||
|
||||
expect(subsonic.getRandomStarredSongs).toHaveBeenCalled();
|
||||
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"}];
|
||||
scope.song = [
|
||||
{id: 1084},
|
||||
{id: 6810},
|
||||
{id: 214}
|
||||
];
|
||||
mockUI = {
|
||||
item: {}
|
||||
};
|
||||
|
@ -151,10 +218,20 @@ describe("Subsonic controller", function() {
|
|||
expect(mockUI.item.data).toHaveBeenCalledWith('start');
|
||||
// The second song should now be first
|
||||
expect(scope.song).toEqual([
|
||||
{id: "6810"}, {id: "1084"}, {id: "214"}
|
||||
{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
|
||||
});
|
14
bower.json
14
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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue