From 5af003e5b918b5fbfa86266b455b593a6f33fa68 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 03:41:53 -0600 Subject: [PATCH 01/28] vrfs-988: fixed broken musician filter test --- web/spec/requests/musician_search_api_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/spec/requests/musician_search_api_spec.rb b/web/spec/requests/musician_search_api_spec.rb index 99c87588b..284a7f596 100644 --- a/web/spec/requests/musician_search_api_spec.rb +++ b/web/spec/requests/musician_search_api_spec.rb @@ -53,13 +53,13 @@ describe "Musician Search API", :type => :api do it "gets musicians for default instrument" do get_query({:instrument => 'electric guitar'}) good_response - expect(json.count).to be >= 1 + expect(json['musicians'].count).to be >= 1 end it "gets no musicians for unused instruments" do get_query({:instrument => 'tuba'}) good_response - expect(json.count).to eq(0) + expect(json['musicians'].count).to eq(0) end end From ef01c9a8f1863a8d8d439508a80f3cd56570f14f Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 07:38:47 -0600 Subject: [PATCH 02/28] vrfs1008: moved invite musician html into template for reuse in sessions page --- web/app/assets/javascripts/createSession.js | 1 + web/app/views/clients/_createSession.html.erb | 11 +---------- web/app/views/clients/_session.html.erb | 14 +++++++++++++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 6a6f1a9dc..1345b3b7d 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -405,6 +405,7 @@ function initialize(invitationDialogInstance, friendSelectorDialogInstance) { friendSelectorDialog = friendSelectorDialogInstance; invitationDialog = invitationDialogInstance; + $('#create-session-invite-musicians').append(context.JK.fillTemplate($('#template-invite-session-musicians').html(), {})); events(); loadBands(); loadSessionSettings(); diff --git a/web/app/views/clients/_createSession.html.erb b/web/app/views/clients/_createSession.html.erb index abacce414..faa1ad847 100644 --- a/web/app/views/clients/_createSession.html.erb +++ b/web/app/views/clients/_createSession.html.erb @@ -84,16 +84,7 @@

invite musicians


-
- -
- Start typing friends' names or: -
-
-
-
+
diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb index d044ff38c..86b69b577 100644 --- a/web/app/views/clients/_session.html.erb +++ b/web/app/views/clients/_session.html.erb @@ -70,7 +70,7 @@

live tracks

@@ -178,3 +178,15 @@ + From 8fdbc7cc08a41ad1f86e3e9e97f81c0b42223deb Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 10:11:26 -0600 Subject: [PATCH 03/28] vrfs1008: refactoring invite logic --- web/app/assets/javascripts/createSession.js | 142 +------------- web/app/assets/javascripts/inviteMusicians.js | 173 ++++++++++++++++++ web/app/assets/javascripts/session.js | 2 + .../clients/_inviteSessionMusicians.html.erb | 26 +++ web/app/views/clients/_session.html.erb | 13 +- web/app/views/clients/index.html.erb | 5 +- 6 files changed, 212 insertions(+), 149 deletions(-) create mode 100644 web/app/assets/javascripts/inviteMusicians.js create mode 100644 web/app/views/clients/_inviteSessionMusicians.html.erb diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 1345b3b7d..29e52754a 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -7,81 +7,19 @@ var logger = context.JK.logger; var rest = context.JK.Rest(); var realtimeMessaging = context.JK.JamServer; - var friendSelectorDialog = null; var invitationDialog = null; - var autoComplete = null; - var userNames = []; - var userIds = []; - var userPhotoUrls = []; + var inviteMusiciansDialog = null; var MAX_GENRES = 1; - var selectedFriendIds = {}; var sessionSettings = {}; function beforeShow(data) { - userNames = []; - userIds = []; - userPhotoUrls = []; + inviteMusiciansDialog.afterShow(data); context.JK.GenreSelectorHelper.render('#create-session-genre'); resetForm(); } function afterShow(data) { - friendSelectorDialog.setCallback(friendSelectorCallback); - - var friends = rest.getFriends({ id: context.JK.currentUserId }) - .done(function(friends) { - $.each(friends, function() { - userNames.push(this.name); - userIds.push(this.id); - userPhotoUrls.push(this.photo_url); - }); - - var autoCompleteOptions = { - lookup: { suggestions: userNames, data: userIds }, - onSelect: addInvitation - }; - - $('#friend-input').attr("placeholder", "Type a friend\'s name").prop('disabled', false); - - if (!autoComplete) { - autoComplete = $('#friend-input').autocomplete(autoCompleteOptions); - } - else { - autoComplete.setOptions(autoCompleteOptions); - } - - $(".autocomplete").width("150px"); - }) - .fail(function() { - $('#friend-input').attr("placeholder", "Unable to lookup friends"); - app.ajaxError(arguments); - }); - } - - function friendSelectorCallback(newSelections) { - var keys = Object.keys(newSelections); - for (var i=0; i < keys.length; i++) { - addInvitation(newSelections[keys[i]].userName, newSelections[keys[i]].userId); - } - } - - function addInvitation(value, data) { - if ($('#selected-friends div[user-id=' + data + ']').length === 0) { - var template = $('#template-added-invitation').html(); - var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value}); - $('#selected-friends').append(invitationHtml); - $('#friend-input').select(); - selectedFriendIds[data] = true; - } - else { - $('#friend-input').select(); - context.alert('Invitation already exists for this musician.'); - } - } - - function removeInvitation(evt) { - delete selectedFriendIds[$(evt.currentTarget).parent().attr('user-id')]; - $(evt.currentTarget).closest('.invitation').remove(); + inviteMusiciansDialog.afterShow(data); } function resetForm() { @@ -248,49 +186,12 @@ return false; } - function createInvitations(sessionId, onComplete) { - var callCount = 0; - var totalInvitations = 0; - $('#selected-friends .invitation').each(function(index, invitation) { - callCount++; - totalInvitations++; - var invite_id = $(invitation).attr('user-id'); - var invite = { - music_session: sessionId, - receiver: invite_id - }; - $.ajax({ - type: "POST", - url: "/api/invitations", - data: invite - }).done(function(response) { - callCount--; - }).fail(app.ajaxError); - }); - // TODO - this is the second time I've used this pattern. - // refactor to make a common utility for this. - function checker() { - if (callCount === 0) { - onComplete(); - } else { - context.setTimeout(checker, 10); - } - } - checker(); - return totalInvitations; - } - function events() { $('#create-session-form').on('submit', submitForm); $('#btn-create-session').on("click", submitForm); - $('#selected-friends').on("click", ".invitation a", removeInvitation); $('#musician-access').change(toggleMusicianAccess); $('#fan-access').change(toggleFanAccess); - $('#btn-choose-friends').click(function() { - friendSelectorDialog.showDialog(selectedFriendIds); - }); - $('div[layout-id="createSession"] .btn-email-invitation').click(function() { invitationDialog.showEmailDialog(); }); @@ -373,39 +274,10 @@ }); } - function searchFriends(query) { - if (query.length < 2) { - $('#friend-search-results').empty(); - return; - } - var url = "/api/search?query=" + query + "&userId=" + context.JK.currentUserId; - $.ajax({ - type: "GET", - url: url, - success: friendSearchComplete - }); - } - - function friendSearchComplete(response) { - // reset search results each time - $('#friend-search-results').empty(); - - // loop through each - $.each(response.friends, function() { - // only show friends who are musicians - if (this.musician === true) { - var template = $('#template-friend-search-results').html(); - var searchResultHtml = context.JK.fillTemplate(template, {userId: this.id, name: this.first_name + ' ' + this.last_name}); - $('#friend-search-results').append(searchResultHtml); - $('#friend-search-results').attr('style', 'display:block'); - } - }); - } - - function initialize(invitationDialogInstance, friendSelectorDialogInstance) { - friendSelectorDialog = friendSelectorDialogInstance; + function initialize(invitationDialogInstance, inviteMusiciansDialogInstance) { invitationDialog = invitationDialogInstance; - $('#create-session-invite-musicians').append(context.JK.fillTemplate($('#template-invite-session-musicians').html(), {})); + inviteMusiciansDialog = inviteMusiciansDialogInstance; + $('#create-session-invite-musicians').append(inviteMusiciansDialog.friendSelectorHTML); events(); loadBands(); loadSessionSettings(); @@ -420,8 +292,6 @@ this.submitForm = submitForm; this.validateForm = validateForm; this.loadBands = loadBands; - this.searchFriends = searchFriends; - this.addInvitation = addInvitation; return this; }; diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js new file mode 100644 index 000000000..bb3d45b2e --- /dev/null +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -0,0 +1,173 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.InviteMusiciansDialog = function(app) { + var logger = context.JK.logger; + var userNames = []; + var userIds = []; + var userPhotoUrls = []; + var friendSelectorDialog = null; + var friendSelectorHTML = null; + var selectedFriendIds = {}; + var autoComplete = null; + var rest = context.JK.Rest(); + var selectedIds = {}; + var newSelections = {}; + var mySaveCallback; + + this. beforeShow = function(data) { + userNames = []; + userIds = []; + userPhotoUrls = []; + } + + this.afterShow = function(data) { + friendSelectorDialog.setCallback(friendSelectorCallback); + + var friends = rest.getFriends({ id: context.JK.currentUserId }) + .done(function(friends) { + $.each(friends, function() { + userNames.push(this.name); + userIds.push(this.id); + userPhotoUrls.push(this.photo_url); + }); + + var autoCompleteOptions = { + lookup: { suggestions: userNames, data: userIds }, + onSelect: addInvitation + }; + + $('#friend-input').attr("placeholder", "Type a friend\'s name").prop('disabled', false); + + if (!autoComplete) { + autoComplete = $('#friend-input').autocomplete(autoCompleteOptions); + } + else { + autoComplete.setOptions(autoCompleteOptions); + } + + $(".autocomplete").width("150px"); + }) + .fail(function() { + $('#friend-input').attr("placeholder", "Unable to lookup friends"); + app.ajaxError(arguments); + }); + } + + function friendSelectorCallback(newSelections) { + var keys = Object.keys(newSelections); + for (var i=0; i < keys.length; i++) { + addInvitation(newSelections[keys[i]].userName, newSelections[keys[i]].userId); + } + } + + function addInvitation(value, data) { + if ($('#selected-friends div[user-id=' + data + ']').length === 0) { + var template = $('#template-added-invitation').html(); + var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value}); + $('#selected-friends').append(invitationHtml); + $('#friend-input').select(); + selectedFriendIds[data] = true; + } + else { + $('#friend-input').select(); + context.alert('Invitation already exists for this musician.'); + } + } + + function removeInvitation(evt) { + delete selectedFriendIds[$(evt.currentTarget).parent().attr('user-id')]; + $(evt.currentTarget).closest('.invitation').remove(); + } + + function createInvitations(sessionId, onComplete) { + var callCount = 0; + var totalInvitations = 0; + $('#selected-friends .invitation').each(function(index, invitation) { + callCount++; + totalInvitations++; + var invite_id = $(invitation).attr('user-id'); + var invite = { + music_session: sessionId, + receiver: invite_id + }; + $.ajax({ + type: "POST", + url: "/api/invitations", + data: invite + }).done(function(response) { + callCount--; + }).fail(app.ajaxError); + }); + // TODO - this is the second time I've used this pattern. + // refactor to make a common utility for this. + function checker() { + if (callCount === 0) { + onComplete(); + } else { + context.setTimeout(checker, 10); + } + } + checker(); + return totalInvitations; + } + + function searchFriends(query) { + if (query.length < 2) { + $('#friend-search-results').empty(); + return; + } + var url = "/api/search?query=" + query + "&userId=" + context.JK.currentUserId; + $.ajax({ + type: "GET", + url: url, + success: friendSearchComplete + }); + } + + function friendSearchComplete(response) { + // reset search results each time + $('#friend-search-results').empty(); + + // loop through each + $.each(response.friends, function() { + // only show friends who are musicians + if (this.musician === true) { + var template = $('#template-friend-search-results').html(); + var searchResultHtml = context.JK.fillTemplate(template, {userId: this.id, name: this.first_name + ' ' + this.last_name}); + $('#friend-search-results').append(searchResultHtml); + $('#friend-search-results').attr('style', 'display:block'); + } + }); + } + + function events() { + $('#btn-choose-friends').click(function() { + friendSelectorDialog.showDialog(selectedFriendIds); + }); + $('#selected-friends').on("click", ".invitation a", removeInvitation); + } + + function showDialog(ids) { + } + + this.initialize = function(friendSelectorDialogInstance) { + friendSelectorDialog = friendSelectorDialogInstance; + friendSelectorHTML = context.JK.fillTemplate($('#template-session-invite-musicians').html()); + events(); + }; + + this.setCallback = function(callback) { + mySaveCallback = callback; + } + + this.searchFriends = searchFriends; + this.addInvitation = addInvitation; + + this.showDialog = showDialog; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 4245dbee8..311134271 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -14,6 +14,8 @@ var addNewGearDialog; var localRecordingsDialog = null; var recordingFinishedDialog = null; + var friendSelectorDialog = null; + var inviteMusicianDialog = null; var screenActive = false; var currentMixerRangeMin = null; var currentMixerRangeMax = null; diff --git a/web/app/views/clients/_inviteSessionMusicians.html.erb b/web/app/views/clients/_inviteSessionMusicians.html.erb new file mode 100644 index 000000000..c47606211 --- /dev/null +++ b/web/app/views/clients/_inviteSessionMusicians.html.erb @@ -0,0 +1,26 @@ + +
+
+
+
+
+ CANCEL  +
+
+ INVITE +
+
+ + + diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb index 86b69b577..f4f90c341 100644 --- a/web/app/views/clients/_session.html.erb +++ b/web/app/views/clients/_session.html.erb @@ -178,15 +178,4 @@ - + diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index bbd434dbf..da5464594 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -105,6 +105,9 @@ var friendSelectorDialog = new JK.FriendSelectorDialog(JK.app); friendSelectorDialog.initialize(); + var inviteMusiciansDialog = new JK.InviteMusiciansDialog(JK.app); + inviteMusiciansDialog.initialize(friendSelectorDialog); + var userDropdown = new JK.UserDropdown(JK.app); JK.UserDropdown = userDropdown; userDropdown.initialize(invitationDialog); @@ -148,7 +151,7 @@ JK.Banner.initialize(); var createSessionScreen = new JK.CreateSessionScreen(JK.app); - createSessionScreen.initialize(invitationDialog, friendSelectorDialog); + createSessionScreen.initialize(invitationDialog, inviteMusiciansDialog); var bandSetupScreen = new JK.BandSetupScreen(JK.app); bandSetupScreen.initialize(invitationDialog, friendSelectorDialog); From f3ab5f829712c34b4f3064caac67d63c22f134fa Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 10:35:34 -0600 Subject: [PATCH 04/28] vrfs1008: createSession invite refactor integration --- web/app/assets/javascripts/createSession.js | 2 +- web/app/assets/javascripts/inviteMusicians.js | 10 ++++++++-- web/app/views/clients/_createSession.html.erb | 6 ------ web/app/views/clients/_inviteSessionMusicians.html.erb | 5 +++++ web/app/views/clients/index.html.erb | 1 + 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 29e52754a..719be1472 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -277,7 +277,7 @@ function initialize(invitationDialogInstance, inviteMusiciansDialogInstance) { invitationDialog = invitationDialogInstance; inviteMusiciansDialog = inviteMusiciansDialogInstance; - $('#create-session-invite-musicians').append(inviteMusiciansDialog.friendSelectorHTML); + $('#create-session-invite-musicians').append(inviteMusiciansDialog.friendSelectorHTML()); events(); loadBands(); loadSessionSettings(); diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index bb3d45b2e..3a53f04d8 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -9,7 +9,7 @@ var userIds = []; var userPhotoUrls = []; var friendSelectorDialog = null; - var friendSelectorHTML = null; + var _friendSelectorHTML = null; var selectedFriendIds = {}; var autoComplete = null; var rest = context.JK.Rest(); @@ -153,9 +153,15 @@ function showDialog(ids) { } + this.friendSelectorHTML = function() { + if (_friendSelectorHTML === null) { + _friendSelectorHTML = context.JK.fillTemplate($('#template-session-invite-musicians').html(),{}); + } + return _friendSelectorHTML; + } + this.initialize = function(friendSelectorDialogInstance) { friendSelectorDialog = friendSelectorDialogInstance; - friendSelectorHTML = context.JK.fillTemplate($('#template-session-invite-musicians').html()); events(); }; diff --git a/web/app/views/clients/_createSession.html.erb b/web/app/views/clients/_createSession.html.erb index faa1ad847..56e5304a4 100644 --- a/web/app/views/clients/_createSession.html.erb +++ b/web/app/views/clients/_createSession.html.erb @@ -86,12 +86,6 @@
- -
-
- -
-
Invite friends and contacts to join you on JamKazam from:
diff --git a/web/app/views/clients/_inviteSessionMusicians.html.erb b/web/app/views/clients/_inviteSessionMusicians.html.erb index c47606211..278fce9de 100644 --- a/web/app/views/clients/_inviteSessionMusicians.html.erb +++ b/web/app/views/clients/_inviteSessionMusicians.html.erb @@ -23,4 +23,9 @@

+ +
+
+ +
diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index da5464594..56cee55ab 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -35,6 +35,7 @@ <%= render "account_profile_avatar" %> <%= render "account_audio_profile" %> <%= render "invitationDialog" %> +<%= render "inviteSessionMusicians" %> <%= render "whatsNextDialog" %> <%= render "recordingFinishedDialog" %> <%= render "localRecordingsDialog" %> From d9d240541d2f89a46641cb8b81fea5643875ef1a Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 14:50:56 -0600 Subject: [PATCH 05/28] vrfs1008: fixing integration of refactoring with createsession --- web/app/assets/javascripts/createSession.js | 2 +- web/app/assets/javascripts/inviteMusicians.js | 16 ++++++++++------ .../stylesheets/client/createSession.css.scss | 2 +- .../clients/_inviteSessionMusicians.html.erb | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 719be1472..8cda52311 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -277,7 +277,7 @@ function initialize(invitationDialogInstance, inviteMusiciansDialogInstance) { invitationDialog = invitationDialogInstance; inviteMusiciansDialog = inviteMusiciansDialogInstance; - $('#create-session-invite-musicians').append(inviteMusiciansDialog.friendSelectorHTML()); + inviteMusiciansDialog.appendFriendSelector($('#create-session-invite-musicians')); events(); loadBands(); loadSessionSettings(); diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 3a53f04d8..a79e7ab6a 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -64,10 +64,10 @@ } function addInvitation(value, data) { - if ($('#selected-friends div[user-id=' + data + ']').length === 0) { + if ($('.selected-friends div[user-id=' + data + ']').length === 0) { var template = $('#template-added-invitation').html(); var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value}); - $('#selected-friends').append(invitationHtml); + $('.selected-friends').append(invitationHtml); $('#friend-input').select(); selectedFriendIds[data] = true; } @@ -85,7 +85,7 @@ function createInvitations(sessionId, onComplete) { var callCount = 0; var totalInvitations = 0; - $('#selected-friends .invitation').each(function(index, invitation) { + $('.selected-friends .invitation').each(function(index, invitation) { callCount++; totalInvitations++; var invite_id = $(invitation).attr('user-id'); @@ -144,10 +144,10 @@ } function events() { - $('#btn-choose-friends').click(function() { + $('.selected-friends').on("click", ".invitation a", removeInvitation); + $('.btn-choose-friends').click(function(){ friendSelectorDialog.showDialog(selectedFriendIds); }); - $('#selected-friends').on("click", ".invitation a", removeInvitation); } function showDialog(ids) { @@ -160,9 +160,13 @@ return _friendSelectorHTML; } + this.appendFriendSelector = function(elem) { + elem.append(this.friendSelectorHTML()); + events(); + }; + this.initialize = function(friendSelectorDialogInstance) { friendSelectorDialog = friendSelectorDialogInstance; - events(); }; this.setCallback = function(callback) { diff --git a/web/app/assets/stylesheets/client/createSession.css.scss b/web/app/assets/stylesheets/client/createSession.css.scss index c9fe9d001..3d189bae2 100644 --- a/web/app/assets/stylesheets/client/createSession.css.scss +++ b/web/app/assets/stylesheets/client/createSession.css.scss @@ -11,7 +11,7 @@ } } -#btn-choose-friends { +.btn-choose-friends { margin:0; } #create-session-genre select, #create-session-band select { diff --git a/web/app/views/clients/_inviteSessionMusicians.html.erb b/web/app/views/clients/_inviteSessionMusicians.html.erb index 278fce9de..f7a9c6683 100644 --- a/web/app/views/clients/_inviteSessionMusicians.html.erb +++ b/web/app/views/clients/_inviteSessionMusicians.html.erb @@ -15,7 +15,7 @@ From ddcfef56c43858053dd5ad44fe472d214fcdffd7 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 14:57:22 -0600 Subject: [PATCH 06/28] vrfs1008: added hook to external createInvitations function --- web/app/assets/javascripts/createSession.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 8cda52311..8acf248c0 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -164,7 +164,7 @@ data: jsonData, success: function(response) { var newSessionId = response.id; - var invitationCount = createInvitations(newSessionId, function() { + var invitationCount = inviteMusiciansDialog.createInvitations(newSessionId, function() { context.location = '#/session/' + newSessionId; }); // Re-loading the session settings will cause the form to reset with the right stuff in it. From a923025a80d1e46ed8202b51764eae103a6c91e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 18:35:05 -0600 Subject: [PATCH 07/28] vrfs1008: changed createInvitations to be anon fcn --- web/app/assets/javascripts/inviteMusicians.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index a79e7ab6a..89a60aab3 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -82,7 +82,7 @@ $(evt.currentTarget).closest('.invitation').remove(); } - function createInvitations(sessionId, onComplete) { + this.createInvitations = function(sessionId, onComplete) { var callCount = 0; var totalInvitations = 0; $('.selected-friends .invitation').each(function(index, invitation) { From 3e2ae677fe67e1ec97e44b8100246f491be50472 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 19:00:42 -0600 Subject: [PATCH 08/28] vrfs1008: integration with active session --- web/app/assets/javascripts/inviteMusicians.js | 1 + web/app/assets/javascripts/session.js | 10 ++++++++-- web/app/controllers/api_music_sessions_controller.rb | 1 + web/app/views/clients/_session.html.erb | 4 ++-- web/app/views/clients/index.html.erb | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 89a60aab3..b9b253bd8 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -151,6 +151,7 @@ } function showDialog(ids) { + //alert('showDialog'); } this.friendSelectorHTML = function() { diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 311134271..d6bd28b2e 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -15,7 +15,7 @@ var localRecordingsDialog = null; var recordingFinishedDialog = null; var friendSelectorDialog = null; - var inviteMusicianDialog = null; + var inviteMusiciansDialog = null; var screenActive = false; var currentMixerRangeMin = null; var currentMixerRangeMax = null; @@ -1304,12 +1304,17 @@ } } + function inviteMusicians() { + inviteMusiciansDialog.showDialog(); + } + function events() { $('#session-resync').on('click', sessionResync); $('#session-contents').on("click", '[action="delete"]', deleteSession); $('#tracks').on('click', 'div[control="mute"]', toggleMute); $('#recording-start-stop').on('click', startStopRecording); $('#open-a-recording').on('click', openRecording); + $('#session-invite-musicians').on('click', inviteMusicians); $('#track-settings').click(function() { configureTrackDialog.showVoiceChatPanel(true); configureTrackDialog.showMusicAudioPanel(true); @@ -1321,9 +1326,10 @@ .on('change-position', onChangePlayPosition); } - this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance) { + this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, inviteMusiciansDialogInstance) { localRecordingsDialog = localRecordingsDialogInstance; recordingFinishedDialog = recordingFinishedDialogInstance; + inviteMusiciansDialog = inviteMusiciansDialogInstance; context.jamClient.SetVURefreshRate(150); playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls')); events(); diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index 79b8cfe84..f24ed56ee 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -45,6 +45,7 @@ class ApiMusicSessionsController < ApiController params[:legal_terms]) if @music_session.errors.any? + byebug # we have to do this because api_session_detail_url will fail with a bad @music_session response.status = :unprocessable_entity respond_with @music_session diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb index f4f90c341..308178ef1 100644 --- a/web/app/views/clients/_session.html.erb +++ b/web/app/views/clients/_session.html.erb @@ -69,8 +69,8 @@

live tracks

-
- + diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 56cee55ab..42586fd45 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -174,7 +174,7 @@ findBandScreen.initialize(); var sessionScreen = new JK.SessionScreen(JK.app); - sessionScreen.initialize(localRecordingsDialog, recordingFinishedDialog); + sessionScreen.initialize(localRecordingsDialog, recordingFinishedDialog, inviteMusiciansDialog); var sessionSettingsDialog = new JK.SessionSettingsDialog(JK.app, sessionScreen); sessionSettingsDialog.initialize(); From 895866398743d8034415ec279ed17c5c2b61b68e Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 19:01:11 -0600 Subject: [PATCH 09/28] vrfs1008: removed debug stmt --- web/app/controllers/api_music_sessions_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index f24ed56ee..79b8cfe84 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -45,7 +45,6 @@ class ApiMusicSessionsController < ApiController params[:legal_terms]) if @music_session.errors.any? - byebug # we have to do this because api_session_detail_url will fail with a bad @music_session response.status = :unprocessable_entity respond_with @music_session From 11074044a70834fea40e09914332523ac9f13134 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 19:37:52 -0600 Subject: [PATCH 10/28] vrfs1008: --- web/app/assets/javascripts/inviteMusicians.js | 4 +++- web/app/views/clients/_inviteSessionMusicians.html.erb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index b9b253bd8..17c08459d 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -151,7 +151,9 @@ } function showDialog(ids) { - //alert('showDialog'); + if (0 == $('#join-session-invite-musicians .friendbox').length) { + this.appendFriendSelector($('#join-session-invite-musicians')); + } } this.friendSelectorHTML = function() { diff --git a/web/app/views/clients/_inviteSessionMusicians.html.erb b/web/app/views/clients/_inviteSessionMusicians.html.erb index f7a9c6683..6963c4f10 100644 --- a/web/app/views/clients/_inviteSessionMusicians.html.erb +++ b/web/app/views/clients/_inviteSessionMusicians.html.erb @@ -1,6 +1,6 @@
-
+

From 33c64c28811125fdf24d4c276d483be0f905a756 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 22:33:32 -0600 Subject: [PATCH 11/28] vrfs1008: fixing integration bugs --- web/app/assets/javascripts/createSession.js | 4 +-- web/app/assets/javascripts/inviteMusicians.js | 36 ++++++++++++++----- web/app/assets/javascripts/session.js | 2 +- .../controllers/api_invitations_controller.rb | 7 ++-- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 8acf248c0..b5c7808bf 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -13,13 +13,13 @@ var sessionSettings = {}; function beforeShow(data) { - inviteMusiciansDialog.afterShow(data); + inviteMusiciansDialog.clearSelections(); context.JK.GenreSelectorHelper.render('#create-session-genre'); resetForm(); } function afterShow(data) { - inviteMusiciansDialog.afterShow(data); + inviteMusiciansDialog.loadFriends(); } function resetForm() { diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 17c08459d..74e767f73 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -17,13 +17,17 @@ var newSelections = {}; var mySaveCallback; - this. beforeShow = function(data) { + this.clearSelections = function() { userNames = []; userIds = []; userPhotoUrls = []; - } + selectedIds = {}; + selectedFriendIds = {}; + newSelections = {}; + $('.selected-friends').empty(); + }; - this.afterShow = function(data) { + this.loadFriends = function() { friendSelectorDialog.setCallback(friendSelectorCallback); var friends = rest.getFriends({ id: context.JK.currentUserId }) @@ -144,16 +148,32 @@ } function events() { - $('.selected-friends').on("click", ".invitation a", removeInvitation); - $('.btn-choose-friends').click(function(){ - friendSelectorDialog.showDialog(selectedFriendIds); - }); + if ($('.btn-choose-friends').data('events') === undefined) { + $('.selected-friends').on("click", ".invitation a", removeInvitation); + $('.btn-choose-friends').click(function(){ + friendSelectorDialog.showDialog(selectedFriendIds); + }); + } } - function showDialog(ids) { + function showDialog(sessionId) { if (0 == $('#join-session-invite-musicians .friendbox').length) { this.appendFriendSelector($('#join-session-invite-musicians')); } + selectedFriendIds = {}; + $.ajax({ + ajax: false, + url: "/api/invitations", + data: { + session_id: sessionId, + sender: context.JK.currentUserId + } + }).done(function(response) { + response.map(function(item) { + selectedFriendIds[item['receiver']['id']] = true; + }); + logger.debug(selectedFriendIds); + }).fail(app.ajaxError); } this.friendSelectorHTML = function() { diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index d6bd28b2e..ba21b7340 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -1305,7 +1305,7 @@ } function inviteMusicians() { - inviteMusiciansDialog.showDialog(); + inviteMusiciansDialog.showDialog(sessionId); } function events() { diff --git a/web/app/controllers/api_invitations_controller.rb b/web/app/controllers/api_invitations_controller.rb index 266f97d6d..1de124bec 100644 --- a/web/app/controllers/api_invitations_controller.rb +++ b/web/app/controllers/api_invitations_controller.rb @@ -14,8 +14,11 @@ class ApiInvitationsController < ApiController if current_user.id != sender_id raise PermissionError, "You can only ask for your own sent invitations" end - - @invitations = Invitation.where(:sender_id => current_user.id) + if session_id = params[:session_id] + @invitations = Invitation.where(:sender_id => sender_id, :music_session_id => session_id) + else + @invitations = Invitation.where(:sender_id => current_user.id) + end elsif !receiver_id.nil? if current_user.id != receiver_id raise PermissionError, "You can only ask for your own received invitations" From a8b7f3e5a610d8a87c3aaa56cdf0cb9ceea72551 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 13 Jan 2014 23:18:09 -0600 Subject: [PATCH 12/28] vrfs1008: integration bug fixing --- web/app/assets/javascripts/inviteMusicians.js | 28 +++++-------- web/app/assets/javascripts/session.js | 2 +- .../controllers/api_invitations_controller.rb | 42 +++++++++++-------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 74e767f73..10226ff6b 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -9,21 +9,16 @@ var userIds = []; var userPhotoUrls = []; var friendSelectorDialog = null; - var _friendSelectorHTML = null; var selectedFriendIds = {}; var autoComplete = null; var rest = context.JK.Rest(); - var selectedIds = {}; - var newSelections = {}; var mySaveCallback; this.clearSelections = function() { userNames = []; userIds = []; userPhotoUrls = []; - selectedIds = {}; selectedFriendIds = {}; - newSelections = {}; $('.selected-friends').empty(); }; @@ -148,19 +143,19 @@ } function events() { - if ($('.btn-choose-friends').data('events') === undefined) { + //if ($('.btn-choose-friends').data('events') === undefined) { $('.selected-friends').on("click", ".invitation a", removeInvitation); $('.btn-choose-friends').click(function(){ friendSelectorDialog.showDialog(selectedFriendIds); }); - } + //} } - function showDialog(sessionId) { + this.inviteJoinSession = function(sessionId) { if (0 == $('#join-session-invite-musicians .friendbox').length) { this.appendFriendSelector($('#join-session-invite-musicians')); } - selectedFriendIds = {}; + this.clearSelections(); $.ajax({ ajax: false, url: "/api/invitations", @@ -170,21 +165,19 @@ } }).done(function(response) { response.map(function(item) { - selectedFriendIds[item['receiver']['id']] = true; + var dd = item['receiver'] + selectedFriendIds[dd['id']] = true; + addInvitation(dd['name'], dd['id']); }); - logger.debug(selectedFriendIds); }).fail(app.ajaxError); } - this.friendSelectorHTML = function() { - if (_friendSelectorHTML === null) { - _friendSelectorHTML = context.JK.fillTemplate($('#template-session-invite-musicians').html(),{}); - } - return _friendSelectorHTML; + function friendSelectorHTML() { + return context.JK.fillTemplate($('#template-session-invite-musicians').html(),{}); } this.appendFriendSelector = function(elem) { - elem.append(this.friendSelectorHTML()); + elem.append(friendSelectorHTML()); events(); }; @@ -199,7 +192,6 @@ this.searchFriends = searchFriends; this.addInvitation = addInvitation; - this.showDialog = showDialog; return this; }; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index ba21b7340..0b253d8bd 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -1305,7 +1305,7 @@ } function inviteMusicians() { - inviteMusiciansDialog.showDialog(sessionId); + inviteMusiciansDialog.inviteJoinSession(sessionId); } function events() { diff --git a/web/app/controllers/api_invitations_controller.rb b/web/app/controllers/api_invitations_controller.rb index 1de124bec..ac487c8a9 100644 --- a/web/app/controllers/api_invitations_controller.rb +++ b/web/app/controllers/api_invitations_controller.rb @@ -37,25 +37,33 @@ class ApiInvitationsController < ApiController sender = current_user join_request = JoinRequest.find(params[:join_request]) unless params[:join_request].nil? - @invitation = Invitation.new - @invitation.music_session = music_session - @invitation.sender = sender - @invitation.receiver = receiver - @invitation.join_request = join_request - - @invitation.save - - unless @invitation.errors.any? - User.save_session_settings(current_user, music_session) - - # send notification - Notification.send_session_invitation(receiver, current_user, music_session.id) + @invitation = Invitation.limit(1) + .where(:receiver_id => params[:receiver], + :sender_id => current_user.id, + :music_session_id => params[:music_session]) + .first + if @invitation respond_with @invitation, :responder => ApiResponder, :location => api_invitation_detail_url(@invitation) - else - # we have to do this because api_invitation_detail_url will fail with a bad @invitation - response.status = :unprocessable_entity - respond_with @invitation + @invitation = Invitation.new + @invitation.music_session = music_session + @invitation.sender = sender + @invitation.receiver = receiver + @invitation.join_request = join_request + @invitation.save + + unless @invitation.errors.any? + User.save_session_settings(current_user, music_session) + + # send notification + Notification.send_session_invitation(receiver, current_user, music_session.id) + respond_with @invitation, :responder => ApiResponder, :location => api_invitation_detail_url(@invitation) + + else + # we have to do this because api_invitation_detail_url will fail with a bad @invitation + response.status = :unprocessable_entity + respond_with @invitation + end end end From 28d346fe23d03b00432bcf4ddaa4fc6d5f9074f6 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 00:02:54 -0600 Subject: [PATCH 13/28] vrfs1008: refactoring, cleanup, bug fixes --- web/app/assets/javascripts/createSession.js | 2 +- web/app/assets/javascripts/inviteMusicians.js | 76 ++++++++++--------- web/app/assets/javascripts/session.js | 2 +- web/app/views/clients/_createSession.html.erb | 7 -- ...ans.html.erb => _inviteMusicians.html.erb} | 13 +++- web/app/views/clients/index.html.erb | 2 +- 6 files changed, 55 insertions(+), 47 deletions(-) rename web/app/views/clients/{_inviteSessionMusicians.html.erb => _inviteMusicians.html.erb} (63%) diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index b5c7808bf..4f8026dc0 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -277,7 +277,7 @@ function initialize(invitationDialogInstance, inviteMusiciansDialogInstance) { invitationDialog = invitationDialogInstance; inviteMusiciansDialog = inviteMusiciansDialogInstance; - inviteMusiciansDialog.appendFriendSelector($('#create-session-invite-musicians')); + inviteMusiciansDialog.inviteSessionCreate('#create-session-invite-musicians'); events(); loadBands(); loadSessionSettings(); diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 10226ff6b..fa26c0429 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -13,6 +13,31 @@ var autoComplete = null; var rest = context.JK.Rest(); var mySaveCallback; + var inviteAction = 'create'; // create/update + + this.inviteSessionCreate = function(elemSelector) { + inviteAction = 'create'; + _appendFriendSelector($(elemSelector)); + }; + + this.inviteSessionUpdate = function(elemSelector, sessionId) { + inviteAction = 'update'; + if (0 == $(elemSelector + ' .friendbox').length) { + _appendFriendSelector($(elemSelector)); + } + this.clearSelections(); + $.ajax({ + ajax: false, + url: "/api/invitations", + data: { session_id: sessionId, sender: context.JK.currentUserId } + }).done(function(response) { + response.map(function(item) { + var dd = item['receiver'] + selectedFriendIds[dd['id']] = true; + addInvitation(dd['name'], dd['id']); + }); + }).fail(app.ajaxError); + } this.clearSelections = function() { userNames = []; @@ -65,7 +90,11 @@ function addInvitation(value, data) { if ($('.selected-friends div[user-id=' + data + ']').length === 0) { var template = $('#template-added-invitation').html(); - var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value}); + var image_style = inviteAction == 'update' ? 'display:none' : ''; + var invitationHtml = context.JK.fillTemplate(template, + {userId: data, + userName: value, + image_style: image_style}); $('.selected-friends').append(invitationHtml); $('#friend-input').select(); selectedFriendIds[data] = true; @@ -142,43 +171,22 @@ }); } - function events() { - //if ($('.btn-choose-friends').data('events') === undefined) { - $('.selected-friends').on("click", ".invitation a", removeInvitation); - $('.btn-choose-friends').click(function(){ - friendSelectorDialog.showDialog(selectedFriendIds); - }); - //} + function _events() { + $('#selected-friends-'+inviteAction).on("click", ".invitation a", removeInvitation); + $('#btn-choose-friends-'+inviteAction).click(function(){ + friendSelectorDialog.showDialog(selectedFriendIds); + }); } - this.inviteJoinSession = function(sessionId) { - if (0 == $('#join-session-invite-musicians .friendbox').length) { - this.appendFriendSelector($('#join-session-invite-musicians')); - } - this.clearSelections(); - $.ajax({ - ajax: false, - url: "/api/invitations", - data: { - session_id: sessionId, - sender: context.JK.currentUserId - } - }).done(function(response) { - response.map(function(item) { - var dd = item['receiver'] - selectedFriendIds[dd['id']] = true; - addInvitation(dd['name'], dd['id']); - }); - }).fail(app.ajaxError); + function _friendSelectorHTML() { + return context.JK.fillTemplate($('#template-session-invite-musicians').html(), + {choose_friends_id: 'btn-choose-friends-'+inviteAction, + selected_friends_id: 'selected-friends-'+inviteAction}); } - function friendSelectorHTML() { - return context.JK.fillTemplate($('#template-session-invite-musicians').html(),{}); - } - - this.appendFriendSelector = function(elem) { - elem.append(friendSelectorHTML()); - events(); + function _appendFriendSelector(elemSelector) { + elemSelector.append(_friendSelectorHTML()); + _events(); }; this.initialize = function(friendSelectorDialogInstance) { diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 0b253d8bd..569b011f9 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -1305,7 +1305,7 @@ } function inviteMusicians() { - inviteMusiciansDialog.inviteJoinSession(sessionId); + inviteMusiciansDialog.inviteSessionUpdate('#update-session-invite-musicians', sessionId); } function events() { diff --git a/web/app/views/clients/_createSession.html.erb b/web/app/views/clients/_createSession.html.erb index 56e5304a4..7a0b3f565 100644 --- a/web/app/views/clients/_createSession.html.erb +++ b/web/app/views/clients/_createSession.html.erb @@ -156,13 +156,6 @@
- - - + + + diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 42586fd45..d19151888 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -35,7 +35,7 @@ <%= render "account_profile_avatar" %> <%= render "account_audio_profile" %> <%= render "invitationDialog" %> -<%= render "inviteSessionMusicians" %> +<%= render "inviteMusicians" %> <%= render "whatsNextDialog" %> <%= render "recordingFinishedDialog" %> <%= render "localRecordingsDialog" %> From cfeed312bf2ed5887299c5f264d26f20deb30ae7 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 00:12:00 -0600 Subject: [PATCH 14/28] vrfs1008: refactoring --- web/app/assets/javascripts/createSession.js | 14 +++++++------- web/app/assets/javascripts/inviteMusicians.js | 6 +++--- web/app/assets/javascripts/session.js | 8 ++++---- web/app/views/clients/_inviteMusicians.html.erb | 2 +- web/app/views/clients/index.html.erb | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index 4f8026dc0..ae3e5ff92 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -8,18 +8,18 @@ var rest = context.JK.Rest(); var realtimeMessaging = context.JK.JamServer; var invitationDialog = null; - var inviteMusiciansDialog = null; + var inviteMusiciansUtil = null; var MAX_GENRES = 1; var sessionSettings = {}; function beforeShow(data) { - inviteMusiciansDialog.clearSelections(); + inviteMusiciansUtil.clearSelections(); context.JK.GenreSelectorHelper.render('#create-session-genre'); resetForm(); } function afterShow(data) { - inviteMusiciansDialog.loadFriends(); + inviteMusiciansUtil.loadFriends(); } function resetForm() { @@ -164,7 +164,7 @@ data: jsonData, success: function(response) { var newSessionId = response.id; - var invitationCount = inviteMusiciansDialog.createInvitations(newSessionId, function() { + var invitationCount = inviteMusiciansUtil.createInvitations(newSessionId, function() { context.location = '#/session/' + newSessionId; }); // Re-loading the session settings will cause the form to reset with the right stuff in it. @@ -274,10 +274,10 @@ }); } - function initialize(invitationDialogInstance, inviteMusiciansDialogInstance) { + function initialize(invitationDialogInstance, inviteMusiciansUtilInstance) { invitationDialog = invitationDialogInstance; - inviteMusiciansDialog = inviteMusiciansDialogInstance; - inviteMusiciansDialog.inviteSessionCreate('#create-session-invite-musicians'); + inviteMusiciansUtil = inviteMusiciansUtilInstance; + inviteMusiciansUtil.inviteSessionCreate('#create-session-invite-musicians'); events(); loadBands(); loadSessionSettings(); diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index fa26c0429..98213fb16 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -3,7 +3,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.InviteMusiciansDialog = function(app) { + context.JK.InviteMusiciansUtil = function(app) { var logger = context.JK.logger; var userNames = []; var userIds = []; @@ -90,11 +90,11 @@ function addInvitation(value, data) { if ($('.selected-friends div[user-id=' + data + ']').length === 0) { var template = $('#template-added-invitation').html(); - var image_style = inviteAction == 'update' ? 'display:none' : ''; + var imgStyle = inviteAction == 'update' ? 'display:none' : ''; var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value, - image_style: image_style}); + imageStyle: imgStyle}); $('.selected-friends').append(invitationHtml); $('#friend-input').select(); selectedFriendIds[data] = true; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 569b011f9..e017d1de6 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -15,7 +15,7 @@ var localRecordingsDialog = null; var recordingFinishedDialog = null; var friendSelectorDialog = null; - var inviteMusiciansDialog = null; + var inviteMusiciansUtil = null; var screenActive = false; var currentMixerRangeMin = null; var currentMixerRangeMax = null; @@ -1305,7 +1305,7 @@ } function inviteMusicians() { - inviteMusiciansDialog.inviteSessionUpdate('#update-session-invite-musicians', sessionId); + inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', sessionId); } function events() { @@ -1326,10 +1326,10 @@ .on('change-position', onChangePlayPosition); } - this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, inviteMusiciansDialogInstance) { + this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, inviteMusiciansUtilInstance) { localRecordingsDialog = localRecordingsDialogInstance; recordingFinishedDialog = recordingFinishedDialogInstance; - inviteMusiciansDialog = inviteMusiciansDialogInstance; + inviteMusiciansUtil = inviteMusiciansUtilInstance; context.jamClient.SetVURefreshRate(150); playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls')); events(); diff --git a/web/app/views/clients/_inviteMusicians.html.erb b/web/app/views/clients/_inviteMusicians.html.erb index 3148e5daa..09999942c 100644 --- a/web/app/views/clients/_inviteMusicians.html.erb +++ b/web/app/views/clients/_inviteMusicians.html.erb @@ -33,6 +33,6 @@ diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index d19151888..0f0e47fb3 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -106,8 +106,8 @@ var friendSelectorDialog = new JK.FriendSelectorDialog(JK.app); friendSelectorDialog.initialize(); - var inviteMusiciansDialog = new JK.InviteMusiciansDialog(JK.app); - inviteMusiciansDialog.initialize(friendSelectorDialog); + var inviteMusiciansUtil = new JK.InviteMusiciansUtil(JK.app); + inviteMusiciansUtil.initialize(friendSelectorDialog); var userDropdown = new JK.UserDropdown(JK.app); JK.UserDropdown = userDropdown; @@ -152,7 +152,7 @@ JK.Banner.initialize(); var createSessionScreen = new JK.CreateSessionScreen(JK.app); - createSessionScreen.initialize(invitationDialog, inviteMusiciansDialog); + createSessionScreen.initialize(invitationDialog, inviteMusiciansUtil); var bandSetupScreen = new JK.BandSetupScreen(JK.app); bandSetupScreen.initialize(invitationDialog, friendSelectorDialog); @@ -174,7 +174,7 @@ findBandScreen.initialize(); var sessionScreen = new JK.SessionScreen(JK.app); - sessionScreen.initialize(localRecordingsDialog, recordingFinishedDialog, inviteMusiciansDialog); + sessionScreen.initialize(localRecordingsDialog, recordingFinishedDialog, inviteMusiciansUtil); var sessionSettingsDialog = new JK.SessionSettingsDialog(JK.app, sessionScreen); sessionSettingsDialog.initialize(); From 61a7dcd50f47fd40c8ac8d34dadbf6b6bd70eeaf Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 01:01:37 -0600 Subject: [PATCH 15/28] vrfs1008: added existingInvites storage for updates --- web/app/assets/javascripts/inviteMusicians.js | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 98213fb16..1e8e29fb7 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -10,9 +10,9 @@ var userPhotoUrls = []; var friendSelectorDialog = null; var selectedFriendIds = {}; + var existingInvites = []; var autoComplete = null; var rest = context.JK.Rest(); - var mySaveCallback; var inviteAction = 'create'; // create/update this.inviteSessionCreate = function(elemSelector) { @@ -21,6 +21,7 @@ }; this.inviteSessionUpdate = function(elemSelector, sessionId) { + friendSelectorDialog.setCallback(friendSelectorCallback); inviteAction = 'update'; if (0 == $(elemSelector + ' .friendbox').length) { _appendFriendSelector($(elemSelector)); @@ -32,9 +33,9 @@ data: { session_id: sessionId, sender: context.JK.currentUserId } }).done(function(response) { response.map(function(item) { - var dd = item['receiver'] - selectedFriendIds[dd['id']] = true; - addInvitation(dd['name'], dd['id']); + var dd = item['receiver']; + existingInvites.push(dd.id); + addInvitation(dd.name, dd.id); }); }).fail(app.ajaxError); } @@ -44,6 +45,7 @@ userIds = []; userPhotoUrls = []; selectedFriendIds = {}; + existingInvites = []; $('.selected-friends').empty(); }; @@ -83,14 +85,19 @@ function friendSelectorCallback(newSelections) { var keys = Object.keys(newSelections); for (var i=0; i < keys.length; i++) { - addInvitation(newSelections[keys[i]].userName, newSelections[keys[i]].userId); + var dd = newSelections[keys[i]]; + addInvitation(dd.userName, dd.userId); } } + function _inviteExists(userID) { + return 0 <= existingInvites.indexOf(userID); + } + function addInvitation(value, data) { if ($('.selected-friends div[user-id=' + data + ']').length === 0) { var template = $('#template-added-invitation').html(); - var imgStyle = inviteAction == 'update' ? 'display:none' : ''; + var imgStyle = _inviteExists(data) ? 'display:none' : ''; var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value, @@ -98,36 +105,39 @@ $('.selected-friends').append(invitationHtml); $('#friend-input').select(); selectedFriendIds[data] = true; - } - else { + + } else { $('#friend-input').select(); context.alert('Invitation already exists for this musician.'); } } function removeInvitation(evt) { - delete selectedFriendIds[$(evt.currentTarget).parent().attr('user-id')]; + var dd = $(evt.currentTarget).parent().attr('user-id'); + delete selectedFriendIds[dd]; $(evt.currentTarget).closest('.invitation').remove(); } this.createInvitations = function(sessionId, onComplete) { var callCount = 0; - var totalInvitations = 0; + var totalInvitations = existingInvites.length; $('.selected-friends .invitation').each(function(index, invitation) { - callCount++; - totalInvitations++; var invite_id = $(invitation).attr('user-id'); - var invite = { - music_session: sessionId, - receiver: invite_id - }; - $.ajax({ - type: "POST", - url: "/api/invitations", - data: invite - }).done(function(response) { - callCount--; - }).fail(app.ajaxError); + if (!_inviteExists(invite_id)) { + callCount++; + totalInvitations++; + var invite = { + music_session: sessionId, + receiver: invite_id + }; + $.ajax({ + type: "POST", + url: "/api/invitations", + data: invite + }).done(function(response) { + callCount--; + }).fail(app.ajaxError); + } }); // TODO - this is the second time I've used this pattern. // refactor to make a common utility for this. @@ -171,13 +181,6 @@ }); } - function _events() { - $('#selected-friends-'+inviteAction).on("click", ".invitation a", removeInvitation); - $('#btn-choose-friends-'+inviteAction).click(function(){ - friendSelectorDialog.showDialog(selectedFriendIds); - }); - } - function _friendSelectorHTML() { return context.JK.fillTemplate($('#template-session-invite-musicians').html(), {choose_friends_id: 'btn-choose-friends-'+inviteAction, @@ -186,20 +189,16 @@ function _appendFriendSelector(elemSelector) { elemSelector.append(_friendSelectorHTML()); - _events(); + $('#selected-friends-'+inviteAction).on("click", ".invitation a", removeInvitation); + $('#btn-choose-friends-'+inviteAction).click(function(){ + friendSelectorDialog.showDialog(selectedFriendIds); + }); }; this.initialize = function(friendSelectorDialogInstance) { friendSelectorDialog = friendSelectorDialogInstance; }; - this.setCallback = function(callback) { - mySaveCallback = callback; - } - - this.searchFriends = searchFriends; - this.addInvitation = addInvitation; - return this; }; From 3c6af2e028e574eed8cc1c5554a18085ea8ba4b3 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 01:31:20 -0600 Subject: [PATCH 16/28] vrfs1008: sending updated invites --- web/app/assets/javascripts/inviteMusicians.js | 18 +++++++++++------- .../views/clients/_inviteMusicians.html.erb | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 1e8e29fb7..8bd750bce 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -9,7 +9,7 @@ var userIds = []; var userPhotoUrls = []; var friendSelectorDialog = null; - var selectedFriendIds = {}; + var invitedFriends = {}; var existingInvites = []; var autoComplete = null; var rest = context.JK.Rest(); @@ -25,6 +25,9 @@ inviteAction = 'update'; if (0 == $(elemSelector + ' .friendbox').length) { _appendFriendSelector($(elemSelector)); + $('#btn-save-invites').click(function() { + createInvitations(sessionId); + }); } this.clearSelections(); $.ajax({ @@ -44,7 +47,7 @@ userNames = []; userIds = []; userPhotoUrls = []; - selectedFriendIds = {}; + invitedFriends = {}; existingInvites = []; $('.selected-friends').empty(); }; @@ -104,7 +107,7 @@ imageStyle: imgStyle}); $('.selected-friends').append(invitationHtml); $('#friend-input').select(); - selectedFriendIds[data] = true; + invitedFriends[data] = true; } else { $('#friend-input').select(); @@ -114,11 +117,11 @@ function removeInvitation(evt) { var dd = $(evt.currentTarget).parent().attr('user-id'); - delete selectedFriendIds[dd]; + delete invitedFriends[dd]; $(evt.currentTarget).closest('.invitation').remove(); } - this.createInvitations = function(sessionId, onComplete) { + function createInvitations(sessionId, onComplete) { var callCount = 0; var totalInvitations = existingInvites.length; $('.selected-friends .invitation').each(function(index, invitation) { @@ -148,9 +151,10 @@ context.setTimeout(checker, 10); } } - checker(); + if (onComplete) checker(); return totalInvitations; } + this.createInvitations = createInvitations; function searchFriends(query) { if (query.length < 2) { @@ -191,7 +195,7 @@ elemSelector.append(_friendSelectorHTML()); $('#selected-friends-'+inviteAction).on("click", ".invitation a", removeInvitation); $('#btn-choose-friends-'+inviteAction).click(function(){ - friendSelectorDialog.showDialog(selectedFriendIds); + friendSelectorDialog.showDialog(invitedFriends); }); }; diff --git a/web/app/views/clients/_inviteMusicians.html.erb b/web/app/views/clients/_inviteMusicians.html.erb index 09999942c..b80f76f5d 100644 --- a/web/app/views/clients/_inviteMusicians.html.erb +++ b/web/app/views/clients/_inviteMusicians.html.erb @@ -1,4 +1,4 @@ - +
From ea6391e46e2a368a51908e336aa980dc1a173bf0 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 01:52:22 -0600 Subject: [PATCH 17/28] vrfs1008: styled the invite dialog box; fixed bug updating invites --- web/app/assets/javascripts/inviteMusicians.js | 3 ++- web/app/assets/stylesheets/client/content.css.scss | 1 - web/app/assets/stylesheets/client/session.css.scss | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 8bd750bce..802fc5839 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -23,10 +23,11 @@ this.inviteSessionUpdate = function(elemSelector, sessionId) { friendSelectorDialog.setCallback(friendSelectorCallback); inviteAction = 'update'; + var sid = sessionId; if (0 == $(elemSelector + ' .friendbox').length) { _appendFriendSelector($(elemSelector)); $('#btn-save-invites').click(function() { - createInvitations(sessionId); + createInvitations(sid); }); } this.clearSelections(); diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss index 88550ab29..1f96cb299 100644 --- a/web/app/assets/stylesheets/client/content.css.scss +++ b/web/app/assets/stylesheets/client/content.css.scss @@ -204,7 +204,6 @@ .friendbox { padding:5px; - width:100%; height:60px; } diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index ad415a5da..fc455d55a 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -716,3 +716,6 @@ table.vu td { } +#update-session-invite-musicians { + margin: 10px; +} \ No newline at end of file From 91007055a0e0de518f35dfdfe496fae3bc6c89c7 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 02:29:07 -0600 Subject: [PATCH 18/28] vrfs1008: fixed updating invites bug --- web/app/assets/javascripts/inviteMusicians.js | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 802fc5839..95f5f58cb 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -9,11 +9,12 @@ var userIds = []; var userPhotoUrls = []; var friendSelectorDialog = null; - var invitedFriends = {}; + var invitedFriends = []; var existingInvites = []; var autoComplete = null; var rest = context.JK.Rest(); var inviteAction = 'create'; // create/update + var updateSessionID = null; this.inviteSessionCreate = function(elemSelector) { inviteAction = 'create'; @@ -21,16 +22,16 @@ }; this.inviteSessionUpdate = function(elemSelector, sessionId) { + this.clearSelections(); + updateSessionID = sessionId; friendSelectorDialog.setCallback(friendSelectorCallback); inviteAction = 'update'; - var sid = sessionId; if (0 == $(elemSelector + ' .friendbox').length) { _appendFriendSelector($(elemSelector)); $('#btn-save-invites').click(function() { - createInvitations(sid); + createInvitations(updateSessionID); }); } - this.clearSelections(); $.ajax({ ajax: false, url: "/api/invitations", @@ -48,8 +49,9 @@ userNames = []; userIds = []; userPhotoUrls = []; - invitedFriends = {}; + invitedFriends = []; existingInvites = []; + updateSessionID = null; $('.selected-friends').empty(); }; @@ -99,7 +101,7 @@ } function addInvitation(value, data) { - if ($('.selected-friends div[user-id=' + data + ']').length === 0) { + if (0 > invitedFriends.indexOf(data)) { var template = $('#template-added-invitation').html(); var imgStyle = _inviteExists(data) ? 'display:none' : ''; var invitationHtml = context.JK.fillTemplate(template, @@ -108,7 +110,7 @@ imageStyle: imgStyle}); $('.selected-friends').append(invitationHtml); $('#friend-input').select(); - invitedFriends[data] = true; + invitedFriends.push(data); } else { $('#friend-input').select(); @@ -117,19 +119,17 @@ } function removeInvitation(evt) { - var dd = $(evt.currentTarget).parent().attr('user-id'); - delete invitedFriends[dd]; + var idx = invitedFriends.indexOf($(evt.currentTarget).parent().attr('user-id')); + if (0 <= idx) invitedFriends.splice(idx, 1); $(evt.currentTarget).closest('.invitation').remove(); } function createInvitations(sessionId, onComplete) { var callCount = 0; - var totalInvitations = existingInvites.length; - $('.selected-friends .invitation').each(function(index, invitation) { - var invite_id = $(invitation).attr('user-id'); + var totalInvitations = invitedFriends.length - existingInvites.length; + invitedFriends.map(function(invite_id) { if (!_inviteExists(invite_id)) { callCount++; - totalInvitations++; var invite = { music_session: sessionId, receiver: invite_id @@ -146,11 +146,7 @@ // TODO - this is the second time I've used this pattern. // refactor to make a common utility for this. function checker() { - if (callCount === 0) { - onComplete(); - } else { - context.setTimeout(checker, 10); - } + callCount === 0 ? onComplete() : context.setTimeout(checker, 10); } if (onComplete) checker(); return totalInvitations; @@ -196,7 +192,9 @@ elemSelector.append(_friendSelectorHTML()); $('#selected-friends-'+inviteAction).on("click", ".invitation a", removeInvitation); $('#btn-choose-friends-'+inviteAction).click(function(){ - friendSelectorDialog.showDialog(invitedFriends); + var obj = {}; + invitedFriends.map(function(uid) { obj[uid] = true; }); + friendSelectorDialog.showDialog(obj); }); }; From 51c9892c6327977a62ca3208be1fec44824c24ee Mon Sep 17 00:00:00 2001 From: Daniel Weigh Date: Tue, 21 Jan 2014 21:54:39 -0500 Subject: [PATCH 19/28] * lint error cleanup --- web/app/views/api_search/index.rabl | 2 +- web/app/views/clients/_testBridge.html.erb | 2 +- web/app/views/users/_create_account_form.html.erb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index 6442d3cae..627b96a7a 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -81,7 +81,7 @@ if @search.bands_filter_search? node :user_id do |uu| uu.id end node :photo_url do |uu| uu.photo_url end node :name do |uu| uu.name end - node :instruments do |uu| uu.instruments.map(&:id).join(',') end + node :instruments do |uu| uu.instruments.map(&:id).join(',') end end node :follow_count do |band| @search.follow_count(band) end diff --git a/web/app/views/clients/_testBridge.html.erb b/web/app/views/clients/_testBridge.html.erb index 6b6d51ada..7a6286a27 100644 --- a/web/app/views/clients/_testBridge.html.erb +++ b/web/app/views/clients/_testBridge.html.erb @@ -40,7 +40,7 @@
-

SetFTUE

diff --git a/web/app/views/users/_create_account_form.html.erb b/web/app/views/users/_create_account_form.html.erb index 44d0567b2..77cbbafed 100644 --- a/web/app/views/users/_create_account_form.html.erb +++ b/web/app/views/users/_create_account_form.html.erb @@ -102,7 +102,7 @@

--> - <%= f.submit "CREATE ACCOUNT", class: "right button-orange" %>

+ <%= f.submit "CREATE ACCOUNT", class: "right button-orange" %>

From b8a51e9f6c800fca8b16afc2a97d895c1ae6ab0d Mon Sep 17 00:00:00 2001 From: Daniel Weigh Date: Tue, 21 Jan 2014 21:55:38 -0500 Subject: [PATCH 20/28] VRFS-930 Musician fixes --- web/app/assets/javascripts/findMusician.js | 12 +++-- .../stylesheets/client/content.css.scss | 3 ++ .../stylesheets/client/profile.css.scss | 6 +++ web/app/views/api_search/index.rabl | 4 ++ web/app/views/clients/_musicians.html.erb | 49 ++++++++++++------- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/web/app/assets/javascripts/findMusician.js b/web/app/assets/javascripts/findMusician.js index cdd36acb0..52a44c077 100644 --- a/web/app/assets/javascripts/findMusician.js +++ b/web/app/assets/javascripts/findMusician.js @@ -89,6 +89,10 @@ for (ii=0, len=musicians.length; ii < len; ii++) { mm = musicians[ii]; + if (context.JK.currentUserId === mm.id) { + // VRFS-294.3 (David) => skip if current user is musician + continue; + } instr_logos = ''; for (var jj=0, ilen=mm['instruments'].length; jj a.smallbutton { margin: 2px; + &.button-grey { + display:none; // @FIXME VRFS-930 / VRFS-931 per comment from David - don't show. + } } } diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss index 5e766429d..b51f22bf6 100644 --- a/web/app/assets/stylesheets/client/profile.css.scss +++ b/web/app/assets/stylesheets/client/profile.css.scss @@ -1,5 +1,11 @@ @import "client/common.css.scss"; +.profile-head { + +} +.profile-body { + +} .profile-header { padding:10px 20px; // height:120px; diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index 627b96a7a..1a9a9c946 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -43,6 +43,10 @@ if @search.musicians_filter_search? @search.is_follower?(musician) end + node :biography do |musician| + musician.biography.nil? ? "" : musician.biography + end + child :musician_instruments => :instruments do attributes :instrument_id, :description, :proficiency_level, :priority end diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb index 0df6f6e06..2b4c565de 100644 --- a/web/app/views/clients/_musicians.html.erb +++ b/web/app/views/clients/_musicians.html.erb @@ -22,24 +22,35 @@ From b5a32b0a53a528024f793ad250ea010eb861df93 Mon Sep 17 00:00:00 2001 From: Daniel Weigh Date: Tue, 21 Jan 2014 21:56:03 -0500 Subject: [PATCH 21/28] VRFS-931 Band tweaks --- web/app/views/api_search/index.rabl | 4 ++++ web/app/views/api_users/band_index.rabl | 6 +++++- web/app/views/clients/_bands.html.erb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index 1a9a9c946..05009f2e3 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -77,6 +77,10 @@ if @search.bands_filter_search? @search.is_follower?(band) end + node :biography do |band| + band.biography.nil? ? "" : band.biography + end + child :genres => :genres do attributes :genre_id, :description end diff --git a/web/app/views/api_users/band_index.rabl b/web/app/views/api_users/band_index.rabl index fb7b3a748..2439ee419 100644 --- a/web/app/views/api_users/band_index.rabl +++ b/web/app/views/api_users/band_index.rabl @@ -26,4 +26,8 @@ node :genres do |band| attributes :id, :description end end -end \ No newline at end of file +end + +node :biography do |band| + band.biography.nil? ? "" : band.biography +end diff --git a/web/app/views/clients/_bands.html.erb b/web/app/views/clients/_bands.html.erb index 2dc713b39..4b6c04455 100644 --- a/web/app/views/clients/_bands.html.erb +++ b/web/app/views/clients/_bands.html.erb @@ -22,7 +22,7 @@ diff --git a/web/app/views/clients/_web_filter.html.erb b/web/app/views/clients/_web_filter.html.erb index cfad92687..a4c6d0ad6 100644 --- a/web/app/views/clients/_web_filter.html.erb +++ b/web/app/views/clients/_web_filter.html.erb @@ -4,36 +4,61 @@ filter_label = :band when Search::PARAM_MUSICIAN filter_label = :musician + when Search::PARAM_FEED + filter_label = :feed end %> <%= content_tag(:div, :id => defined?(id) ? id : 'session-controls', :class => "#{filter_label}-filter filter-head") do %> <%= content_tag(:div, :class => "filter-element wrapper") do -%> - <%= content_tag(:div, 'Filter By:', :class => 'filter-element desc') %> - - <%= select_tag("#{filter_label}_order_by", options_for_select(Search::ORDERINGS), {:class => "#{filter_label}-order-by"} ) %> + <% if :feed == filter_label %> + + <%= content_tag(:div, 'Sort Feed by:', :class => 'filter-element desc') %> + <%= select_tag("#{filter_label}_order_by", options_for_select(Search::F_SORT_OPTS), {:class => "#{filter_label}-order-by"} ) %> + + <% else %> + + <%= content_tag(:div, 'Filter By:', :class => 'filter-element desc') %> + <%= select_tag("#{filter_label}_order_by", options_for_select(Search::ORDERINGS), {:class => "#{filter_label}-order-by"} ) %> + + <% end %> <% end -%> <%= content_tag(:div, :class => 'filter-element wrapper') do -%> <% if :musician == filter_label %> - + <%= content_tag(:div, 'Instrument:', :class => 'filter-element desc') %> <%= select_tag("#{filter_label}_instrument", options_for_select([['Any', '']].concat(JamRuby::Instrument.all.collect { |ii| [ii.description, ii.id] }))) %> + <% elsif :band == filter_label %> - + <%= content_tag(:div, 'Genre:', :class => 'filter-element desc') %> <%= select_tag("#{filter_label}_genre", options_for_select([['Any', '']].concat(JamRuby::Genre.all.collect { |ii| [ii.description, ii.id] }))) %> + + <% elsif :feed == filter_label %> + + <%= content_tag(:div, 'Include Dates:', :class => 'filter-element desc') %> + <%= select_tag("#{filter_label}_date", options_for_select(Search::DATE_OPTS)) %> + <% end %> <% end -%> - <%= content_tag(:div, :class => 'filter-element wrapper') do -%> - <%= content_tag(:div, 'Within', :class => 'filter-element desc') %> - <%= content_tag(:div, :class => 'query-distance-params') do -%> - <% default_distance = :musician == filter_label ? Search::M_MILES_DEFAULT : Search::B_MILES_DEFAULT %> - <%= select_tag("#{filter_label}_query_distance", options_for_select(Search::DISTANCE_OPTS, default_distance)) %> - <% end -%> - <%= content_tag(:div, :class => 'filter-element desc') do -%> - miles of <%= content_tag(:span, current_user.current_city(request.remote_ip), :id => "#{filter_label}-filter-city") %> - <% end -%> + <% if :feed == filter_label %> + + <%= content_tag(:div, 'Show:', :class => 'filter-element desc') %> + <%= select_tag("#{filter_label}_show", options_for_select(Search::SHOW_OPTS)) %> + + <% else %> + + <%= content_tag(:div, 'Within', :class => 'filter-element desc') %> + <%= content_tag(:div, :class => 'query-distance-params') do -%> + <% default_distance = :musician == filter_label ? Search::M_MILES_DEFAULT : Search::B_MILES_DEFAULT %> + <%= select_tag("#{filter_label}_query_distance", options_for_select(Search::DISTANCE_OPTS, default_distance)) %> + <% end -%> + <%= content_tag(:div, :class => 'filter-element desc') do -%> + miles of <%= content_tag(:span, current_user.current_city(request.remote_ip), :id => "#{filter_label}-filter-city") %> + <% end -%> + + <% end %> <% end -%> <% end -%> \ No newline at end of file From 8eb1bbe81d884d21bee107f195588aed517e9748 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 02:58:29 -0600 Subject: [PATCH 23/28] vrfs1008: disabled the autocomplete feature for musician invites --- web/app/views/clients/_inviteMusicians.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/views/clients/_inviteMusicians.html.erb b/web/app/views/clients/_inviteMusicians.html.erb index b80f76f5d..27931c00e 100644 --- a/web/app/views/clients/_inviteMusicians.html.erb +++ b/web/app/views/clients/_inviteMusicians.html.erb @@ -26,7 +26,7 @@
- +
From a32697b2e4fa4ccef4629d36c07f1a37e20ca6ea Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Tue, 14 Jan 2014 03:19:21 -0600 Subject: [PATCH 24/28] vrfs1008: fixed typo --- web/app/assets/javascripts/inviteMusicians.js | 1 - 1 file changed, 1 deletion(-) diff --git a/web/app/assets/javascripts/inviteMusicians.js b/web/app/assets/javascripts/inviteMusicians.js index 95f5f58cb..36a11a465 100644 --- a/web/app/assets/javascripts/inviteMusicians.js +++ b/web/app/assets/javascripts/inviteMusicians.js @@ -33,7 +33,6 @@ }); } $.ajax({ - ajax: false, url: "/api/invitations", data: { session_id: sessionId, sender: context.JK.currentUserId } }).done(function(response) { From fcec0a776bda3f2243e4a9b68c7ddb45c61a1ea5 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 21 Jan 2014 14:51:03 +0000 Subject: [PATCH 25/28] * icecast working locally on my mac (VRFS-1002) --- admin/Gemfile | 1 + admin/app/admin/icecast_bootstrap.rb | 82 ++++++- admin/app/admin/icecast_mount_template.rb | 3 + admin/app/admin/icecast_server_group.rb | 3 + admin/app/helpers/application_helper.rb | 1 + admin/lib/utils.rb | 1 + db/build | 2 +- db/manifest | 4 +- db/up/integrate_icecast_into_sessions.sql | 207 ++++++++---------- db/up/music_sessions_unlogged.sql | 128 +++++++++++ pb/src/client_container.proto | 119 +++++----- ruby/Gemfile | 1 + ruby/lib/jam_ruby.rb | 4 + ruby/lib/jam_ruby/connection_manager.rb | 8 + ruby/lib/jam_ruby/message_factory.rb | 29 ++- ruby/lib/jam_ruby/models/genre.rb | 1 - .../models/icecast_admin_authentication.rb | 3 +- ruby/lib/jam_ruby/models/icecast_directory.rb | 3 +- ruby/lib/jam_ruby/models/icecast_limit.rb | 3 +- .../jam_ruby/models/icecast_listen_socket.rb | 3 +- ruby/lib/jam_ruby/models/icecast_logging.rb | 3 +- .../models/icecast_master_server_relay.rb | 3 +- ruby/lib/jam_ruby/models/icecast_mount.rb | 189 ++++++++++++---- .../jam_ruby/models/icecast_mount_template.rb | 62 ++++++ ruby/lib/jam_ruby/models/icecast_path.rb | 3 +- ruby/lib/jam_ruby/models/icecast_relay.rb | 3 +- ruby/lib/jam_ruby/models/icecast_security.rb | 3 +- ruby/lib/jam_ruby/models/icecast_server.rb | 136 ++++++++---- .../jam_ruby/models/icecast_server_group.rb | 11 + .../jam_ruby/models/icecast_server_socket.rb | 6 + ruby/lib/jam_ruby/models/icecast_template.rb | 4 +- .../models/icecast_template_socket.rb | 6 + .../models/icecast_user_authentication.rb | 3 +- ruby/lib/jam_ruby/models/music_session.rb | 20 ++ ruby/lib/jam_ruby/models/notification.rb | 18 +- ruby/lib/jam_ruby/models/user.rb | 2 + ruby/lib/jam_ruby/mq_router.rb | 19 +- .../jam_ruby/resque/icecast_config_writer.rb | 4 +- .../resque/scheduled/IcecastSourceCheck.rb | 32 +++ .../resque/scheduled/icecast_config_retry.rb | 4 + .../resque/scheduled/icecast_source_check.rb | 59 +++++ ruby/spec/factories.rb | 35 ++- .../icecast_admin_authentication_spec.rb | 12 + .../jam_ruby/models/icecast_directory_spec.rb | 12 + .../jam_ruby/models/icecast_limit_spec.rb | 12 + .../models/icecast_listen_socket_spec.rb | 12 + .../jam_ruby/models/icecast_logging_spec.rb | 12 + .../icecast_master_server_relay_spec.rb | 12 + .../jam_ruby/models/icecast_mount_spec.rb | 162 +++++++++++++- .../models/icecast_mount_template_spec.rb | 37 ++++ .../spec/jam_ruby/models/icecast_path_spec.rb | 12 + .../jam_ruby/models/icecast_relay_spec.rb | 6 + .../jam_ruby/models/icecast_security_spec.rb | 12 + .../jam_ruby/models/icecast_server_spec.rb | 30 +++ .../jam_ruby/models/icecast_template_spec.rb | 12 +- .../jam_ruby/models/music_session_spec.rb | 31 +++ .../resque/icecast_source_check_spec.rb | 110 ++++++++++ ruby/spec/spec_helper.rb | 4 +- ruby/spec/support/utilities.rb | 4 + web/Gemfile | 1 + .../assets/javascripts/AAB_message_factory.js | 6 + web/app/assets/javascripts/fakeJamClient.js | 12 + web/app/assets/javascripts/sidebar.js | 87 +++++++- web/app/controllers/api_icecast_controller.rb | 9 +- web/config/application.rb | 1 + web/config/scheduler.yml | 7 + web/lib/music_session_manager.rb | 13 +- websocket-gateway/Gemfile | 1 + 68 files changed, 1548 insertions(+), 312 deletions(-) create mode 100644 admin/app/admin/icecast_mount_template.rb create mode 100644 admin/app/admin/icecast_server_group.rb create mode 100644 db/up/music_sessions_unlogged.sql create mode 100644 ruby/lib/jam_ruby/models/icecast_mount_template.rb create mode 100644 ruby/lib/jam_ruby/models/icecast_server_group.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb create mode 100644 ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb create mode 100644 ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb diff --git a/admin/Gemfile b/admin/Gemfile index 7c8ba4476..a14387965 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -54,6 +54,7 @@ gem 'gon' gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' +gem 'resque-lonely_job', '~> 1.0.0' gem 'eventmachine', '1.0.3' gem 'amqp', '0.9.8' diff --git a/admin/app/admin/icecast_bootstrap.rb b/admin/app/admin/icecast_bootstrap.rb index 1cc15e22e..3b3f643b6 100644 --- a/admin/app/admin/icecast_bootstrap.rb +++ b/admin/app/admin/icecast_bootstrap.rb @@ -3,16 +3,18 @@ ActiveAdmin.register_page "Bootstrap" do page_action :create_server, :method => :post do - template = IcecastTemplate.find(params[:jam_ruby_icecast_server][:template_id]) + template = IcecastTemplate.find_by_id(params[:jam_ruby_icecast_server][:template_id]) + mount_template = IcecastMountTemplate.find_by_id(params[:jam_ruby_icecast_server][:mount_template_id]) hostname = params[:jam_ruby_icecast_server][:hostname] server = IcecastServer.new server.template = template + server.mount_template = mount_template server.hostname = hostname server.server_id = hostname server.save! - redirect_to admin_bootstrap_path, :notice => "Server created. If you start job worker (bundle exec rake all_jobs), it should update your icecast config." + redirect_to admin_bootstrap_path, :notice => "Server created. If you start a job worker (bundle exec rake all_jobs in /web), it should update your icecast config." end page_action :brew_template, :method => :post do @@ -81,23 +83,85 @@ ActiveAdmin.register_page "Bootstrap" do template.save! end - - redirect_to admin_bootstrap_path, :notice => "Brew template created. Create a server now with that template specified." + redirect_to admin_bootstrap_path, :notice => "Brew template created. Now, create a mount template." end + page_action :create_mount_template, :method => :post do + IcecastServer.transaction do + hostname = params[:jam_ruby_icecast_mount_template][:hostname] + type = params[:jam_ruby_icecast_mount_template][:default_mime_type] + + auth = IcecastUserAuthentication.new + auth.authentication_type = 'url' + auth.mount_add = 'http://' + hostname + '/api/icecast/mount_add' + auth.mount_remove = 'http://' + hostname + '/api/icecast/mount_remove' + auth.listener_add = 'http://' + hostname + '/api/icecast/listener_add' + auth.listener_remove = 'http://' + hostname + '/api/icecast/listener_remove' + auth.auth_header = 'HTTP/1.1 200 OK' + auth.timelimit_header = 'icecast-auth-timelimit:' + auth.save! + + mount_template = IcecastMountTemplate.new + mount_template.name = "#{type}-#{IcecastMountTemplate.count}" + mount_template.source_username = nil # mount will override + mount_template.source_pass = nil # mount will override + mount_template.max_listeners = 20000 # huge + mount_template.max_listener_duration = 3600 * 24 # one day + mount_template.fallback_override = 1 + mount_template.fallback_when_full = 1 + mount_template.is_public = 0 + mount_template.stream_name = nil # mount will override + mount_template.stream_description = nil # mount will override + mount_template.stream_url = nil # mount will override + mount_template.genre = nil # mount will override + mount_template.bitrate = 128 + mount_template.burst_size = 65536 + mount_template.hidden = 1 + mount_template.on_connect = nil + mount_template.on_disconnect = nil + mount_template.authentication = auth + + if type == 'ogg' + mount_template.mp3_metadata_interval = nil + mount_template.mime_type ='audio/ogg' + mount_template.subtype = 'vorbis' + mount_template.fallback_mount = "/fallback-#{mount_template.bitrate}.ogg" + else + mount_template.mp3_metadata_interval = 4096 + mount_template.mime_type ='audio/mpeg' + mount_template.subtype = nil + mount_template.fallback_mount = "/fallback-#{mount_template.bitrate}.mp3" + end + mount_template.save! + end + + redirect_to admin_bootstrap_path, :notice => "Mount template created. Create a server now with your new templates specified." + end + + action_item do - link_to "Create Brew Template", admin_bootstrap_brew_template_path, :method => :post + link_to "Create MacOSX (Brew) Template", admin_bootstrap_brew_template_path, :method => :post end content do if IcecastTemplate.count == 0 - para "You need to create at least one template for your environment" + para "You need to create at least one server template, and one mount template. Click one of the top-left buttons based on your platform" + + elsif IcecastMountTemplate.count == 0 + semantic_form_for IcecastMountTemplate.new, :url => admin_bootstrap_create_mount_template_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "New Mount Template" do + f.input :hostname, :label => "jam-web hostname:port" + f.input :default_mime_type, :as => :select, :collection => ["ogg", "mp3"] + end + f.actions + end else semantic_form_for IcecastServer.new, :url => admin_bootstrap_create_server_path, :builder => ActiveAdmin::FormBuilder do |f| - f.inputs "New Server" do - f.input :hostname - f.input :template + f.inputs "New Icecast Server" do + f.input :hostname, :hint => "Just the icecast hostname; no port" + f.input :template, :hint => "This is the template associated with the server. Not as useful for the 1st server, but subsequent servers can use this same template, and share config" + f.input :mount_template, :hint => "The mount template. When mounts are made as music sessions are created, this template will satisfy templatable values" end f.actions end diff --git a/admin/app/admin/icecast_mount_template.rb b/admin/app/admin/icecast_mount_template.rb new file mode 100644 index 000000000..3a369da11 --- /dev/null +++ b/admin/app/admin/icecast_mount_template.rb @@ -0,0 +1,3 @@ +ActiveAdmin.register JamRuby::IcecastMountTemplate, :as => 'IcecastMountTemplate' do + menu :parent => 'Icecast' +end diff --git a/admin/app/admin/icecast_server_group.rb b/admin/app/admin/icecast_server_group.rb new file mode 100644 index 000000000..2c560996d --- /dev/null +++ b/admin/app/admin/icecast_server_group.rb @@ -0,0 +1,3 @@ +ActiveAdmin.register JamRuby::IcecastServerGroup, :as => 'IcecastServerGroup' do + menu :parent => 'Icecast' +end diff --git a/admin/app/helpers/application_helper.rb b/admin/app/helpers/application_helper.rb index a2f487023..6e9385e59 100644 --- a/admin/app/helpers/application_helper.rb +++ b/admin/app/helpers/application_helper.rb @@ -1,3 +1,4 @@ module ApplicationHelper + end diff --git a/admin/lib/utils.rb b/admin/lib/utils.rb index aa92de6f7..4af46d734 100644 --- a/admin/lib/utils.rb +++ b/admin/lib/utils.rb @@ -3,4 +3,5 @@ module Utils chars = ((('a'..'z').to_a + ('0'..'9').to_a) - %w(i o 0 1 l 0)) (1..size).collect{|a| cc = chars[rand(chars.size)]; 0==rand(2) ? cc.upcase : cc }.join end + end diff --git a/db/build b/db/build index e0ece07a9..259aea93b 100755 --- a/db/build +++ b/db/build @@ -19,7 +19,7 @@ rm -rf $TARGET mkdir -p $PG_BUILD_OUT mkdir -p $PG_RUBY_PACKAGE_OUT -#bundle update +bundle update echo "building migrations" bundle exec pg_migrate build --source . --out $PG_BUILD_OUT --test --verbose diff --git a/db/manifest b/db/manifest index 5a9d3c4c8..8e39f5b24 100755 --- a/db/manifest +++ b/db/manifest @@ -88,4 +88,6 @@ icecast.sql home_page_promos.sql mix_job_watch.sql music_session_constraints.sql -mixes_drop_manifest_add_retry.sql \ No newline at end of file +mixes_drop_manifest_add_retry.sql +music_sessions_unlogged.sql +integrate_icecast_into_sessions.sql \ No newline at end of file diff --git a/db/up/integrate_icecast_into_sessions.sql b/db/up/integrate_icecast_into_sessions.sql index d92fa96c5..6c29b3562 100644 --- a/db/up/integrate_icecast_into_sessions.sql +++ b/db/up/integrate_icecast_into_sessions.sql @@ -1,129 +1,96 @@ --- this manifest update makes every table associated with music_sessions UNLOGGED --- tables to mark UNLOGGED --- connections, fan_invitations, invitations, genres_music_sessions, join_requests, tracks, music_sessions - --- tables to just get rid of --- session_plays - --- breaking foreign keys for tables --- connections: user_id --- fan_invitations: receiver_id, sender_id --- music_session: user_id, band_id, claimed_recording_id, claimed_recording_initiator_id --- genres_music_sessions: genre_id --- invitations: sender_id, receiver_id --- fan_invitations: user_id --- notifications: invitation_id, join_request_id, session_id - -DROP TABLE sessions_plays; - --- divorce notifications from UNLOGGED tables - --- NOTIFICATIONS ----------------- --- "notifications_session_id_fkey" FOREIGN KEY (session_id) REFERENCES music_sessions(id) ON DELETE CASCADE -ALTER TABLE notifications DROP CONSTRAINT notifications_session_id_fkey; --- "notifications_join_request_id_fkey" FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE -ALTER TABLE notifications DROP CONSTRAINT notifications_join_request_id_fkey; --- "notifications_invitation_id_fkey" FOREIGN KEY (invitation_id) REFERENCES invitations(id) ON DELETE CASCADE -ALTER TABLE notifications DROP CONSTRAINT notifications_invitation_id_fkey; - --- FAN_INVITATIONS ------------------- -DROP TABLE fan_invitations; -DROP TABLE invitations; -DROP TABLE join_requests; -DROP TABLE genres_music_sessions; -DROP TABLE tracks; -DROP TABLE connections; -DROP TABLE music_sessions; - --- MUSIC_SESSIONS ------------------ -CREATE UNLOGGED TABLE music_sessions ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - description VARCHAR(8000), - user_id VARCHAR(64) NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - musician_access BOOLEAN NOT NULL, - band_id VARCHAR(64), - approval_required BOOLEAN NOT NULL, - fan_access BOOLEAN NOT NULL, - fan_chat BOOLEAN NOT NULL, - claimed_recording_id VARCHAR(64), - claimed_recording_initiator_id VARCHAR(64) +CREATE TABLE icecast_mount_templates( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + name VARCHAR(256) NOT NULL, + source_username VARCHAR(64), + source_pass VARCHAR(64), + max_listeners INTEGER DEFAULT 4, + max_listener_duration INTEGER DEFAULT 3600, + dump_file VARCHAR(1024), + intro VARCHAR(1024), + fallback_mount VARCHAR(1024), + fallback_override INTEGER DEFAULT 1, + fallback_when_full INTEGER DEFAULT 1, + charset VARCHAR(1024) DEFAULT 'ISO8859-1', + is_public INTEGER DEFAULT 0, + stream_name VARCHAR(1024), + stream_description VARCHAR(10000), + stream_url VARCHAR(1024), + genre VARCHAR(256), + bitrate INTEGER, + mime_type VARCHAR(64) NOT NULL DEFAULT 'audio/mpeg', + subtype VARCHAR(64), + burst_size INTEGER, + mp3_metadata_interval INTEGER, + hidden INTEGER DEFAULT 1, + on_connect VARCHAR(1024), + on_disconnect VARCHAR(1024), + authentication_id varchar(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); --- CONNECTIONS --------------- -CREATE UNLOGGED TABLE connections ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - user_id VARCHAR(64), - client_id VARCHAR(64) UNIQUE NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - music_session_id VARCHAR(64), - ip_address VARCHAR(64), - as_musician BOOLEAN, - aasm_state VARCHAR(64) DEFAULT 'idle'::VARCHAR NOT NULL +ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP NOT NULL; +ALTER TABLE icecast_mounts ALTER COLUMN mime_type DROP DEFAULT; +ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP NOT NULL; +ALTER TABLE icecast_mounts ALTER COLUMN subtype DROP DEFAULT; +ALTER TABLE icecast_mounts ADD COLUMN music_session_id VARCHAR(64) REFERENCES music_sessions(id) ON DELETE CASCADE; +ALTER TABLE icecast_mounts ADD COLUMN icecast_server_id VARCHAR(64) NOT NULL REFERENCES icecast_servers(id); +ALTER TABLE icecast_mounts ADD COLUMN icecast_mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id); +ALTER TABLE icecast_mounts ADD COLUMN sourced_needs_changing_at TIMESTAMP; +; +CREATE TABLE icecast_server_groups ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + name VARCHAR(255) UNIQUE NOT NULL ); -ALTER TABLE ONLY connections ADD CONSTRAINT connections_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE SET NULL; +-- bootstrap the default icecast group +INSERT INTO icecast_server_groups (id, name) VALUES ('default', 'default'); +INSERT INTO icecast_server_groups (id, name) VALUES ('unused', 'unused'); --- GENRES_MUSIC_SESSIONS ------------------------- -CREATE UNLOGGED TABLE genres_music_sessions ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - genre_id VARCHAR(64), - music_session_id VARCHAR(64) -); -ALTER TABLE ONLY genres_music_sessions ADD CONSTRAINT genres_music_sessions_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; +ALTER TABLE users ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default'; -CREATE UNLOGGED TABLE fan_invitations ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - sender_id VARCHAR(64), - receiver_id VARCHAR(64), - music_session_id VARCHAR(64), - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL -); -ALTER TABLE ONLY fan_invitations ADD CONSTRAINT fan_invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; +-- and by default, all servers and users are in this group +ALTER TABLE icecast_servers ADD COLUMN icecast_server_group_id VARCHAR(64) NOT NULL REFERENCES icecast_server_groups(id) DEFAULT 'default'; +ALTER TABLE icecast_servers ADD COLUMN mount_template_id VARCHAR(64) REFERENCES icecast_mount_templates(id); -CREATE UNLOGGED TABLE join_requests ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - user_id VARCHAR(64), - music_session_id VARCHAR(64), - text VARCHAR(2000), - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL -); -ALTER TABLE ONLY join_requests ADD CONSTRAINT user_music_session_uniqkey UNIQUE (user_id, music_session_id); -ALTER TABLE ONLY join_requests ADD CONSTRAINT join_requests_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; --- INVITATIONS --------------- -CREATE UNLOGGED TABLE invitations ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - sender_id VARCHAR(64), - receiver_id VARCHAR(64), - music_session_id VARCHAR(64), - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - join_request_id VARCHAR(64) -); -ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_uniqkey UNIQUE (sender_id, receiver_id, music_session_id); -ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_join_request_id_fkey FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE; -ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_admin_auth_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_admin_auth_id_fkey" FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_mount_template_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_mount_template_id_fkey" FOREIGN KEY (mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_directory_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_icecast_server_group_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_icecast_server_group_id_fkey" FOREIGN KEY (icecast_server_group_id) REFERENCES icecast_server_groups(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_limit_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_limit_id_fkey" FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_logging_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_logging_id_fkey" FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_master_relay_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_master_relay_id_fkey" FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_path_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_path_id_fkey" FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_security_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_security_id_fkey" FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL; +ALTER TABLE icecast_servers DROP CONSTRAINT "icecast_servers_template_id_fkey"; +ALTER TABLE icecast_servers ADD CONSTRAINT "icecast_servers_template_id_fkey" FOREIGN KEY (template_id) REFERENCES icecast_templates(id) ON DELETE SET NULL; --- TRACKS ---------- -CREATE UNLOGGED TABLE tracks ( - id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - connection_id VARCHAR(64), - instrument_id VARCHAR(64), - sound VARCHAR(64) NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - client_track_id VARCHAR(64) NOT NULL -); -ALTER TABLE ONLY tracks ADD CONSTRAINT connections_tracks_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE; +ALTER TABLE icecast_mounts DROP CONSTRAINT "icecast_mounts_icecast_mount_template_id_fkey"; +ALTER TABLE icecast_mounts ADD CONSTRAINT "icecast_mounts_icecast_mount_template_id_fkey" FOREIGN KEY (icecast_mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL; +ALTER TABLE icecast_mounts DROP CONSTRAINT "icecast_mounts_icecast_server_id_fkey"; +ALTER TABLE icecast_mounts ADD CONSTRAINT "icecast_mounts_icecast_server_id_fkey" FOREIGN KEY (icecast_server_id) REFERENCES icecast_servers(id) ON DELETE SET NULL; + +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_admin_auth_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_admin_auth_id_fkey" FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_directory_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_directory_id_fkey" FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_limit_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_limit_id_fkey" FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_logging_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_logging_id_fkey" FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_master_relay_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_master_relay_id_fkey" FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_path_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_path_id_fkey" FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL; +ALTER TABLE icecast_templates DROP CONSTRAINT "icecast_templates_security_id_fkey"; +ALTER TABLE icecast_templates ADD CONSTRAINT "icecast_templates_security_id_fkey" FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL; diff --git a/db/up/music_sessions_unlogged.sql b/db/up/music_sessions_unlogged.sql new file mode 100644 index 000000000..fad3bc8f5 --- /dev/null +++ b/db/up/music_sessions_unlogged.sql @@ -0,0 +1,128 @@ +-- this manifest update makes every table associated with music_sessions UNLOGGED + +-- tables to mark UNLOGGED +-- connections, fan_invitations, invitations, genres_music_sessions, join_requests, tracks, music_sessions + +-- breaking foreign keys for tables +-- connections: user_id +-- fan_invitations: receiver_id, sender_id +-- music_session: user_id, band_id, claimed_recording_id, claimed_recording_initiator_id +-- genres_music_sessions: genre_id +-- invitations: sender_id, receiver_id +-- fan_invitations: user_id +-- notifications: invitation_id, join_request_id, session_id + + +-- divorce notifications from UNLOGGED tables + +DROP TABLE sessions_plays; + +-- NOTIFICATIONS +---------------- +-- "notifications_session_id_fkey" FOREIGN KEY (session_id) REFERENCES music_sessions(id) ON DELETE CASCADE +ALTER TABLE notifications DROP CONSTRAINT notifications_session_id_fkey; +-- "notifications_join_request_id_fkey" FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE +ALTER TABLE notifications DROP CONSTRAINT notifications_join_request_id_fkey; +-- "notifications_invitation_id_fkey" FOREIGN KEY (invitation_id) REFERENCES invitations(id) ON DELETE CASCADE +ALTER TABLE notifications DROP CONSTRAINT notifications_invitation_id_fkey; + +-- FAN_INVITATIONS +------------------ +DROP TABLE fan_invitations; +DROP TABLE invitations; +DROP TABLE join_requests; +DROP TABLE genres_music_sessions; +DROP TABLE tracks; +DROP TABLE connections; +DROP TABLE music_sessions; + +-- MUSIC_SESSIONS +----------------- +CREATE UNLOGGED TABLE music_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + description VARCHAR(8000), + user_id VARCHAR(64) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + musician_access BOOLEAN NOT NULL, + band_id VARCHAR(64), + approval_required BOOLEAN NOT NULL, + fan_access BOOLEAN NOT NULL, + fan_chat BOOLEAN NOT NULL, + claimed_recording_id VARCHAR(64), + claimed_recording_initiator_id VARCHAR(64) +); + +-- CONNECTIONS +-------------- +CREATE UNLOGGED TABLE connections ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64), + client_id VARCHAR(64) UNIQUE NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + music_session_id VARCHAR(64), + ip_address VARCHAR(64), + as_musician BOOLEAN, + aasm_state VARCHAR(64) DEFAULT 'idle'::VARCHAR NOT NULL +); +ALTER TABLE ONLY connections ADD CONSTRAINT connections_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE SET NULL; + +-- GENRES_MUSIC_SESSIONS +------------------------ +CREATE UNLOGGED TABLE genres_music_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + genre_id VARCHAR(64), + music_session_id VARCHAR(64) +); +ALTER TABLE ONLY genres_music_sessions ADD CONSTRAINT genres_music_sessions_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +CREATE UNLOGGED TABLE fan_invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + sender_id VARCHAR(64), + receiver_id VARCHAR(64), + music_session_id VARCHAR(64), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); +ALTER TABLE ONLY fan_invitations ADD CONSTRAINT fan_invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +CREATE UNLOGGED TABLE join_requests ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64), + music_session_id VARCHAR(64), + text VARCHAR(2000), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); +ALTER TABLE ONLY join_requests ADD CONSTRAINT user_music_session_uniqkey UNIQUE (user_id, music_session_id); +ALTER TABLE ONLY join_requests ADD CONSTRAINT join_requests_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +-- INVITATIONS +-------------- +CREATE UNLOGGED TABLE invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + sender_id VARCHAR(64), + receiver_id VARCHAR(64), + music_session_id VARCHAR(64), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + join_request_id VARCHAR(64) +); +ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_uniqkey UNIQUE (sender_id, receiver_id, music_session_id); +ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_join_request_id_fkey FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE; +ALTER TABLE ONLY invitations ADD CONSTRAINT invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + +-- TRACKS +--------- +CREATE UNLOGGED TABLE tracks ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + connection_id VARCHAR(64), + instrument_id VARCHAR(64), + sound VARCHAR(64) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + client_track_id VARCHAR(64) NOT NULL +); +ALTER TABLE ONLY tracks ADD CONSTRAINT connections_tracks_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE; + diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index ca59728b2..c6669a824 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -9,67 +9,69 @@ package jampb; message ClientMessage { enum Type { - LOGIN = 100; - LOGIN_ACK = 105; - LOGIN_MUSIC_SESSION = 110; - LOGIN_MUSIC_SESSION_ACK = 115; - LEAVE_MUSIC_SESSION = 120; - LEAVE_MUSIC_SESSION_ACK = 125; - HEARTBEAT = 130; - HEARTBEAT_ACK = 135; + LOGIN = 100; + LOGIN_ACK = 105; + LOGIN_MUSIC_SESSION = 110; + LOGIN_MUSIC_SESSION_ACK = 115; + LEAVE_MUSIC_SESSION = 120; + LEAVE_MUSIC_SESSION_ACK = 125; + HEARTBEAT = 130; + HEARTBEAT_ACK = 135; // friend notifications - FRIEND_UPDATE = 140; - FRIEND_REQUEST = 145; - FRIEND_REQUEST_ACCEPTED = 150; - FRIEND_SESSION_JOIN = 155; - NEW_USER_FOLLOWER = 160; - NEW_BAND_FOLLOWER = 161; + FRIEND_UPDATE = 140; + FRIEND_REQUEST = 145; + FRIEND_REQUEST_ACCEPTED = 150; + FRIEND_SESSION_JOIN = 155; + NEW_USER_FOLLOWER = 160; + NEW_BAND_FOLLOWER = 161; // session invitations - SESSION_INVITATION = 165; - SESSION_ENDED = 170; - JOIN_REQUEST = 175; - JOIN_REQUEST_APPROVED = 180; - JOIN_REQUEST_REJECTED = 185; - SESSION_JOIN = 190; - SESSION_DEPART = 195; - MUSICIAN_SESSION_JOIN = 196; + SESSION_INVITATION = 165; + SESSION_ENDED = 170; + JOIN_REQUEST = 175; + JOIN_REQUEST_APPROVED = 180; + JOIN_REQUEST_REJECTED = 185; + SESSION_JOIN = 190; + SESSION_DEPART = 195; + MUSICIAN_SESSION_JOIN = 196; // recording notifications - MUSICIAN_RECORDING_SAVED = 200; - BAND_RECORDING_SAVED = 205; - RECORDING_STARTED = 210; - RECORDING_ENDED = 215; - RECORDING_MASTER_MIX_COMPLETE = 220; - DOWNLOAD_AVAILABLE = 221; + MUSICIAN_RECORDING_SAVED = 200; + BAND_RECORDING_SAVED = 205; + RECORDING_STARTED = 210; + RECORDING_ENDED = 215; + RECORDING_MASTER_MIX_COMPLETE = 220; + DOWNLOAD_AVAILABLE = 221; // band notifications - BAND_INVITATION = 225; - BAND_INVITATION_ACCEPTED = 230; - BAND_SESSION_JOIN = 235; + BAND_INVITATION = 225; + BAND_INVITATION_ACCEPTED = 230; + BAND_SESSION_JOIN = 235; - MUSICIAN_SESSION_FRESH = 240; - MUSICIAN_SESSION_STALE = 245; + MUSICIAN_SESSION_FRESH = 240; + MUSICIAN_SESSION_STALE = 245; // icecast notifications - SOURCE_UP_REQUESTED = 250; - SOURCE_DOWN_REQUESTED = 255; + SOURCE_UP_REQUESTED = 250; + SOURCE_DOWN_REQUESTED = 251; + SOURCE_UP = 252; + SOURCE_DOWN = 253; - TEST_SESSION_MESSAGE = 295; + TEST_SESSION_MESSAGE = 295; - PING_REQUEST = 300; - PING_ACK = 305; - PEER_MESSAGE = 310; - TEST_CLIENT_MESSAGE = 315; + PING_REQUEST = 300; + PING_ACK = 305; + PEER_MESSAGE = 310; + TEST_CLIENT_MESSAGE = 315; - SERVER_BAD_STATE_RECOVERED = 900; + SERVER_BAD_STATE_RECOVERED = 900; - SERVER_GENERIC_ERROR = 1000; - SERVER_REJECTION_ERROR = 1005; - SERVER_PERMISSION_ERROR = 1010; - SERVER_BAD_STATE_ERROR = 1015; + SERVER_GENERIC_ERROR = 1000; + SERVER_REJECTION_ERROR = 1005; + SERVER_PERMISSION_ERROR = 1010; + SERVER_BAD_STATE_ERROR = 1015; } // Identifies which inner message is filled in @@ -126,7 +128,9 @@ message ClientMessage { // icecast notifications optional SourceUpRequested source_up_requested = 250; - optional SourceDownRequested source_down_requested = 255; + optional SourceDownRequested source_down_requested = 251; + optional SourceUp source_up = 252; + optional SourceDown source_down = 253; // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -381,15 +385,26 @@ message MusicianSessionStale { } message SourceUpRequested { - optional string host = 1; // icecast server host - optional int32 port = 2; // icecast server port - optional string mount = 3; // mount name - optional string source_user = 4; // source user - optional string source_pass = 5; // source pass + optional string music_session = 1; // music session id + optional string host = 2; // icecast server host + optional int32 port = 3; // icecast server port + optional string mount = 4; // mount name + optional string source_user = 5; // source user + optional string source_pass = 6; // source pass + optional int32 bitrate = 7; } message SourceDownRequested { - optional string mount = 1; // mount name + optional string music_session = 1; // music session id + optional string mount = 2; // mount name +} + +message SourceUp { + optional string music_session = 1; // music session id +} + +message SourceDown { + optional string music_session = 1; // music session id } // route_to: session diff --git a/ruby/Gemfile b/ruby/Gemfile index ca3ab001a..f8a1643ac 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -29,6 +29,7 @@ gem 'postgres_ext' gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' #, :path => "/Users/seth/workspace/resque_failed_job_mailer" +gem 'resque-lonely_job', '~> 1.0.0' gem 'oj' gem 'builder' diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 9f5eb11eb..ce77bf09d 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -33,6 +33,7 @@ require "jam_ruby/resque/audiomixer" require "jam_ruby/resque/icecast_config_writer" require "jam_ruby/resque/scheduled/audiomixer_retry" require "jam_ruby/resque/scheduled/icecast_config_retry" +require "jam_ruby/resque/scheduled/icecast_source_check" require "jam_ruby/mq_router" require "jam_ruby/base_manager" require "jam_ruby/connection_manager" @@ -113,6 +114,9 @@ require "jam_ruby/models/icecast_server_mount" require "jam_ruby/models/icecast_server_relay" require "jam_ruby/models/icecast_server_socket" require "jam_ruby/models/icecast_template_socket" +require "jam_ruby/models/icecast_server_group" +require "jam_ruby/models/icecast_mount_template" + include Jampb diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index a16ee93b8..5a13351c5 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -37,6 +37,12 @@ module JamRuby return friend_ids end + # this simulates music_session destroy callbacks with activerecord + def before_destroy_music_session(music_session_id) + music_session = MusicSession.find_by_id(music_session_id) + music_session.before_destroy if music_session + end + # reclaim the existing connection, def reconnect(conn, reconnect_music_session_id) music_session_id = nil @@ -218,6 +224,7 @@ SQL # same for session-if we are down to the last participant, delete the session unless music_session_id.nil? + before_destroy_music_session(music_session_id) result = conn.exec("DELETE FROM music_sessions WHERE id = $1 AND 0 = (select count(music_session_id) FROM connections where music_session_id = $1)", [music_session_id]) if result.cmd_tuples == 1 music_session_id = nil @@ -258,6 +265,7 @@ SQL end if num_participants == 0 # delete the music_session + before_destroy_music_session(previous_music_session_id) conn.exec("DELETE from music_sessions WHERE id = $1", [previous_music_session_id]) do |result| if result.cmd_tuples == 1 diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index f23eb7087..2c91c16da 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -558,13 +558,15 @@ module JamRuby # create a source up requested message to send to clients in a session, # so that one of the clients will start sending source audio to icecast - def source_up_requested (session_id, host, port, mount, source_user, source_pass) + def source_up_requested (session_id, host, port, mount, source_user, source_pass, bitrate) source_up_requested = Jampb::SourceUpRequested.new( + music_session: session_id, host: host, port: port, mount: mount, source_user: source_user, - source_pass: source_pass) + source_pass: source_pass, + bitrate: bitrate) Jampb::ClientMessage.new( type: ClientMessage::Type::SOURCE_UP_REQUESTED, @@ -575,7 +577,7 @@ module JamRuby # create a source up requested message to send to clients in a session, # so that one of the clients will start sending source audio to icecast def source_down_requested (session_id, mount) - source_down_requested = Jampb::SourceDownRequested.new(mount: mount) + source_down_requested = Jampb::SourceDownRequested.new(music_session: session_id, mount: mount) Jampb::ClientMessage.new( type: ClientMessage::Type::SOURCE_DOWN_REQUESTED, @@ -583,6 +585,27 @@ module JamRuby source_down_requested: source_down_requested) end + # let's someone know that the source came online. the stream activate shortly + # it might be necessary to refresh the client + def source_up (session_id) + source_up = Jampb::SourceUp.new(music_session: session_id) + + Jampb::ClientMessage.new( + type: ClientMessage::Type::SOURCE_UP, + route_to: SESSION_TARGET_PREFIX + session_id, + source_up: source_up) + end + + # let's someone know that the source went down. the stream will go offline + def source_down (session_id) + source_down = Jampb::SourceDown.new(music_session: session_id) + + Jampb::ClientMessage.new( + type: ClientMessage::Type::SOURCE_DOWN, + route_to: SESSION_TARGET_PREFIX + session_id, + source_down: source_down) + end + # create a test message to send in session def test_session_message(session_id, msg) test = Jampb::TestSessionMessage.new(:msg => msg) diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index 80fb3c1f8..45776c59f 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -11,6 +11,5 @@ module JamRuby # music sessions has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions" - end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb b/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb index 4880495b5..55fb29dda 100644 --- a/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb +++ b/ruby/lib/jam_ruby/models/icecast_admin_authentication.rb @@ -12,7 +12,8 @@ module JamRuby validates :relay_pass, presence: true, length: {minimum: 5} validates :admin_user, presence: true, length: {minimum: 5} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_directory.rb b/ruby/lib/jam_ruby/models/icecast_directory.rb index 29ddd2b6a..0f425c55f 100644 --- a/ruby/lib/jam_ruby/models/icecast_directory.rb +++ b/ruby/lib/jam_ruby/models/icecast_directory.rb @@ -9,7 +9,8 @@ module JamRuby validates :yp_url_timeout, presence: true, numericality: {only_integer: true}, length: {in: 1..30} validates :yp_url, presence: true - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_limit.rb b/ruby/lib/jam_ruby/models/icecast_limit.rb index 76a95ee3e..b47475e7c 100644 --- a/ruby/lib/jam_ruby/models/icecast_limit.rb +++ b/ruby/lib/jam_ruby/models/icecast_limit.rb @@ -15,7 +15,8 @@ module JamRuby validates :source_timeout, presence: true, numericality: {only_integer: true} validates :burst_size, presence: true, numericality: {only_integer: true} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_listen_socket.rb b/ruby/lib/jam_ruby/models/icecast_listen_socket.rb index 569740eb4..90ce3a2c1 100644 --- a/ruby/lib/jam_ruby/models/icecast_listen_socket.rb +++ b/ruby/lib/jam_ruby/models/icecast_listen_socket.rb @@ -12,7 +12,8 @@ module JamRuby validates :port, presence: true, numericality: {only_integer: true}, length: {in: 1..65535} validates :shoutcast_compat, :inclusion => {:in => [nil, 0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_logging.rb b/ruby/lib/jam_ruby/models/icecast_logging.rb index 0d41bd0e1..b9c579084 100644 --- a/ruby/lib/jam_ruby/models/icecast_logging.rb +++ b/ruby/lib/jam_ruby/models/icecast_logging.rb @@ -12,7 +12,8 @@ module JamRuby validates :log_archive, :inclusion => {:in => [nil, 0, 1]} validates :log_size, numericality: {only_integer: true}, if: lambda {|m| m.log_size.present?} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb b/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb index 0c3d04733..6215c5a03 100644 --- a/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb +++ b/ruby/lib/jam_ruby/models/icecast_master_server_relay.rb @@ -14,7 +14,8 @@ module JamRuby validates :master_pass, presence: true, length: {minimum: 5} validates :relays_on_demand, :inclusion => {:in => [0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_mount.rb b/ruby/lib/jam_ruby/models/icecast_mount.rb index 2b19cee51..1014561fd 100644 --- a/ruby/lib/jam_ruby/models/icecast_mount.rb +++ b/ruby/lib/jam_ruby/models/icecast_mount.rb @@ -1,17 +1,22 @@ module JamRuby class IcecastMount < ActiveRecord::Base + @@log = Logging.logger[IcecastMount] + attr_accessible :authentication_id, :name, :source_username, :source_pass, :max_listeners, :max_listener_duration, :dump_file, :intro, :fallback_mount, :fallback_override, :fallback_when_full, :charset, :is_public, :stream_name, :stream_description, :stream_url, :genre, :bitrate, :mime_type, :subtype, :burst_size, - :mp3_metadata_interval, :hidden, :on_connect, :on_disconnect, as: :admin + :mp3_metadata_interval, :hidden, :on_connect, :on_disconnect, + :music_session_id, :icecast_server_id, :icecast_mount_template_id, :listeners, :sourced, + :sourced_needs_changing_at, as: :admin belongs_to :authentication, class_name: "JamRuby::IcecastUserAuthentication", inverse_of: :mount, :foreign_key => 'authentication_id' + belongs_to :music_session, class_name: "JamRuby::MusicSession", inverse_of: :mount, foreign_key: 'music_session_id' - has_many :server_mounts, :class_name => "JamRuby::IcecastServerMount", :inverse_of => :mount, :foreign_key => 'icecast_mount_id' - has_many :servers, :class_name => "JamRuby::IcecastServer", :through => :server_mounts, :source => :server + belongs_to :server, class_name: "JamRuby::IcecastServer", inverse_of: :mounts, foreign_key: 'icecast_server_id' + belongs_to :mount_template, class_name: "JamRuby::IcecastMountTemplate", inverse_of: :mounts, foreign_key: 'icecast_mount_template_id' - validates :name, presence: true + validates :name, presence: true, uniqueness: true validates :source_username, length: {minimum: 5}, if: lambda {|m| m.source_username.present?} validates :source_pass, length: {minimum: 5}, if: lambda {|m| m.source_pass.present?} validates :max_listeners, length: {in: 1..15000}, if: lambda {|m| m.max_listeners.present?} @@ -19,93 +24,167 @@ module JamRuby validates :fallback_override, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} validates :fallback_when_full, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} validates :is_public, presence: true, :inclusion => {:in => [-1, 0, 1]} - validates :stream_name, presence: true - validates :stream_description, presence: true - validates :stream_url, presence: true - validates :genre, presence: true validates :bitrate, numericality: {only_integer: true}, if: lambda {|m| m.bitrate.present?} - validates :mime_type, presence: true - validates :subtype, presence: true validates :burst_size, numericality: {only_integer: true}, if: lambda {|m| m.burst_size.present?} validates :mp3_metadata_interval, numericality: {only_integer: true}, if: lambda {|m| m.mp3_metadata_interval.present?} validates :hidden, :inclusion => {:in => [0, 1]} - validate :name_has_correct_format + validates :server, presence: true + validate :name_has_correct_format - before_save :sanitize_active_admin - after_save :after_save - after_commit :after_commit + before_save :sanitize_active_admin + after_save :after_save + after_save :poke_config + before_destroy :poke_config def name_has_correct_format errors.add(:name, "must start with /") unless name && name.start_with?('/') end - def after_save - IcecastServer.update(servers, config_changed: 1) + def poke_config + server.update_attribute(:config_changed, 1) if server + end + + def after_save + server.update_attribute(:config_changed, 1) - # transiting to sourced from not sourced if !sourced_was && sourced + # went from NOT SOURCED to SOURCED + notify_source_up + + elsif sourced_was && !sourced + + # went from SOURCED to NOT SOURCED + notify_source_down + end + + if listeners_was == 0 && listeners > 0 && !sourced + # listener count went above 0 and there is no source. ask the musician clients to source + notify_source_up_requested + end + + # Note: + # Notification.send_source_down_requested does not occur here. + # we set up a cron that checks for streams that have not been successfully source up/down (after timeout ) in IcecastSourceCheck end def sanitize_active_admin self.authentication_id = nil if self.authentication_id == '' + self.music_session_id = nil if self.music_session_id == '' + self.icecast_server_id = nil if self.icecast_server_id == '' + end + + # creates a templated + def self.build_session_mount(music_session) + + # only public sessions get mounts currently + return nil unless music_session.fan_access + + icecast_server = IcecastServer.find_best_server_for_user(music_session.creator) + + mount = nil + if icecast_server && icecast_server.mount_template_id + # we have a server with an associated mount_template; we can create a mount automatically + mount = icecast_server.mount_template.build_session_mount(music_session) + mount.server = icecast_server + end + mount end def source_up with_lock do self.sourced = true - self.save(:validate => false) + self.sourced_needs_changing_at = nil + save(validate: false) end end def source_down with_lock do - sourced = false - save(:validate => false) + self.sourced = false + self.sourced_needs_changing_at = nil + save(validate: false) end end def listener_add with_lock do - increment!(:listeners) + sourced_needs_changing_at = Time.now if listeners == 0 + + # this is completely unsafe without that 'with_lock' statement above + self.listeners = self.listeners + 1 + + save(validate: false) end end - def listener_remove + if listeners == 0 + @@log.warn("listeners is at 0, but we are being asked to remove a listener. maybe we missed a listener_add request earlier") + return + end + with_lock do - decrement!(:listeners) + sourced_needs_changing_at = Time.now if listeners == 1 + + # this is completely unsafe without that 'with_lock' statement above + self.listeners = self.listeners - 1 + + save(validations: false) end end + def notify_source_up_requested + Notification.send_source_up_requested(music_session, + server.hostname, + server.pick_listen_socket(:port), + name, + resolve_string(:source_username), + resolve_string(:source_pass), + resolve_int(:bitrate)) if music_session_id + end + + def notify_source_down_requested + Notification.send_source_down_requested(music_session, name) + end + + def notify_source_up + Notification.send_source_up(music_session) if music_session_id + end + + def notify_source_down + Notification.send_source_down(music_session) if music_session_id + end + + # Check if the icecast_mount specifies the value; if not, use the mount_template's value take effect def dumpXml(builder) builder.tag! 'mount' do |mount| mount.tag! 'mount-name', name - mount.tag! 'username', source_username if !source_username.nil? && !source_username.empty? - mount.tag! 'password', source_pass if !source_pass.nil? && !source_pass.empty? - mount.tag! 'max-listeners', max_listeners unless max_listeners.nil? - mount.tag! 'max-listener-duration', max_listener_duration unless max_listener_duration.nil? - mount.tag! 'dump-file', dump_file if !dump_file.nil? && !dump_file.empty? - mount.tag! 'intro', intro if !intro.nil? && !intro.empty? - mount.tag! 'fallback-mount', fallback_mount if !fallback_mount.nil? && !fallback_mount.empty? - mount.tag! 'fallback-override', fallback_override if fallback_override - mount.tag! 'fallback-when-full', fallback_when_full if fallback_when_full - mount.tag! 'charset', charset if charset - mount.tag! 'public', is_public - mount.tag! 'stream-name', stream_name if !stream_name.nil? && !stream_name.empty? - mount.tag! 'stream-description', stream_description if !stream_description.nil? && !stream_description.empty? - mount.tag! 'stream-url', stream_url if !stream_url.nil? && !stream_url.empty? - mount.tag! 'genre', genre unless genre.empty? - mount.tag! 'bitrate', bitrate if bitrate - mount.tag! 'type', mime_type - mount.tag! 'subtype', subtype - mount.tag! 'burst-size', burst_size if burst_size - mount.tag! 'mp3-metadata-interval', mp3_metadata_interval unless mp3_metadata_interval.nil? - mount.tag! 'hidden', hidden - mount.tag! 'on-connect', on_connect if on_connect - mount.tag! 'on-disconnect', on_disconnect if on_disconnect + mount.tag! 'username', resolve_string(:source_username) if string_present?(:source_username) + mount.tag! 'password', resolve_string(:source_pass) if string_present?(:source_pass) + mount.tag! 'max-listeners', resolve_int(:max_listeners) if int_present?(:max_listeners) + mount.tag! 'max-listener-duration', resolve_string(:max_listener_duration) if int_present?(:max_listener_duration) + mount.tag! 'dump-file', resolve_string(:dump_file) if string_present?(:dump_file) + mount.tag! 'intro', resolve_string(:intro) if string_present?(:intro) + mount.tag! 'fallback-mount', resolve_string(:fallback_mount) if string_present?(:fallback_mount) + mount.tag! 'fallback-override', resolve_int(:fallback_override) if int_present?(:fallback_override) + mount.tag! 'fallback-when-full', resolve_int(:fallback_when_full) if int_present?(:fallback_when_full) + mount.tag! 'charset', resolve_string(:charset) if string_present?(:charset) + mount.tag! 'public', resolve_int(:is_public) if int_present?(:is_public) + mount.tag! 'stream-name', resolve_string(:stream_name) if string_present?(:stream_name) + mount.tag! 'stream-description', resolve_string(:stream_description) if string_present?(:stream_description) + mount.tag! 'stream-url', resolve_string(:stream_url) if string_present?(:stream_url) + mount.tag! 'genre', resolve_string(:genre) if string_present?(:genre) + mount.tag! 'bitrate', resolve_int(:bitrate) if int_present?(:bitrate) + mount.tag! 'type', resolve_string(:mime_type) if string_present?(:mime_type) + mount.tag! 'subtype', resolve_string(:subtype) if string_present?(:subtype) + mount.tag! 'burst-size', resolve_int(:burst_size) if int_present?(:burst_size) + mount.tag! 'mp3-metadata-interval', resolve_int(:mp3_metadata_interval) if int_present?(:mp3_metadata_interval) + mount.tag! 'hidden', resolve_int(:hidden) if int_present?(:hidden) + mount.tag! 'on-connect', resolve_string(:on_connect) if string_present?(:on_connect) + mount.tag! 'on-disconnect', resolve_string(:on_disconnect) if string_present?(:on_disconnect) authentication.dumpXml(builder) if authentication end @@ -117,5 +196,23 @@ module JamRuby "http://" + server_mount.server.hostname + self.name end + + + def resolve_string(field) + self[field].present? ? self[field] : mount_template && mount_template[field] + end + + def string_present?(field) + val = resolve_string(field) + val ? val.present? : false + end + + def resolve_int(field) + !self[field].nil? ? self[field]: mount_template && mount_template[field] + end + + def int_present?(field) + resolve_int(field) + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_mount_template.rb b/ruby/lib/jam_ruby/models/icecast_mount_template.rb new file mode 100644 index 000000000..f6b844969 --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_mount_template.rb @@ -0,0 +1,62 @@ +module JamRuby + class IcecastMountTemplate < ActiveRecord::Base + + attr_accessor :hostname, :default_mime_type # used by jam-admin + + attr_accessible :authentication_id, :source_username, :source_pass, :max_listeners, :max_listener_duration, + :dump_file, :intro, :fallback_mount, :fallback_override, :fallback_when_full, :charset, :is_public, + :stream_name, :stream_description, :stream_url, :genre, :bitrate, :mime_type, :subtype, :burst_size, + :mp3_metadata_interval, :hidden, :on_connect, :on_disconnect, :name, as: :admin + + belongs_to :authentication, class_name: "JamRuby::IcecastUserAuthentication", inverse_of: :mount, foreign_key: 'authentication_id' + has_many :mounts, class_name: "JamRuby::IcecastMount", inverse_of: :mount_template, foreign_key: 'icecast_mount_template_id' + has_many :servers, class_name: "JamRuby::IcecastServer", inverse_of: :mount_template, foreign_key: 'mount_template_id' + + validates :source_username, length: {minimum: 5}, if: lambda {|m| m.source_username.present?} + validates :source_pass, length: {minimum: 5}, if: lambda {|m| m.source_pass.present?} + validates :max_listeners, length: {in: 1..15000}, if: lambda {|m| m.max_listeners.present?} + validates :max_listener_duration, length: {in: 1..3600 * 48}, if: lambda {|m| m.max_listener_duration.present?} + validates :fallback_override, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} + validates :fallback_when_full, :inclusion => {:in => [0, 1]} , if: lambda {|m| m.fallback_mount.present?} + validates :is_public, presence: true, :inclusion => {:in => [-1, 0, 1]} + validates :bitrate, numericality: {only_integer: true}, if: lambda {|m| m.bitrate.present?} + validates :mime_type, presence: true + validates :burst_size, numericality: {only_integer: true}, if: lambda {|m| m.burst_size.present?} + validates :mp3_metadata_interval, numericality: {only_integer: true}, if: lambda {|m| m.mp3_metadata_interval.present?} + validates :hidden, :inclusion => {:in => [0, 1]} + + before_save :sanitize_active_admin + after_save :poke_config + after_initialize :after_initialize + before_destroy :poke_config + + def after_initialize # used by jam-admin + self.hostname = 'localhost:3000' + self.default_mime_type = 'mp3' + end + + def poke_config + IcecastServer.update(servers, config_changed: 1) + end + + def sanitize_active_admin + self.authentication_id = nil if self.authentication_id == '' + end + + # pick a server that's in the same group as the user that is under the least load + def build_session_mount(music_session) + mount = IcecastMount.new + mount.authentication = authentication + mount.mount_template = self + mount.name = "/" + SecureRandom.urlsafe_base64 + mount.music_session_id = music_session.id + mount.source_username = 'source' + mount.source_pass = SecureRandom.urlsafe_base64 + mount.stream_name = "JamKazam music session created by #{music_session.creator.name}" + mount.stream_description = music_session.description + mount.stream_url = "http://www.jamkazam.com" ## TODO/XXX, the jamkazam url should be the page hosting the widget + mount.genre = music_session.genres.map {|genre| genre.description}.join(',') + mount + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_path.rb b/ruby/lib/jam_ruby/models/icecast_path.rb index ab74fe45a..7b9f7cecb 100644 --- a/ruby/lib/jam_ruby/models/icecast_path.rb +++ b/ruby/lib/jam_ruby/models/icecast_path.rb @@ -12,7 +12,8 @@ module JamRuby validates :web_root, presence: true validates :admin_root, presence: true - after_save :poke_config + after_save :poke_config + before_destroy :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_relay.rb b/ruby/lib/jam_ruby/models/icecast_relay.rb index f8c6bc712..11f7bec2d 100644 --- a/ruby/lib/jam_ruby/models/icecast_relay.rb +++ b/ruby/lib/jam_ruby/models/icecast_relay.rb @@ -13,7 +13,8 @@ module JamRuby validates :relay_shoutcast_metadata, :inclusion => {:in => [0, 1]} validates :on_demand, presence: true, :inclusion => {:in => [0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, :config_changed => true) diff --git a/ruby/lib/jam_ruby/models/icecast_security.rb b/ruby/lib/jam_ruby/models/icecast_security.rb index 2980f72e4..bae4180e4 100644 --- a/ruby/lib/jam_ruby/models/icecast_security.rb +++ b/ruby/lib/jam_ruby/models/icecast_security.rb @@ -8,7 +8,8 @@ module JamRuby validates :chroot, :inclusion => {:in => [0, 1]} - after_save :poke_config + before_destroy :poke_config + after_save :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_server.rb b/ruby/lib/jam_ruby/models/icecast_server.rb index 39320925b..4cc3bd23d 100644 --- a/ruby/lib/jam_ruby/models/icecast_server.rb +++ b/ruby/lib/jam_ruby/models/icecast_server.rb @@ -3,28 +3,30 @@ module JamRuby attr_accessor :skip_config_changed_flag - attr_accessible :template_id, :limit_id, :admin_auth_id, :directory_id, :master_relay_id, :path_id, :logging_id, + attr_accessible :template_id, :mount_template_id, :limit_id, :admin_auth_id, :directory_id, :master_relay_id, :path_id, :logging_id, :security_id, :config_changed, :hostname, :location, :admin_email, :fileserve, as: :admin - belongs_to :template, :class_name => "JamRuby::IcecastTemplate", foreign_key: 'template_id', :inverse_of => :servers + belongs_to :template, class_name: "JamRuby::IcecastTemplate", foreign_key: 'template_id', inverse_of: :servers + belongs_to :mount_template, class_name: "JamRuby::IcecastMountTemplate", foreign_key: 'mount_template_id', inverse_of: :servers + belongs_to :server_group, class_name: "JamRuby::IcecastServerGroup", foreign_key: 'icecast_server_group_id', inverse_of: :servers # all are overrides, because the template defines all of these as well. When building the XML, we will prefer these if set - belongs_to :limit, :class_name => "JamRuby::IcecastLimit", foreign_key: 'limit_id', :inverse_of => :servers - belongs_to :admin_auth, :class_name => "JamRuby::IcecastAdminAuthentication", foreign_key: 'admin_auth_id', :inverse_of => :servers - belongs_to :directory, :class_name => "JamRuby::IcecastDirectory", foreign_key: 'directory_id', :inverse_of => :servers - belongs_to :master_relay, :class_name => "JamRuby::IcecastMasterServerRelay", foreign_key: 'master_relay_id', :inverse_of => :servers - belongs_to :path, :class_name => "JamRuby::IcecastPath", foreign_key: 'path_id', :inverse_of => :servers - belongs_to :logging, :class_name => "JamRuby::IcecastLogging", foreign_key: 'logging_id', :inverse_of => :servers - belongs_to :security, :class_name => "JamRuby::IcecastSecurity", foreign_key: 'security_id', :inverse_of => :servers - has_many :listen_socket_servers, :class_name => "JamRuby::IcecastServerSocket", :inverse_of => :server - has_many :listen_sockets, :class_name => "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket + belongs_to :limit, class_name: "JamRuby::IcecastLimit", foreign_key: 'limit_id', inverse_of: :servers + belongs_to :admin_auth, class_name: "JamRuby::IcecastAdminAuthentication", foreign_key: 'admin_auth_id', inverse_of: :servers + belongs_to :directory, class_name: "JamRuby::IcecastDirectory", foreign_key: 'directory_id', inverse_of: :servers + belongs_to :master_relay, class_name: "JamRuby::IcecastMasterServerRelay", foreign_key: 'master_relay_id', inverse_of: :servers + belongs_to :path, class_name: "JamRuby::IcecastPath", foreign_key: 'path_id', inverse_of: :servers + belongs_to :logging, class_name: "JamRuby::IcecastLogging", foreign_key: 'logging_id', inverse_of: :servers + belongs_to :security, class_name: "JamRuby::IcecastSecurity", foreign_key: 'security_id', inverse_of: :servers + has_many :listen_socket_servers, class_name: "JamRuby::IcecastServerSocket", inverse_of: :server + has_many :listen_sockets, class_name: "JamRuby::IcecastListenSocket", :through => :listen_socket_servers, :source => :socket # mounts and relays are naturally server-specific, though - has_many :server_mounts, :class_name => "JamRuby::IcecastServerMount", :inverse_of => :server - has_many :mounts, :class_name => "JamRuby::IcecastMount", :through => :server_mounts, :source => :mount + #has_many :server_mounts, class_name: "JamRuby::IcecastServerMount", inverse_of: :server + has_many :mounts, class_name: "JamRuby::IcecastMount", inverse_of: :server, :foreign_key => 'icecast_server_id' - has_many :server_relays, :class_name => "JamRuby::IcecastServerRelay", :inverse_of => :relay - has_many :relays, :class_name => "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay + has_many :server_relays, class_name: "JamRuby::IcecastServerRelay", inverse_of: :relay + has_many :relays, class_name: "JamRuby::IcecastRelay", :through => :server_relays, :source => :relay validates :config_changed, :inclusion => {:in => [0, 1]} validates :hostname, presence: true @@ -32,6 +34,7 @@ module JamRuby validates :server_id, presence: true validates :template, presence: true + validates :mount_template, presence: true before_save :before_save, unless: lambda { skip_config_changed_flag } before_save :sanitize_active_admin @@ -75,8 +78,48 @@ module JamRuby end end + + def pick_listen_socket(field) + current_listen_sockets = listen_sockets.length > 0 ? listen_sockets : template.listen_sockets + socket = current_listen_sockets.first + socket[:field] if socket + end + + + # pick an icecast server with the least listeners * sources + def self.find_best_server_for_user(user) + chosen_server_id = nil + chosen_server_weight = nil + + ActiveRecord::Base.connection_pool.with_connection do |connection| + result = connection.execute('select SUM(listeners), SUM(sourced::int), icecast_servers.id + FROM icecast_servers + LEFT JOIN icecast_mounts ON icecast_servers.id = icecast_mounts.icecast_server_id + WHERE icecast_server_group_id = \'' + user.icecast_server_group_id + '\' + GROUP BY icecast_servers.id;') + + result.cmd_tuples.times do |i| + listeners = result.getvalue(i, 0).to_i + sourced = result.getvalue(i, 1).to_i + icecast_server_id = result.getvalue(i, 2) + + # compute weight. source is much more intensive than listener, based on load tests again 2.3.0 + # http://icecast.org/loadtest2.php + + weight = sourced * 10 + listeners + + if !chosen_server_id || (weight < chosen_server_weight) + chosen_server_id = icecast_server_id + chosen_server_weight = weight + end + end + end + + IcecastServer.find(chosen_server_id) if chosen_server_id + end + def to_s - return server_id + server_id end def dumpXml (output=$stdout, indent=1) @@ -84,29 +127,21 @@ module JamRuby builder = ::Builder::XmlMarkup.new(:target => output, :indent => indent) builder.tag! 'icecast' do |root| - root.tag! 'hostname', hostname - root.tag! 'location', (location.nil? || location.empty?) ? template.location : location - root.tag! 'server-id', server_id - root.tag! 'admin', (admin_email.nil? || admin_email.empty?) ? template.admin_email : admin_email - root.tag! 'fileserve', fileserve.nil? ? template.fileserve : fileserve + root.tag! 'hostname', hostname + root.tag! 'server-id', server_id + root.tag! 'location', resolve_string(:location) if string_present?(:location) + root.tag! 'admin', resolve_string(:admin_email) if string_present?(:admin_email) + root.tag! 'fileserve', resolve_int(:fileserve) if int_present?(:fileserve) + + resolve_association(:limit).dumpXml(builder) if association_present?(:limit) + resolve_association(:admin_auth).dumpXml(builder) if association_present?(:admin_auth) + resolve_association(:directory).dumpXml(builder) if association_present?(:directory) + resolve_association(:master_relay).dumpXml(builder) if association_present?(:master_relay) + resolve_association(:path).dumpXml(builder) if association_present?(:path) + resolve_association(:logging).dumpXml(builder) if association_present?(:logging) + resolve_association(:security).dumpXml(builder) if association_present?(:security) - # do we have an override specified? or do we go with the template - current_limit = limit ? limit : template.limit - current_admin_auth = admin_auth ? admin_auth : template.admin_auth - current_directory = directory ? directory : template.directory - current_master_relay = master_relay ? master_relay : template.master_relay - current_path = path ? path : template.path - current_logging = logging ? logging : template.logging - current_security = security ? security : template.security current_listen_sockets = listen_sockets.length > 0 ? listen_sockets : template.listen_sockets - - current_limit.dumpXml(builder) unless current_limit.nil? - current_admin_auth.dumpXml(builder) unless current_admin_auth.nil? - current_directory.dumpXml(builder) unless current_directory.nil? - current_master_relay.dumpXml(builder) unless current_master_relay.nil? - current_path.dumpXml(builder) unless current_path.nil? - current_logging.dumpXml(builder) unless current_logging.nil? - current_security.dumpXml(builder) unless current_security.nil? current_listen_sockets.each do |listen_socket| listen_socket.dumpXml(builder) end @@ -120,5 +155,32 @@ module JamRuby end end end + + def resolve_string(field) + self[field].present? ? self[field] : template && template[field] + end + + def string_present?(field) + val = resolve_string(field) + val ? val.present? : false + end + + def resolve_int(field) + self[field] ? self[field]: template && template[field] + end + + def int_present?(field) + resolve_int(field) + end + + def resolve_association(field) + self.send(field) ? self.send(field) : template && template.send(field) + end + + def association_present?(field) + resolve_association(field) + end + + end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_server_group.rb b/ruby/lib/jam_ruby/models/icecast_server_group.rb new file mode 100644 index 000000000..cce2e329d --- /dev/null +++ b/ruby/lib/jam_ruby/models/icecast_server_group.rb @@ -0,0 +1,11 @@ +module JamRuby + class IcecastServerGroup < ActiveRecord::Base + + attr_accessible :name, as: :admin + + has_many :users, class_name: "JamRuby::User", inverse_of: :icecast_server_group, foreign_key: 'icecast_server_group_id' + has_many :servers, class_name: "JamRuby::IcecastServer", inverse_of: :server_group, foreign_key: 'icecast_server_group_id' + + validates :name, presence: true, uniqueness: true + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_server_socket.rb b/ruby/lib/jam_ruby/models/icecast_server_socket.rb index b99d5a006..3468e11a1 100644 --- a/ruby/lib/jam_ruby/models/icecast_server_socket.rb +++ b/ruby/lib/jam_ruby/models/icecast_server_socket.rb @@ -11,5 +11,11 @@ module JamRuby validates :socket, :presence => true validates :server, :presence => true + after_save :poke_config + before_destroy :poke_config + + def poke_config + server.update_attribute(:config_changed, 1) if server + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_template.rb b/ruby/lib/jam_ruby/models/icecast_template.rb index 6813c2a04..b2459fe89 100644 --- a/ruby/lib/jam_ruby/models/icecast_template.rb +++ b/ruby/lib/jam_ruby/models/icecast_template.rb @@ -33,8 +33,8 @@ module JamRuby validates :listen_sockets, length: {minimum: 1} before_save :sanitize_active_admin - after_save :poke_config - + after_save :poke_config + before_destroy :poke_config def poke_config IcecastServer.update(servers, config_changed: 1) diff --git a/ruby/lib/jam_ruby/models/icecast_template_socket.rb b/ruby/lib/jam_ruby/models/icecast_template_socket.rb index 91c0f2eed..49ce9fdef 100644 --- a/ruby/lib/jam_ruby/models/icecast_template_socket.rb +++ b/ruby/lib/jam_ruby/models/icecast_template_socket.rb @@ -11,5 +11,11 @@ module JamRuby validates :socket, :presence => true validates :template, :presence => true + after_save :poke_config + before_destroy :poke_config + + def poke_config + IcecastServer.update(template.servers, config_changed: 1) if template + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/icecast_user_authentication.rb b/ruby/lib/jam_ruby/models/icecast_user_authentication.rb index 01da5b32c..bbacfa46e 100644 --- a/ruby/lib/jam_ruby/models/icecast_user_authentication.rb +++ b/ruby/lib/jam_ruby/models/icecast_user_authentication.rb @@ -17,10 +17,11 @@ module JamRuby validates :auth_header, presence: true, if: :url_auth? validates :timelimit_header, presence: true, if: :url_auth? + before_destroy :poke_config after_save :poke_config def poke_config - IcecastServer.update(mount.servers, config_changed: 1) if mount + mount.server.update_attribute(:config_changed, 1) if mount && mount.server end def to_s diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 1a2df5603..ff4ce0c2c 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -9,6 +9,8 @@ module JamRuby belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id" + has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id' + has_many :connections, :class_name => "JamRuby::Connection" has_many :users, :through => :connections, :class_name => "JamRuby::User" has_and_belongs_to_many :genres, :class_name => "::JamRuby::Genre", :join_table => "genres_music_sessions" @@ -37,6 +39,9 @@ module JamRuby validate :creator_is_musician validate :no_new_playback_while_playing + def before_destroy + self.mount.destroy if self.mount + end def creator_is_musician unless creator.musician? @@ -53,6 +58,21 @@ module JamRuby end end + # returns an array of client_id's that are in this session + # if as_musician is nil, all connections in the session ,regardless if it's a musician or not or not + # you can also exclude a client_id from the returned set by setting exclude_client_id + def get_connection_ids(options = {}) + as_musician = options[:as_musician] + exclude_client_id = options[:exclude_client_id] + + where = { :music_session_id => self.id } + where[:as_musician] = as_musician unless as_musician.nil? + + exclude = "client_id != '#{exclude_client_id}'"unless exclude_client_id.nil? + + Connection.select(:client_id).where(where).where(exclude).map(&:client_id) + end + # This is a little confusing. You can specify *BOTH* friends_only and my_bands_only to be true # If so, then it's an OR condition. If both are false, you can get sessions with anyone. def self.index(current_user, participants = nil, genres = nil, friends_only = false, my_bands_only = false, keyword = nil) diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 6587fcc25..a5a858be1 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -766,10 +766,10 @@ module JamRuby @@mq_router.publish_to_user(user_id, msg) end - def send_source_up_requested(music_session, host, port, mount, source_user, source_pass) - msg = @@message_factory.source_up_requested(music_session.id, host, port, mount, source_user, source_pass) + def send_source_up_requested(music_session, host, port, mount, source_user, source_pass, bitrate) + msg = @@message_factory.source_up_requested(music_session.id, host, port, mount, source_user, source_pass, bitrate) - @@mg_router.server_publish_to_session(music_session, msg) + @@mq_router.server_publish_to_session(music_session, msg) end def send_source_down_requested(music_session, mount) @@ -777,6 +777,18 @@ module JamRuby @@mq_router.server_publish_to_session(music_session, msg) end + + def send_source_up(music_session) + msg = @@message_factory.source_up(music_session.id) + + @@mq_router.server_publish_to_everyone_in_session(music_session, msg) + end + + def send_source_down(music_session) + msg = @@message_factory.source_up(music_session.id) + + @@mq_router.server_publish_to_everyone_in_session(music_session, msg) + end end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 024838e72..f1b703f10 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -18,6 +18,8 @@ module JamRuby # updating_password corresponds to a lost_password attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field + belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id' + # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" diff --git a/ruby/lib/jam_ruby/mq_router.rb b/ruby/lib/jam_ruby/mq_router.rb index e117a0974..78c8dd800 100644 --- a/ruby/lib/jam_ruby/mq_router.rb +++ b/ruby/lib/jam_ruby/mq_router.rb @@ -26,11 +26,10 @@ class MQRouter # sends a message to a session on behalf of a user # if this is originating in the context of a client, it should be specified as :client_id => "value" # client_msg should be a well-structure message (jam-pb message) - def user_publish_to_session(music_session, user, client_msg, sender = {:client_id => ""}) + def user_publish_to_session(music_session, user, client_msg, sender = {:client_id => nil}) access_music_session(music_session, user) - # gather up client_ids in the session - client_ids = music_session.connections.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] } + client_ids = music_session.get_connection_ids(as_musician: true, exclude_client_id: sender[:client_id]) publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) end @@ -38,13 +37,21 @@ class MQRouter # sends a message to a session from the server # no access check as with user_publish_to_session # client_msg should be a well-structure message (jam-pb message) - def server_publish_to_session(music_session, client_msg, sender = {:client_id => ""}) + def server_publish_to_session(music_session, client_msg, sender = {:client_id => nil}) # gather up client_ids in the session - client_ids = music_session.connections.map { |client| client.client_id }.reject { |client_id| client_id == sender[:client_id] } + client_ids = music_session.get_connection_ids(as_musician: true, exclude_client_id: sender[:client_id]) publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) end + # sends a message to a session AND fans/listeners from the server + # client_msg should be a well-structure message (jam-pb message) + def server_publish_to_everyone_in_session(music_session, client_msg, sender = {:client_id => nil}) + # gather up client_ids in the session + client_ids = music_session.get_connection_ids(exclude_client_id: sender[:client_id]) + publish_to_session(music_session.id, client_ids, client_msg.to_s, sender) + end + # sends a message to a client with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_client(client_id, client_msg, sender = {:client_id => ""}) @@ -60,7 +67,7 @@ class MQRouter # sends a message to a session with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects - def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => ""}) + def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => nil}) EM.schedule do sender_client_id = sender[:client_id] diff --git a/ruby/lib/jam_ruby/resque/icecast_config_writer.rb b/ruby/lib/jam_ruby/resque/icecast_config_writer.rb index b86c9ead7..96076f41e 100644 --- a/ruby/lib/jam_ruby/resque/icecast_config_writer.rb +++ b/ruby/lib/jam_ruby/resque/icecast_config_writer.rb @@ -1,6 +1,7 @@ require 'json' require 'resque' -require 'resque-retry' + +require 'resque-lonely_job' require 'net/http' require 'digest/md5' @@ -8,6 +9,7 @@ module JamRuby # executes a mix of tracks, creating a final output mix class IcecastConfigWriter + extend Resque::Plugins::LonelyJob @@log = Logging.logger[IcecastConfigWriter] diff --git a/ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb b/ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb new file mode 100644 index 000000000..4d1577637 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/IcecastSourceCheck.rb @@ -0,0 +1,32 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + + # http://blog.bignerdranch.com/1643-never-use-resque-for-serial-jobs/ + # periodically scheduled to find sources that need to be brought down, or alternatively, it seems the client failed to start sourcing + class IcecastSourceCheck + + @queue = :icecast_source_check + + @@log = Logging.logger[IcecastSourceCheck] + + def self.perform + @@log.debug("waking up") + + # if we haven't seen updated_at be tickled in 5 minutes, but config_changed is still set to TRUE, this record has gotten stale + IcecastMount.find_each(:conditions => "sourced_needs_changing_at < (NOW() - interval '#{APP_CONFIG.icecast_max_sourced_changed} second')", :batch_size => 100) do |server| + server.with_lock do + IcecastConfigWriter.enqueue(server.server_id) + end + + end + + @@log.debug("done") + end + end + +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb b/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb index f0388d8f5..340e9a05f 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/icecast_config_retry.rb @@ -14,7 +14,11 @@ module JamRuby @@log = Logging.logger[IcecastConfigRetry] def self.perform + @@log.debug("waking up") + IcecastConfigWriter.queue_jobs_needing_retry + + @@log.debug("done") end end diff --git a/ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb b/ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb new file mode 100644 index 000000000..d832f4d2d --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/icecast_source_check.rb @@ -0,0 +1,59 @@ +require 'json' +require 'resque' +require 'resque-lonely_job' +require 'net/http' +require 'digest/md5' + +module JamRuby + + # http://blog.bignerdranch.com/1643-never-use-resque-for-serial-jobs/ + # periodically scheduled to find sources that need to be brought down, or alternatively, it seems the client failed to start sourcing + class IcecastSourceCheck + extend Resque::Plugins::LonelyJob + + + @queue = :icecast_source_check + + + @@log = Logging.logger[IcecastSourceCheck] + + + def self.perform + @@log.debug("waking up") + + JamWebEventMachine.run_wait_stop do + IcecastSourceCheck.new.run + end + + @@log.debug("done") + end + + def run # if we haven't seen updated_at be tickled in 5 minutes, but config_changed is still set to TRUE, this record has gotten stale + IcecastMount.find_each(lock: true, :conditions => "sourced_needs_changing_at < (NOW() - interval '#{APP_CONFIG.icecast_max_sourced_changed} second')", :batch_size => 100) do |mount| + if mount.music_session_id + mount.with_lock do + handle_notifications(mount) + end + end + end + end + + def handle_notifications(mount) + if mount.listeners == 0 && mount.sourced + # if no listeners, but we are sourced, then ask it to stop sourcing + @@log.debug("SOURCE_DOWN_REQUEST called on mount #{mount.name}") + + mount.update_attribute(:sourced_needs_changing_at, Time.now) # we send out a source request, so we need to update the time + mount.notify_source_down_requested + + elsif mount.listeners > 0 && !mount.sourced + # if we have some listeners, and still are not sourced, then ask to start sourcing again + @@log.debug("SOURCE_UP_REQUEST called on mount #{mount.name}") + + mount.update_attribute(:sourced_needs_changing_at, Time.now) # we send out a source request, so we need to update the time + mount.notify_source_up_requested + + end + end + end +end \ No newline at end of file diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 4a51defe2..0724273f6 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -195,7 +195,7 @@ FactoryGirl.define do factory :icecast_mount, :class => JamRuby::IcecastMount do name "/" + Faker::Lorem.characters(10) source_username Faker::Lorem.characters(10) - source_pass Faker::Lorem.characters(10) + source_pass Faker::Lorem.characters(10) max_listeners 100 max_listener_duration 3600 fallback_mount Faker::Lorem.characters(10) @@ -207,10 +207,21 @@ FactoryGirl.define do stream_url Faker::Lorem.characters(10) genre Faker::Lorem.characters(10) hidden 0 + association :server, factory: :icecast_server_with_overrides factory :icecast_mount_with_auth do association :authentication, :factory => :icecast_user_authentication + + factory :iceast_mount_with_template do + association :mount_template, :factory => :icecast_mount_template + + factory :iceast_mount_with_music_session do + association :music_session, :factory => :music_session + end + end end + + end factory :icecast_listen_socket, :class => JamRuby::IcecastListenSocket do @@ -227,7 +238,7 @@ FactoryGirl.define do factory :icecast_user_authentication, :class => JamRuby::IcecastUserAuthentication do authentication_type 'url' unused_username Faker::Lorem.characters(10) - unused_pass Faker::Lorem.characters(10) + unused_pass Faker::Lorem.characters(10) mount_add Faker::Lorem.characters(10) mount_remove Faker::Lorem.characters(10) listener_add Faker::Lorem.characters(10) @@ -242,6 +253,7 @@ FactoryGirl.define do factory :icecast_server_minimal do association :template, :factory => :icecast_template_minimal + association :mount_template, :factory => :icecast_mount_template factory :icecast_server_with_overrides do association :limit, :factory => :icecast_limit @@ -274,4 +286,23 @@ FactoryGirl.define do end end end + + factory :icecast_mount_template, :class => JamRuby::IcecastMountTemplate do + sequence(:name) { |n| "name-#{n}"} + source_username Faker::Lorem.characters(10) + source_pass Faker::Lorem.characters(10) + max_listeners 100 + max_listener_duration 3600 + fallback_mount Faker::Lorem.characters(10) + fallback_override 1 + fallback_when_full 1 + is_public -1 + stream_name Faker::Lorem.characters(10) + stream_description Faker::Lorem.characters(10) + stream_url Faker::Lorem.characters(10) + genre Faker::Lorem.characters(10) + hidden 0 + association :authentication, :factory => :icecast_user_authentication + end + end diff --git a/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb b/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb index 48ec8f43e..85f562405 100644 --- a/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_admin_authentication_spec.rb @@ -50,10 +50,22 @@ describe IcecastAdminAuthentication do server.config_changed.should == 1 end + it "success when deleted via template" do + server.template.admin_auth.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.admin_auth.save! server.reload server.config_changed.should == 1 end + + it "success when deleted via server" do + server.admin_auth.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_directory_spec.rb b/ruby/spec/jam_ruby/models/icecast_directory_spec.rb index 203cead6a..f77f65768 100644 --- a/ruby/spec/jam_ruby/models/icecast_directory_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_directory_spec.rb @@ -57,11 +57,23 @@ describe IcecastDirectory do server.config_changed.should == 1 end + it "delete via template" do + server.template.directory.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.directory.save! server.reload server.config_changed.should == 1 end + + it "destroy via server" do + server.directory.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_limit_spec.rb b/ruby/spec/jam_ruby/models/icecast_limit_spec.rb index 910b2fca4..bab815e50 100644 --- a/ruby/spec/jam_ruby/models/icecast_limit_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_limit_spec.rb @@ -66,10 +66,22 @@ describe IcecastLimit do server.config_changed.should == 1 end + it "delete via template" do + server.template.limit.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.limit.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.limit.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb b/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb index 1de0009a0..19a1efa60 100644 --- a/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_listen_socket_spec.rb @@ -27,10 +27,22 @@ describe IcecastListenSocket do server.config_changed.should == 1 end + it "delete via template" do + server.template.listen_sockets.first.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.listen_sockets.first.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.listen_sockets.first.destroy + server.reload + server.config_changed.should == 1 + end end end diff --git a/ruby/spec/jam_ruby/models/icecast_logging_spec.rb b/ruby/spec/jam_ruby/models/icecast_logging_spec.rb index 2ad726bcb..2892bbc82 100644 --- a/ruby/spec/jam_ruby/models/icecast_logging_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_logging_spec.rb @@ -40,10 +40,22 @@ describe IcecastLogging do server.config_changed.should == 1 end + it "delete via template" do + server.template.logging.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.logging.save! server.reload server.config_changed.should == 1 end + + it "deete via server" do + server.logging.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb b/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb index 5de60553e..5dfa1231a 100644 --- a/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_master_server_relay_spec.rb @@ -53,11 +53,23 @@ describe IcecastMasterServerRelay do server.config_changed.should == 1 end + it "delete via template" do + server.template.master_relay.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.master_relay.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.master_relay.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_mount_spec.rb b/ruby/spec/jam_ruby/models/icecast_mount_spec.rb index 5a42a0397..bcd0955a7 100644 --- a/ruby/spec/jam_ruby/models/icecast_mount_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_mount_spec.rb @@ -10,10 +10,6 @@ describe IcecastMount do mount = IcecastMount.new mount.save.should be_false mount.errors[:name].should == ["can't be blank", "must start with /"] - mount.errors[:stream_name].should == ["can't be blank"] - mount.errors[:stream_description].should == ["can't be blank"] - mount.errors[:stream_url].should == ["can't be blank"] - mount.errors[:genre].should == ["can't be blank"] end @@ -35,6 +31,7 @@ describe IcecastMount do mount.max_listeners = 1000 mount.max_listener_duration = 3600 mount.authentication = FactoryGirl.create(:icecast_user_authentication) + mount.server = FactoryGirl.create(:icecast_server_with_overrides) mount.save! @@ -70,11 +67,39 @@ describe IcecastMount do xml.css('mount authentication').length.should == 1 # no reason to test futher; it's tested in that model end + describe "override xml over mount template" do + let(:mount) {FactoryGirl.create(:iceast_mount_with_template)} + + it "should allow override by mount" do + mount.dumpXml(builder) + output.rewind + xml = Nokogiri::XML(output) + xml.css('mount mount-name').text.should == mount.name + xml.css('mount username').text.should == mount.source_username + xml.css('mount bitrate').text.should == mount.bitrate.to_s + xml.css('mount type').text.should == mount.mount_template.mime_type + xml.css('mount stream-url').text.should == mount.stream_url + + # now see the stream_url, and bitrate, go back to the template's value because we set it to nil + mount.bitrate = nil + mount.stream_url = nil + mount.save! + + output = StringIO.new + builder = ::Builder::XmlMarkup.new(:target => output, :indent => 1) + mount.dumpXml(builder) + output.rewind + xml = Nokogiri::XML(output) + xml.css('mount bitrate').text.should == mount.mount_template.bitrate.to_s + xml.css('mount stream-url').text.should == mount.mount_template.stream_url + end + end + describe "poke configs" do let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) } before(:each) do - server.mounts << FactoryGirl.create(:icecast_mount) + server.mounts << FactoryGirl.create(:icecast_mount, server: server) server.save! server.config_updated server.reload @@ -86,6 +111,12 @@ describe IcecastMount do server.reload server.config_changed.should == 1 end + + it "success when deleted" do + server.mounts.first.destroy + server.reload + server.config_changed.should == 1 + end end describe "icecast server callbacks" do @@ -93,4 +124,125 @@ describe IcecastMount do icecast_mount.source_up end end + + describe "listener/source" do + let(:mount) {FactoryGirl.create(:iceast_mount_with_template)} + + describe "listeners" do + it "listener_add" do + mount.listener_add + mount.listeners.should == 1 + end + + it "listener_remove when at 0" do + mount.listener_remove + mount.listeners.should == 0 + end + + it "listener_remove" do + mount.listener_add + mount.listener_remove + mount.listeners.should == 0 + end + end + + describe "sources" do + it "source_up" do + mount.source_up + mount.sourced.should == true + end + end + + describe "sources" do + it "source_down" do + mount.source_up + mount.source_down + mount.sourced.should == false + end + end + end + describe "build_session_mount" do + + let(:server1) {FactoryGirl.create(:icecast_server_minimal)} + let(:server2) {FactoryGirl.create(:icecast_server_with_overrides)} + let(:server3) {FactoryGirl.create(:icecast_server_with_overrides)} + let(:hidden_music_session) { FactoryGirl.create(:music_session, :fan_access => false)} + let(:public_music_session) { FactoryGirl.create(:music_session, :fan_access => true)} + let(:public_music_session2) { FactoryGirl.create(:music_session, :fan_access => true)} + let(:public_music_session3) { FactoryGirl.create(:music_session, :fan_access => true)} + + before(:each) do + + end + + it "no fan access means no mount" do + mount = IcecastMount.build_session_mount(hidden_music_session) + mount.should be_nil + end + + it "with no servers" do + IcecastServer.count.should == 0 + mount = IcecastMount.build_session_mount(public_music_session) + mount.should be_nil + end + + it "with a server that has a mount template" do + server1.mount_template.should_not be_nil + mount = IcecastMount.build_session_mount(public_music_session) + mount.should_not be_nil + mount.save! + end + + it "with a server that already has an associated mount" do + server1.mount_template.should_not be_nil + mount = IcecastMount.build_session_mount(public_music_session) + mount.save! + + mount = IcecastMount.build_session_mount(public_music_session2) + mount.save! + server1.reload + server1.mounts.length.should == 2 + end + + it "picks a second server once the 1st has been chosen" do + server1.touch + + mount = IcecastMount.build_session_mount(public_music_session) + mount.listeners = 1 # affect the weight + mount.save! + + server2.touch + + mount = IcecastMount.build_session_mount(public_music_session2) + mount.save! + server1.reload + server1.mounts.length.should == 1 + server2.reload + server2.mounts.length.should == 1 + end + + it "picks the 1st server again once the 2nd has higher weight" do + server1.touch + + mount = IcecastMount.build_session_mount(public_music_session) + mount.listeners = 1 # affect the weight + mount.save! + + server2.touch + + mount = IcecastMount.build_session_mount(public_music_session2) + mount.sourced = 1 + mount.save! + + mount = IcecastMount.build_session_mount(public_music_session3) + mount.listeners = 1 + mount.save! + + server1.reload + server1.mounts.length.should == 2 + server2.reload + server2.mounts.length.should == 1 + end + end + end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb b/ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb new file mode 100644 index 000000000..2478af012 --- /dev/null +++ b/ruby/spec/jam_ruby/models/icecast_mount_template_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe IcecastMountTemplate do + + let(:mount_template) { template = FactoryGirl.create(:icecast_mount_template) } + + it "save" do + mount_template.errors.any?.should be_false + end + + describe "poke configs" do + let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) } + let(:music_session) { FactoryGirl.create(:music_session, :fan_access => true)} + + before(:each) do + server.touch + mount = IcecastMount.build_session_mount(music_session) + mount.save! + server.save! + server.config_updated + server.reload + server.config_changed.should == 0 + end + + it "success via server" do + server.mounts.first.mount_template.save! + server.reload + server.config_changed.should == 1 + end + + it "delete via server" do + server.mounts.first.mount_template.destroy + server.reload + server.config_changed.should == 1 + end + end +end diff --git a/ruby/spec/jam_ruby/models/icecast_path_spec.rb b/ruby/spec/jam_ruby/models/icecast_path_spec.rb index 0f73acd09..50354d38d 100644 --- a/ruby/spec/jam_ruby/models/icecast_path_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_path_spec.rb @@ -64,10 +64,22 @@ describe IcecastPath do server.config_changed.should == 1 end + it "delete via template" do + server.template.path.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.path.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.path.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_relay_spec.rb b/ruby/spec/jam_ruby/models/icecast_relay_spec.rb index 09c46e303..705434b3b 100644 --- a/ruby/spec/jam_ruby/models/icecast_relay_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_relay_spec.rb @@ -48,5 +48,11 @@ describe IcecastRelay do server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.relays.first.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_security_spec.rb b/ruby/spec/jam_ruby/models/icecast_security_spec.rb index 08ad498a9..e33d9e180 100644 --- a/ruby/spec/jam_ruby/models/icecast_security_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_security_spec.rb @@ -40,10 +40,22 @@ describe IcecastSecurity do server.config_changed.should == 1 end + it "delete via template" do + server.template.security.destroy + server.reload + server.config_changed.should == 1 + end + it "success via server" do server.security.save! server.reload server.config_changed.should == 1 end + + it "delete via server" do + server.security.destroy + server.reload + server.config_changed.should == 1 + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_server_spec.rb b/ruby/spec/jam_ruby/models/icecast_server_spec.rb index a7831aa72..a9e6adbb2 100644 --- a/ruby/spec/jam_ruby/models/icecast_server_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_server_spec.rb @@ -30,4 +30,34 @@ describe IcecastServer do xml.css('icecast security').length.should == 1 xml.css('icecast listen-socket').length.should == 1 end + + it "xml overrides" do + server = FactoryGirl.create(:icecast_server_minimal) + server.save! + server.reload + server.dumpXml(output) + + output.rewind + + xml = Nokogiri::XML(output) + xml.css('icecast location').text.should == server.template.location + xml.css('icecast fileserve').text.should == server.template.fileserve.to_s + xml.css('icecast limits').length.should == 1 + xml.css('icecast limits queue-size').text.should == server.template.limit.queue_size.to_s + + server.location = "override" + server.fileserve = 1 + server.limit = FactoryGirl.create(:icecast_limit, :queue_size => 777) + server.save! + + output = StringIO.new + builder = ::Builder::XmlMarkup.new(:target => output, :indent => 1) + server.dumpXml(builder) + output.rewind + xml = Nokogiri::XML(output) + xml.css('icecast location').text.should == server.location + xml.css('icecast fileserve').text.should == server.fileserve.to_s + xml.css('icecast limits').length.should == 1 + xml.css('icecast limits queue-size').text.should == server.limit.queue_size.to_s + end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/icecast_template_spec.rb b/ruby/spec/jam_ruby/models/icecast_template_spec.rb index 267acdd0d..690cf9122 100644 --- a/ruby/spec/jam_ruby/models/icecast_template_spec.rb +++ b/ruby/spec/jam_ruby/models/icecast_template_spec.rb @@ -1,10 +1,20 @@ require 'spec_helper' -describe IcecastListenSocket do +describe IcecastTemplate do let(:template) { template = FactoryGirl.create(:icecast_template_minimal) } it "save" do template.errors.any?.should be_false end + + describe "poke configs" do + let(:server) { a = FactoryGirl.create(:icecast_server_with_overrides); a.config_updated; IcecastServer.find(a.id) } + + it "success via template" do + server.template.save! + server.reload + server.config_changed.should == 1 + end + end end diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index 32909739e..d16972b6e 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -480,5 +480,36 @@ describe MusicSession do end end end + + describe "get_connection_ids" do + before(:each) do + @user1 = FactoryGirl.create(:user) + @user2 = FactoryGirl.create(:user) + @music_session = FactoryGirl.create(:music_session, :creator => @user1, :musician_access => true) + @connection1 = FactoryGirl.create(:connection, :user => @user1, :music_session => @music_session, :as_musician => true) + @connection2 = FactoryGirl.create(:connection, :user => @user2, :music_session => @music_session, :as_musician => false) + + end + + it "get all connections" do + @music_session.get_connection_ids().should == [@connection1.client_id, @connection2.client_id] + end + + it "exclude non-musicians" do + @music_session.get_connection_ids(as_musician: true).should == [@connection1.client_id] + end + + it "exclude musicians" do + @music_session.get_connection_ids(as_musician: false).should == [@connection2.client_id] + end + + it "exclude particular client" do + @music_session.get_connection_ids(exclude_client_id: @connection1.client_id).should == [@connection2.client_id] + end + + it "exclude particular client and exclude non-musicians" do + @music_session.get_connection_ids(exclude_client_id: @connection2.client_id, as_musician: true).should == [@connection1.client_id] + end + end end diff --git a/ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb b/ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb new file mode 100644 index 000000000..2c528bf6d --- /dev/null +++ b/ruby/spec/jam_ruby/resque/icecast_source_check_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' +require 'fileutils' + +# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests +describe IcecastSourceCheck do + + let(:check) { IcecastSourceCheck.new } + + describe "integration" do + + it "be OK with no mounts" do + IcecastMount.count().should == 0 + check.should_not_receive(:handle_notifications) + check.run + end + + + it "find no mounts if source_hanged timestamp is nil and listeners = 1/sourced = false" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced: false, listeners: 1) + check.should_not_receive(:handle_notifications) + check.run + end + + it "find no mounts if source_changed timestamp is nil and listeners = 0/sourced = true" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced: true, listeners: 1) + check.should_not_receive(:handle_notifications) + check.run + end + + it "find no mounts if source_changed timestamp is very recent and listeners = 1/sourced = false" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: Time.now, sourced: false, listeners: 1) + check.should_not_receive(:handle_notifications) + check.run + end + + it "find no mounts if source_changed timestamp is very recent and listeners = 0/sourced = true" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: Time.now, sourced: true, listeners: 0) + check.should_not_receive(:handle_notifications) + check.run + end + + it "sends notify_source_down_requested when old source_changed timestamp, and sourced = true and listeners = 0" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:true, listeners: 0) + check.stub(:handle_notifications) do |mount| + mount.should_receive(:notify_source_down_requested).once + mount.should_not_receive(:notify_source_up_requested) + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + it "does not send notify_source_down_requested when old source_changed timestamp, and sourced = true and listeners = 1" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:true, listeners: 1) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_not_receive(:notify_source_up_requested) + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + it "sends notify_source_up_requested when old source_changed timestamp, and sourced = false and listeners = 1" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:false, listeners: 1) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_receive(:notify_source_up_requested).once + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + + it "does not send notify_source_up_requested when old source_changed timestamp, and sourced = false and listeners = 0" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:false, listeners: 0) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_not_receive(:notify_source_up_requested) + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + end + + it "resets source_changed_at when a notification is sent out" do + mount = FactoryGirl.create(:iceast_mount_with_music_session, sourced_needs_changing_at: 2.days.ago, sourced:false, listeners: 1) + check.stub(:handle_notifications) do |mount| + mount.should_not_receive(:notify_source_down_requested) + mount.should_receive(:notify_source_up_requested).once + mount.should_not_receive(:notify_source_up) + mount.should_not_receive(:notify_source_down) + check.unstub!(:handle_notifications) + check.handle_notifications(mount) + end + check.run + mount.reload + (mount.sourced_needs_changing_at.to_i - Time.now.to_i).abs.should < 10 # less than 5 seconds -- just a little slop for a very slow build server + end + end +end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 2fd679cc5..bbdbd7440 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -79,8 +79,8 @@ Spork.prefork do config.filter_run_excluding aws: true unless run_tests? :aws config.before(:suite) do - DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] } - DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] }) + DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres icecast_server_groups] } + DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres icecast_server_groups] }) end config.before(:each) do diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 84b51f440..4cf3ec682 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -37,6 +37,10 @@ def app_config 2 * 60 # 2 minutes end + def icecast_max_sourced_changed + 15 # 15 seconds + end + def rabbitmq_host "localhost" end diff --git a/web/Gemfile b/web/Gemfile index 8132e1b52..30e0c3cd3 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -62,6 +62,7 @@ gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' gem 'resque-dynamic-queues' +gem 'resque-lonely_job', '~> 1.0.0' gem 'quiet_assets', :group => :development gem "bugsnag" diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index b6820a8b1..a104e8e9b 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -48,6 +48,12 @@ BAND_INVITATION : "BAND_INVITATION", BAND_INVITATION_ACCEPTED : "BAND_INVITATION_ACCEPTED", + // broadcast notifications + SOURCE_UP_REQUESTED : "SOURCE_UP_REQUESTED", + SOURCE_DOWN_REQUESTED : "SOURCE_DOWN_REQUESTED", + SOURCE_UP : "SOURCE_UP", + SOURCE_DOWN : "SOURCE_DOWN", + TEST_SESSION_MESSAGE : "TEST_SESSION_MESSAGE", PING_REQUEST : "PING_REQUEST", PING_ACK : "PING_ACK", diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 96ee3781c..bc2d0baf6 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -571,6 +571,14 @@ function CloseRecording() {} function OnDownloadAvailable() {} + function SessionLiveBroadcastStart(host, port, mount, sourceUser, sourcePass, preferredClientId, bitrate) + { + logger.debug("SessionLiveBroadcastStart requested"); + } + + function SessionLiveBroadcastStop() { + logger.debug("SessionLiveBroadcastStop requested"); + } // Javascript Bridge seems to camel-case // Set the instance functions: @@ -703,6 +711,10 @@ this.CloseRecording = CloseRecording; this.OnDownloadAvailable = OnDownloadAvailable; + // Broadcasting + this.SessionLiveBroadcastStart = SessionLiveBroadcastStart; + this.SessionLiveBroadcastStop = SessionLiveBroadcastStop; + // fake calls; not a part of the actual jam client this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; this.SetFakeRecordingImpl = SetFakeRecordingImpl; diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index 252ab20c4..4e63c8c24 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -233,10 +233,95 @@ acceptBandInvitation({ "band_invitation_id": payload.band_invitation_id, "band_id": payload.band_id, "notification_id": payload.notification_id }); }); } - else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) { $notification.find('#div-actions').hide(); } + else if (type === context.JK.MessageType.SOURCE_UP_REQUESTED) { + var current_session_id = context.JK.CurrentSessionModel.id(); + + if (!current_session_id) { + // we are not in a session + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_UP_REQUESTED came in for session_id:" + payload.music_session + ", but we are not in a session and the last session ID did not match the one specified"; + } + } + else { + // we are in a session + if(current_session_id == payload.music_session) { + context.jamClient.SessionLiveBroadcastStart(payload.host, payload.port, payload.mount, + payload.source_user, payload.source_pass, + '', payload.bitrate) + } + else { + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_UP_REQUESTED came in for session_id:" + payload.music_session + ", but we are in a session and the last session ID did not match the one specified"; + } + } + } + } + else if (type === context.JK.MessageType.SOURCE_DOWN_REQUESTED) { + var current_session_id = context.JK.CurrentSessionModel.id(); + + if (!current_session_id) { + // we are not in a session + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_DOWN_REQUESTED came in for session_id:" + payload.music_session + ", but we are not in a session and the last session ID did not match the one specified"; + } + } + else { + // we are in a session + if(current_session_id == payload.music_session) { + context.jamClient.SessionLiveBroadcastStop(); + } + else { + var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession(); + if(last_session && last_session.id == payload.music_session) { + // the last session we were in was responsible for this message. not that odd at all + logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one") + } + else { + // this means we aren't in a session, and, what's worse, + // the last session we were in does not match the specified music_session id + throw "SOURCE_DOWN_REQUESTED came in for session_id:" + payload.music_session + ", but we are in a session and the last session ID did not match the one specified"; + } + } + } + } + else if (type === context.JK.MessageType.SOURCE_UP) { + log.debug("session %o is now being broadcasted", payload.music_session); + app.notify({ + "title": "Now Broadcasting", + "text": "This session is now being broadcasted." + }); + } + else if (type === context.JK.MessageType.SOURCE_DOWN) { + log.debug("session %o is no longer being broadcasted", payload.music_session); + app.notify({ + "title": "No Longer Broadcasting", + "text": "This session is no longer being broadcasted." + }); + } } function deleteNotificationHandler(evt) { diff --git a/web/app/controllers/api_icecast_controller.rb b/web/app/controllers/api_icecast_controller.rb index 2d0986705..2340575b0 100644 --- a/web/app/controllers/api_icecast_controller.rb +++ b/web/app/controllers/api_icecast_controller.rb @@ -7,15 +7,14 @@ class ApiIcecastController < ApiController def mount_add - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.source_up - render text: '', :status => :ok end def mount_remove - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.source_down render text: '', :status => :ok @@ -28,7 +27,7 @@ class ApiIcecastController < ApiController remote_ip = params[:ip] remote_user_agent = params[:agent] - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.listener_add render text: '', :status => :ok @@ -40,7 +39,7 @@ class ApiIcecastController < ApiController pass = params[:pass] duration = params[:duration] # seconds connected to the listen stream - mount = IcecastMount.find(@mount_id) + mount = IcecastMount.find_by_name!(@mount_id) mount.listener_remove render text: '', :status => :ok diff --git a/web/config/application.rb b/web/config/application.rb index fc11d17b8..e6394999e 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -177,6 +177,7 @@ include JamRuby # this will be the qualifier on the IcecastConfigWorker queue name config.icecast_server_id = ENV['ICECAST_SERVER_ID'] || 'localhost' config.icecast_max_missing_check = 2 * 60 # 2 minutes + config.icecast_max_sourced_changed = 15 # 15 seconds config.email_alerts_alias = 'nobody@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails config.email_generic_from = 'nobody@jamkazam.com' diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index 97adc2d3c..8a6dde7df 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -8,3 +8,10 @@ IcecastConfigRetry: cron: 0 * * * * class: "JamRuby::IcecastConfigRetry" description: "Finds icecast servers that have had their config_changed, but no IcecastConfigWriter check recently" + + +IcecastSourceCheck: + cron: "10 * * * * *" + class: "JamRuby::IcecastSourceCheck" + description: "Finds icecast mounts that need their 'sourced' state to change, but haven't in some time" + diff --git a/web/lib/music_session_manager.rb b/web/lib/music_session_manager.rb index 7a71bdcf9..7889db8e2 100644 --- a/web/lib/music_session_manager.rb +++ b/web/lib/music_session_manager.rb @@ -14,7 +14,8 @@ MusicSessionManager < BaseManager ActiveRecord::Base.transaction do # check if we are connected to rabbitmq - music_session = MusicSession.new() + music_session = MusicSession.new + music_session.id = SecureRandom.uuid music_session.creator = user music_session.description = description music_session.musician_access = musician_access @@ -24,9 +25,6 @@ MusicSessionManager < BaseManager music_session.band = band music_session.legal_terms = legal_terms - #genres = genres - @log.debug "Genres class: " + genres.class.to_s - unless genres.nil? genres.each do |genre_id| loaded_genre = Genre.find(genre_id) @@ -34,6 +32,13 @@ MusicSessionManager < BaseManager end end + + if fan_access + # create an icecast mount since regular users can listen in to the broadcast + music_session.mount = IcecastMount.build_session_mount(music_session) + end + + music_session.save unless music_session.errors.any? diff --git a/websocket-gateway/Gemfile b/websocket-gateway/Gemfile index d2c796ec3..78131b08a 100644 --- a/websocket-gateway/Gemfile +++ b/websocket-gateway/Gemfile @@ -41,6 +41,7 @@ gem 'postgres_ext' gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' +gem 'resque-lonely_job', '~> 1.0.0' group :development do gem 'pry' From f91fb27d64a708e97ad89b9a2f7d94fba0855f78 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 21 Jan 2014 16:59:29 +0000 Subject: [PATCH 26/28] * fixing websocket-gateway tests by not deleting icecast server groups table --- websocket-gateway/spec/spec_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/websocket-gateway/spec/spec_helper.rb b/websocket-gateway/spec/spec_helper.rb index 193b012ac..0468e41e5 100644 --- a/websocket-gateway/spec/spec_helper.rb +++ b/websocket-gateway/spec/spec_helper.rb @@ -79,8 +79,8 @@ include Jampb end config.before(:suite) do - DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] } - DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] }) + DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres icecast_server_groups] } + DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres icecast_server_groups] }) end #config.after(:each) do From 7c375278f8e7505d39c2bebbe7d51ee22f44f05c Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 21 Jan 2014 20:30:40 -0600 Subject: [PATCH 27/28] * fixing jamadmin pages --- ruby/lib/jam_ruby/models/artifact_update.rb | 2 +- ruby/lib/jam_ruby/models/invited_user.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ruby/lib/jam_ruby/models/artifact_update.rb b/ruby/lib/jam_ruby/models/artifact_update.rb index d60d0be41..65cd7af3d 100644 --- a/ruby/lib/jam_ruby/models/artifact_update.rb +++ b/ruby/lib/jam_ruby/models/artifact_update.rb @@ -6,7 +6,7 @@ module JamRuby PRODUCTS = ['JamClient/Win32', 'JamClient/MacOSX'] self.primary_key = 'id' - attr_accessible :version, :uri, :sha1, :environment, :product + attr_accessible :version, :uri, :sha1, :environment, :product, as: :admin mount_uploader :uri, ArtifactUploader diff --git a/ruby/lib/jam_ruby/models/invited_user.rb b/ruby/lib/jam_ruby/models/invited_user.rb index 059e2170e..385347d34 100644 --- a/ruby/lib/jam_ruby/models/invited_user.rb +++ b/ruby/lib/jam_ruby/models/invited_user.rb @@ -3,7 +3,7 @@ module JamRuby VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i - attr_accessible :email, :sender_id, :autofriend, :note + attr_accessible :email, :sender_id, :autofriend, :note, as: :admin attr_accessor :accepted_twice self.primary_key = 'id' From 4b13c5c16c1bd809b74092e34bb80deed937c08f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 22 Jan 2014 16:23:33 -0600 Subject: [PATCH 28/28] * making ubuntu command need sudo, and adding ubuntu template maker --- admin/app/admin/icecast_bootstrap.rb | 77 +++++++++++++++++++++- ruby/lib/jam_ruby/models/icecast_server.rb | 3 +- ruby/lib/jam_ruby/models/invited_user.rb | 1 + web/config/application.rb | 2 +- 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/admin/app/admin/icecast_bootstrap.rb b/admin/app/admin/icecast_bootstrap.rb index 3b3f643b6..5a87155d7 100644 --- a/admin/app/admin/icecast_bootstrap.rb +++ b/admin/app/admin/icecast_bootstrap.rb @@ -62,11 +62,11 @@ ActiveAdmin.register_page "Bootstrap" do logging.save! listen_socket1 = IcecastListenSocket.new - listen_socket1.port = 8000 + listen_socket1.port = 9000 listen_socket1.save! listen_socket2 = IcecastListenSocket.new - listen_socket2.port = 8001 + listen_socket2.port = 9001 listen_socket2.save! template = IcecastTemplate.new @@ -86,6 +86,72 @@ ActiveAdmin.register_page "Bootstrap" do redirect_to admin_bootstrap_path, :notice => "Brew template created. Now, create a mount template." end + page_action :ubuntu_template, :method => :post do + # to make this template, I installed icecast233 from jenkins (or our chef'ed apt-repo, same difference), and then based the rest of this code on what I saw in /etc/icecast2/icecast.xml + + IcecastServer.transaction do + + limit = IcecastLimit.new + limit.clients = 100 + limit.sources = 2 + limit.queue_size = 524288 + limit.client_timeout = 30 + limit.header_timeout = 15 + limit.source_timeout = 10 + limit.burst_size = 65535 + limit.save! + + admin_auth = IcecastAdminAuthentication.new + admin_auth.source_pass = 'blueberryjam' + admin_auth.relay_user = 'jamjam' + admin_auth.relay_pass = 'blueberryjam' + admin_auth.admin_user = 'jamjam' + admin_auth.admin_pass = 'blueberryjam' + admin_auth.save! + path = IcecastPath.new + path.base_dir = '/usr/share/icecast2' + path.log_dir = '/var/log/icecast2' + path.web_root = '/usr/share/icecast2/web' + path.admin_root = '/usr/share/icecast2/admin' + path.pid_file = nil + path.save! + + security = IcecastSecurity.new + security.chroot = false + security.save! + + logging = IcecastLogging.new + logging.access_log = 'access.log' + logging.error_log = 'error.log' + logging.log_level = 3 # you might want to change this after creating the template + logging.log_size = 10000 + logging.save! + + listen_socket1 = IcecastListenSocket.new + listen_socket1.port = 9000 + listen_socket1.save! + + listen_socket2 = IcecastListenSocket.new + listen_socket2.port = 9001 + listen_socket2.save! + + template = IcecastTemplate.new + template.name = "Ubuntu-#{IcecastTemplate.count + 1}" + template.location = '@work' + template.admin_email = 'nobody@jamkazam.com' + template.fileserve = true + template.limit = limit + template.admin_auth = admin_auth + template.path = path + template.security = security + template.logging = logging + template.listen_sockets = [listen_socket1, listen_socket2] + template.save! + end + + redirect_to admin_bootstrap_path, :notice => "Ubuntu 12.04 template created. You should also install the icecast233 package: https://int.jamkazam.com/jenkins/job/icecast-debian/" + end + page_action :create_mount_template, :method => :post do IcecastServer.transaction do hostname = params[:jam_ruby_icecast_mount_template][:hostname] @@ -102,7 +168,7 @@ ActiveAdmin.register_page "Bootstrap" do auth.save! mount_template = IcecastMountTemplate.new - mount_template.name = "#{type}-#{IcecastMountTemplate.count}" + mount_template.name = "#{type}-#{IcecastMountTemplate.count + 1}" mount_template.source_username = nil # mount will override mount_template.source_pass = nil # mount will override mount_template.max_listeners = 20000 # huge @@ -143,6 +209,11 @@ ActiveAdmin.register_page "Bootstrap" do link_to "Create MacOSX (Brew) Template", admin_bootstrap_brew_template_path, :method => :post end + action_item do + link_to "Create Ubuntu 12.04 Template", admin_bootstrap_ubuntu_template_path, :method => :post + end + + content do if IcecastTemplate.count == 0 diff --git a/ruby/lib/jam_ruby/models/icecast_server.rb b/ruby/lib/jam_ruby/models/icecast_server.rb index 4cc3bd23d..69145e92b 100644 --- a/ruby/lib/jam_ruby/models/icecast_server.rb +++ b/ruby/lib/jam_ruby/models/icecast_server.rb @@ -4,7 +4,8 @@ module JamRuby attr_accessor :skip_config_changed_flag attr_accessible :template_id, :mount_template_id, :limit_id, :admin_auth_id, :directory_id, :master_relay_id, :path_id, :logging_id, - :security_id, :config_changed, :hostname, :location, :admin_email, :fileserve, as: :admin + :security_id, :config_changed, :hostname, :location, :admin_email, :fileserve, :icecast_server_group_id, as: :admin + belongs_to :template, class_name: "JamRuby::IcecastTemplate", foreign_key: 'template_id', inverse_of: :servers belongs_to :mount_template, class_name: "JamRuby::IcecastMountTemplate", foreign_key: 'mount_template_id', inverse_of: :servers diff --git a/ruby/lib/jam_ruby/models/invited_user.rb b/ruby/lib/jam_ruby/models/invited_user.rb index 385347d34..6481a6bdb 100644 --- a/ruby/lib/jam_ruby/models/invited_user.rb +++ b/ruby/lib/jam_ruby/models/invited_user.rb @@ -4,6 +4,7 @@ module JamRuby VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i attr_accessible :email, :sender_id, :autofriend, :note, as: :admin + attr_accessor :accepted_twice self.primary_key = 'id' diff --git a/web/config/application.rb b/web/config/application.rb index e6394999e..4da7d5464 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -171,7 +171,7 @@ include JamRuby config.audiomixer_path = "/var/lib/audiomixer/audiomixer/audiomixerapp" # if it looks like linux, use init.d script; otherwise use kill - config.icecast_reload_cmd = ENV['ICECAST_RELOAD_CMD'] || (File.exist?('/usr/bin/icecast2') ? '/etc/init.d/icecast2 reload' : "bash -l -c #{Shellwords.escape("kill -1 `ps -f | grep /usr/local/bin/icecast | grep -v grep | awk \'{print $2}\'`")}") + config.icecast_reload_cmd = ENV['ICECAST_RELOAD_CMD'] || (File.exist?('/usr/local/bin/icecast2') ? "bash -l -c #{Shellwords.escape("sudo /etc/init.d/icecast2 reload")}" : "bash -l -c #{Shellwords.escape("kill -1 `ps -f | grep /usr/local/bin/icecast | grep -v grep | awk \'{print $2}\'`")}") # if it looks like linux, use that path; otherwise use the brew default path config.icecast_config_file = ENV['ICECAST_CONFIG_FILE'] || (File.exist?('/etc/icecast2/icecast.xml') ? '/etc/icecast2/icecast.xml' : '/usr/local/etc/icecast.xml') # this will be the qualifier on the IcecastConfigWorker queue name