Merge branch 'develop' into feature/mobile

This commit is contained in:
Jonathan Kolyer 2015-10-13 17:34:12 +00:00
commit c700f856e5
49 changed files with 985 additions and 40 deletions

View File

@ -110,6 +110,7 @@ module JamAdmin
config.redis_host = "localhost:6379"
config.email_social_alias = 'social@jamkazam.com'
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_smtp_address = 'smtp.sendgrid.net'

View File

@ -46,4 +46,5 @@ JamAdmin::Application.configure do
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = 'alerts-dev@jamkazam.com'
config.email_social_alias = 'social-dev@jamkazam.com'
end

View File

@ -1 +1,3 @@
ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE user_authorizations ADD refresh_token VARCHAR;
ALTER TABLE recordings ADD external_video_id VARCHAR;

View File

@ -1,6 +1,6 @@
module JamRuby
# sends out a boring ale
class AdminMailer < ActionMailer::Base
class AdminMailer < ActionMailer::Base
include SendGrid
@ -20,6 +20,14 @@ module JamRuby
subject: options[:subject])
end
def social(options)
mail(to: APP_CONFIG.email_social_alias,
from: APP_CONFIG.email_generic_from,
body: options[:body],
content_type: "text/plain",
subject: options[:subject])
end
def recurly_alerts(user, options)
body = options[:body]

View File

@ -701,6 +701,10 @@ module JamRuby
self.save(:validate => false)
end
def add_video_data(data)
Recording.where(id: self.id).update_all(external_video_id: data[:video_id])
end
def add_timeline(timeline)
global = timeline["global"]
raise JamArgumentError, "global must be specified" unless global

View File

@ -1,7 +1,7 @@
module JamRuby
class UserAuthorization < ActiveRecord::Base
attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user
attr_accessible :provider, :uid, :token, :token_expiration, :secret, :user, :refresh_token
self.table_name = "user_authorizations"
@ -12,6 +12,36 @@ module JamRuby
validates_uniqueness_of :uid, scope: :provider
# token, secret, token_expiration can be missing
def self.refreshing_google_auth(user)
auth = self.where(:user_id => user.id)
.where(:provider => 'google_login')
.limit(1).first
# if we have an auth that will expire in less than 10 minutes
if auth && auth.refresh_token && auth.token_expiration < Time.now - 60 * 10
begin
oauth_client = OAuth2::Client.new(
Rails.application.config.google_client_id, Rails.application.config.google_secret,
:site => "https://accounts.google.com",
:token_url => "/o/oauth2/token",
:authorize_url => "/o/oauth2/auth")
access_token = OAuth2::AccessToken.from_hash(oauth_client, {:refresh_token => auth.refresh_token})
access_token = access_token.refresh!
auth.token = access_token.token
auth.token_expiration = Time.now + access_token.expires_in
auth.save
return auth
rescue Exception => e
# couldn't refresh; probably the user has revoked the app's rights
return nil
end
else
auth
end
end
def self.google_auth(user)
self
.where(:user_id => user.id)

View File

@ -7,6 +7,10 @@ def app_config
'http://localhost:3333'
end
def email_social_alias
'social@jamkazam.com'
end
def email_alerts_alias
'alerts@jamkazam.com'
end

View File

@ -41,12 +41,12 @@ gem 'eventmachine', '1.0.4'
gem 'faraday', '~>0.9.0'
gem 'amqp', '0.9.8'
gem 'logging-rails', :require => 'logging/rails'
gem 'omniauth', '1.1.1'
gem 'omniauth-facebook', '1.4.1'
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-google-oauth2', '0.2.1'
gem 'google-api-client', '0.7.1'
gem 'google-api-omniauth', '0.1.1'
gem 'omniauth-google-oauth2'
gem 'google-api-client' #, '0.7.1'
#gem 'google-api-omniauth' #, '0.1.1'
gem 'signet', '0.5.0'
gem 'twitter'
gem 'fb_graph', '2.5.9'

View File

@ -0,0 +1,69 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.DeleteVideoConfirmDialog = function (app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var recordingId = null;
var $dialog = null;
var $deleteFromDiskChkBox = null;
var $deleteBtn = null;
var deleting = false;
function resetForm() {
}
function beforeShow(args) {
recordingId = args.d1;
if(!recordingId) throw "recordingId must be specified";
$dialog.data('result', null);
deleting = false;
}
function afterHide() {
}
function attemptDelete() {
if(deleting) return;
deleting = true;
rest.deleteRecordingVideoData(recordingId)
.done(function(){
$dialog.data('result', true);
app.layout.closeDialog('delete-video-confirm-dialog');
})
.fail(app.ajaxError)
}
function events() {
$deleteBtn.click(attemptDelete);
}
function initialize() {
var dialogBindings = {
'beforeShow': beforeShow,
'afterHide': afterHide
};
app.bindDialog('delete-video-confirm-dialog', dialogBindings);
$dialog = $('#deleteVideoConfirmDialog');
$deleteFromDiskChkBox = $dialog.find('.delete-from-disk');
$deleteBtn = $dialog.find('.delete-btn');
events();
context.JK.checkbox($deleteFromDiskChkBox);
};
this.initialize = initialize;
}
})(window, jQuery);

View File

@ -14,7 +14,7 @@
var $isPublic = null;
var $saveBtn = null;
var $deleteBtn = null;
var $videoField = null;
var updating = false;
var deleting = false;

View File

@ -8,6 +8,8 @@
var playbackControls = null;
var recording = null; // deferred object
var $dialog = null;
var $saveVideoCheckbox = null
var $uploadToYoutube = null
function resetForm() {
// remove all display errors
@ -113,9 +115,15 @@
if(recording && recording.video) {
var name = $('#recording-finished-dialog form input[name=name]').val();
name = name.replace(/[^A-Za-z0-9\-\ ]/g, '');
var keep = $('#recording-finished-dialog form input[name=save_video]').is(':checked')
logger.debug("VideoDecision rid:" + recording.id + ", name=" + name + ", keep=" + keep)
context.jamClient.VideoDecision(recording.id, name, keep)
var saveToDisk = $('#recording-finished-dialog form input[name=save_video]').is(':checked')
var keepResult = $dialog.data('result');
keepResult = keepResult && keepResult.keep
logger.debug("VideoDecision rid:" + recording.id + ", name=" + name + ", keepResult=" + keepResult + ", saveToDisk=" + saveToDisk);
context.jamClient.VideoDecision(recording.id, name, keepResult && saveToDisk)
}
recording = null;
@ -156,6 +164,7 @@
window._oauth_callback = function() {
window._oauth_win.close()
logger.debug("closing window")
setGoogleAuthState()
}
return false;
@ -167,6 +176,8 @@
var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked')
upload_to_youtube = false // don't prevent user from getting through dialog because popup now handles auth
if (upload_to_youtube) {
$.ajax({
type: "GET",
@ -197,6 +208,7 @@
var save_video = $('#recording-finished-dialog form input[name=save_video]').is(':checked')
var upload_to_youtube = $('#recording-finished-dialog form input[name=upload_to_youtube]').is(':checked')
var recording_id = recording.id
rest.claimRecording({
id: recording.id,
name: name,
@ -210,6 +222,11 @@
$dialog.data('result', {keep:true});
app.layout.closeDialog('recordingFinished');
context.JK.GA.trackMakeRecording();
if(save_video && upload_to_youtube) {
// you have to have elected to save video to have upload to youtube have
context.VideoUploaderActions.showUploader(recording_id);
}
})
.fail(function (jqXHR) {
if (jqXHR.status == 422) {
@ -296,9 +313,9 @@
// Check for google authorization using AJAX and show/hide the
// google login button / "signed in" label as appropriate:
$(window).on('focus', function() {
/**$(window).on('focus', function() {
setGoogleAuthState();
});
}); */
}
function setGoogleAuthState() {
@ -333,10 +350,24 @@
}
function initializeButtons() {
$saveVideoCheckbox = $('#recording-finished-dialog input[name="save_video"]')
$uploadToYoutube = $('#recording-finished-dialog input[name="upload_to_youtube"]')
var isPublic = $('#recording-finished-dialog input[name="is_public"]');
context.JK.checkbox(isPublic);
context.JK.checkbox($('#recording-finished-dialog input[name="save_video"]'));
context.JK.checkbox($('#recording-finished-dialog input[name="upload_to_youtube"]'));
context.JK.checkbox($saveVideoCheckbox);
context.JK.checkbox($uploadToYoutube);
$saveVideoCheckbox.on('ifChanged', function() {
var saveVideoToDisk = $saveVideoCheckbox.is(':checked')
if(saveVideoToDisk) {
$uploadToYoutube.iCheck('enable')
}
else {
$uploadToYoutube.iCheck('disable')
}
})
}
function initialize() {

View File

@ -471,6 +471,43 @@
$controls.data('server-info', feed.mix) // for recordingUtils helper methods
$controls.data('view-context', 'feed')
// tack on video if available
if(feed.external_video_id) {
var $videoWrapper = $feedItem.find('.video-wrapper')
var $videoContainer = $feedItem.find('.video-container')
if(gon.isNativeClient) {
var $embed = $('<a class="video-thumbnail" href="https://www.youtube.com/watch?v=' + feed.external_video_id + '" rel="external">' +
'<img class="video" src="//img.youtube.com/vi/' + feed.external_video_id + '/hqdefault.jpg" />' +
'<img class="play" src="/assets/content/icon_youtube_play.png" /></a>')
$videoContainer.append($embed).addClass('no-embed')
$videoWrapper.removeClass('hidden')
$embed.click(function() {
context.JK.popExternalLink($(this).attr('href'))
return false;
})
}
else {
var $embed = $('<iframe src="//www.youtube.com/embed/' + feed.external_video_id + '" frameborder="0" allowfullscreen="allowfullscreen" />')
$videoContainer.append($embed)
$videoWrapper.removeClass('hidden')
}
if(feed.owner_id == context.JK.currentUserId) {
var $deleteVideoLink = $('<a href="#" class="delete-video">delete video</a>')
$deleteVideoLink.click(function() {
app.layout.showDialog('delete-video-confirm-dialog', { d1: feed.id }).one(EVENTS.DIALOG_CLOSED, function(e, data) {
if(!data.canceled && data.result) {
$deleteVideoLink.remove();
$videoWrapper.slideUp(1000);
}
})
return false;
})
$videoContainer.append($deleteVideoLink);
}
}
$('.timeago', $feedItem).timeago();
context.JK.prettyPrintElements($('.duration', $feedItem));
context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem));
@ -552,6 +589,9 @@
// put the feed item on the page
renderFeed($feedItem);
if(feed.external_video_id) {
toggleRecordingDetails.call($('.details', $feedItem).get(0))
}
// these routines need the item to have height to work (must be after renderFeed)
$controls.listenRecording({recordingId: feed.id, claimedRecordingId: options.candidate_claimed_recording.id, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'});
$controls.bind('statechange.listenRecording', stateChangeRecording);

View File

@ -2,6 +2,7 @@
"use strict";
// http://img.youtube.com/vi/x-Oas9Sc6s0/hqdefault.jpg
context.JK = context.JK || {};
context.JK.FeedItemRecording = function($parentElement, options){

View File

@ -507,6 +507,28 @@
})
}
function getUserAuthorizations(options) {
var id = getId(options);
return $.ajax({
type: "GET",
dataType: "json",
url: "/api/users/" + id + '/authorizations',
processData: false
});
}
function getGoogleAuth(options) {
var id = getId(options);
return $.ajax({
type: "GET",
dataType: "json",
url: "/api/users/authorizations/google",
processData: false
});
}
function getUserDetail(options) {
var id = getId(options);
var detail = null;
@ -1888,6 +1910,26 @@
});
}
function addRecordingVideoData(recordingId, data) {
return $.ajax({
type: "POST",
url: '/api/recordings/' + recordingId + '/video_data',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(data),
});
}
function deleteRecordingVideoData(recordingId) {
return $.ajax({
type: "DELETE",
url: '/api/recordings/' + recordingId + '/video_data',
dataType: "json",
contentType: 'application/json'
});
}
function createSignupHint(data) {
return $.ajax({
type: "POST",
@ -1949,6 +1991,8 @@
this.cancelSession = cancelSession;
this.updateScheduledSession = updateScheduledSession;
this.getUserDetail = getUserDetail;
this.getUserAuthorizations = getUserAuthorizations;
this.getGoogleAuth = getGoogleAuth;
this.getUserProfile = getUserProfile;
this.getAffiliatePartnerData = getAffiliatePartnerData;
this.postAffiliatePartnerData = postAffiliatePartnerData;
@ -2099,6 +2143,8 @@
this.validateUrlSite = validateUrlSite;
this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent;
this.addRecordingTimeline = addRecordingTimeline;
this.addRecordingVideoData = addRecordingVideoData;
this.deleteRecordingVideoData = deleteRecordingVideoData;
this.getMusicianSearchFilter = getMusicianSearchFilter;
this.postMusicianSearchFilter = postMusicianSearchFilter;
this.getBandSearchFilter = getBandSearchFilter;

View File

@ -15,6 +15,7 @@
//= require ./react-components/stores/SessionOtherTracksStore
//= require ./react-components/stores/SessionMediaTracksStore
//= require ./react-components/stores/PlatformStore
//= require ./react-components/stores/VideoUploaderStore
//= require_directory ./react-components/stores
//= require_directory ./react-components/mixins
//= require_directory ./react-components

View File

@ -78,7 +78,7 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged'))
monitorControls: (controls, mediaSummary, jamTrackState) ->
if mediaSummary.mediaOpen || mediaSummary.jamTrack? || jamTrackState?.jamTrack?
if mediaSummary.jamTrackOpen? || mediaSummary.jamTrack? || jamTrackState?.jamTrack?
if mediaSummary.jamTrackOpen || mediaSummary.jamTrack? || jamTrackState?.jamTrack?
controls.startMonitor(PLAYBACK_MONITOR_MODE.JAMTRACK)
else if mediaSummary.backingTrackOpen
controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE)

View File

@ -55,7 +55,7 @@ if accessOpener
<span className="field">
<input type="checkbox" name="dont_show" /><label htmlFor="dont_show">Don't show this again</label>
</span>
<a className="button-orange close-link" onClick={this.close}>CLOSE</a>
<a className="button-orange close-link" onClick={this.close}>CLOSE</a>
</div>
</div>`

View File

@ -0,0 +1,209 @@
context = window
logger = context.JK.logger
rest = context.JK.Rest()
NoVideoRecordActive = 0
WebCamRecordActive = 1
ScreenRecordActive = 2
mixins = []
# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
# this check ensures we attempt to listen if this component is created in a popup
reactContext = if window.opener? then window.opener else window
accessOpener = false
if window.opener?
try
m = window.opener.MixerStore
accessOpener = true
catch e
reactContext = window
VideoUploaderStore = reactContext.VideoUploaderStore
VideoUploaderActions = reactContext.VideoUploaderActions
AppActions = reactContext.AppActions
if accessOpener
mixins.push(Reflux.listenTo(VideoUploaderStore,"onVideoUploadChanged"))
@PopupVideoUploader = React.createClass({
mixins: mixins
onVideoUploadChanged: (uploadState) ->
this.setState({uploadState: uploadState})
render: () ->
# there are a few initial states: if auth is null, we don't know yet (and are checking against the server)
# if auth is set, then we can show the upload btn
# and if auth == false, then we need the user to try and log in
if @state.recording
name = @state.recording.my?.name
description = @state.recording.my?.description
created = @state.recording.created_at
if @state.deleteMe
action = `<div><a className="button-grey close-btn" onClick={this.onCloseRequested}>CANCEL</a> <a className="button-orange close-btn" onClick={this.onDeleteVideo}>DELETE</a></div>`
instructions = `<p>Save space and delete your video from your computer?</p>`
else if @state.recording.video_url?
action = `<a className="button-orange close-btn" onClick={this.onCloseRequested}>CLOSE</a>`
instructions = `<p>Your video has been uploaded previously. <a className="video-url" onClick={this.watchVideo} href={this.state.recording.video_url}>{this.state.uploadState.videoUrl}</a></p>`
else if @state.uploadState.errorReason
action = `<a className="button-orange upload-btn" onClick={this.onUploadRequested}>UPLOAD</a>`
instructions = `<p>The upload failed. You can try again. ({this.state.uploadState.errorReason})</p>`
else if @state.uploadState.done
action = `<a className="button-orange close-btn" onClick={this.onNextToDeletion}>OK</a>`
instructions = `<p>Your video has been uploaded successfully. <a className="video-url" onClick={this.watchVideo} href={this.state.uploadState.videoUrl}>{this.state.uploadState.videoUrl}</a></p>`
else if @state.uploadState.paused
action = `<a className="button-orange resume-btn" onClick={this.onResumeRequested}>RESUME</a>`
instructions = `<p>Press the RESUME button at any time to continue uploading your video.</p>`
else if @state.uploadState.uploading
action = `<div><a className="button-grey cancel-btn" onClick={this.onCancelRequested}>CANCEL</a> <a className="button-grey pause-btn" onClick={this.onPauseRequested}>PAUSE</a></div>`
width = ((@state.uploadState.bytesSent / @state.uploadState.bytesTotal) * 100)
progressWidth = width.toString() + "%";
progressStyle = {width:progressWidth}
rounded = Math.round(width)
instructions = `<p>Press the PAUSE button at any time to pause the upload of your video, or CANCEL to end this upload completely.
<div className="progress-bar" style={progressStyle}><div className="percentage-progress">{rounded}%</div></div>
</p>`
else if @state.auth == false
action = `<input className="google_login_button" type='image' src="/assets/google_signin.png" height="30px" onClick={this.startGoogleLogin}/>`
instructions = `<p>To upload this recording to YouTube, you must give JamKazam the necessary access to your YouTube account by clicking the button below.</p>`
else if @state.auth?
action = `<a className="button-orange upload-btn" onClick={this.onUploadRequested}>UPLOAD</a>`
instructions = `<p>Press the UPLOAD button to start uploading your video to YouTube. When the upload is done, a link to your video will appear.</p>`
else
name = null
action = null
else
name = null
action = null
`<div className="video-uploader">
<h3>Upload Video<br/>"{name}"</h3>
<h3></h3>
{instructions}
<div className="control-holder">
{action}
</div>
</div>`
getInitialState: () ->
{auth: null, uploadState: VideoUploaderStore.getState(), deleteMe: false}
watchVideo: (e) ->
e.preventDefault()
$link = $(e.target)
AppActions.openExternalUrl($link.attr('href'))
onNextToDeletion: (e) ->
e.preventDefault();
@setState({deleteMe: true})
onCloseRequested: (e) ->
e.preventDefault()
window.close()
onDeleteVideo: (e) ->
e.preventDefault();
VideoUploaderActions.delete(gon.recording_id)
window.close()
startGoogleLogin: (e) ->
e.preventDefault()
logger.debug("Starting google login")
window._oauth_win = window.open("/auth/google_login", "Log In to Google", "height=700,width=500,menubar=no,resizable=no,status=no");
window._oauth_callback = @oauthCallback
oauthCallback: () ->
window._oauth_win.close()
@checkAuth()
onUploadRequested: (e) ->
e.preventDefault()
VideoUploaderActions.uploadVideo(gon.recording_id)
onResumeRequested: (e) ->
e.preventDefault()
VideoUploaderActions.resume(gon.recording_id)
onPauseRequested: (e) ->
e.preventDefault()
VideoUploaderActions.pause(gon.recording_id)
onCancelRequested: (e) ->
e.preventDefault()
VideoUploaderActions.cancel(gon.recording_id)
checkAuth:() ->
rest.getGoogleAuth()
.done((auth) =>
if auth.auth?
@setState({auth: auth.auth})
else
@setState({auth: false})
)
.fail(() =>
@setState({errorReason: 'Unable to fetch user authorization'})
)
checkRecording:() ->
rest.getRecording({id: gon.recording_id})
.done((response) =>
logger.debug("recording info fetched #{response}")
@setState({recording:response})
)
.fail((jqXHR) =>
logger.debug("recording info fetch failed #{jqXHR.responseText}")
@setState({errorReason: 'Unable to fetch recording info'})
)
componentDidMount: () ->
$(window).unload(@windowUnloaded)
@checkAuth()
@checkRecording()
VideoUploaderActions.newVideo(gon.recording_id)
@resizeWindow()
# this is necessary due to whatever the client's rendering behavior is.
setTimeout(@resizeWindow, 300)
componentDidUpdate: () ->
@resizeWindow()
setTimeout(@resizeWindow, 1000)
resizeWindow: () =>
$container = $('#minimal-container')
width = $container.width()
height = $container.height()
# there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container)
#mysteryTopMargin = 20
mysteryTopMargin = 0
# deal with chrome in real browsers
offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin
# handle native client chrome that the above outer-inner doesn't catch
#if navigator.userAgent.indexOf('JamKazam') > -1
#offset += 25
width = 100 if width < 100
height = 100 if height < 100
window.resizeTo(width, height + offset)
windowUnloaded: () ->
VideoUploaderActions.uploaderClosed()
})

View File

@ -0,0 +1,12 @@
context = window
@VideoUploaderActions = Reflux.createActions({
uploadVideo: {},
pause: {},
resume: {},
cancel: {},
newVideo: {},
showUploader: {},
uploaderClosed: {}
delete: {}
})

View File

@ -2,6 +2,15 @@ $ = jQuery
context = window
logger = context.JK.logger
BackendToFrontendFPS = {
0: 30,
1: 24,
2: 20,
3: 15,
4: 10
}
@RecordingStore = Reflux.createStore(
{
listenables: @RecordingActions
@ -25,7 +34,9 @@ logger = context.JK.logger
onStartRecording: (recordVideo, recordChat) ->
frameRate = context.jamClient.GetCurrentVideoFrameRate() || 30;
frameRate = context.jamClient.GetCurrentVideoFrameRate() || 0;
frameRate = BackendToFrontendFPS[frameRate]
NoVideoRecordActive = 0
WebCamRecordActive = 1
@ -50,14 +61,23 @@ logger = context.JK.logger
onStoppingRecording: (details) ->
details.cause = 'stopping'
this.trigger(details)
onStoppedRecording: (details) ->
details.cause = 'stopped'
if @recordingWindow?
@recordingWindow.close()
this.trigger(details)
onAbortedRecording: (details) ->
details.cause = 'aborted'
if @recordingWindow?
@recordingWindow.close()
this.trigger(details)
onOpenRecordingControls: () ->

View File

@ -0,0 +1,219 @@
$ = jQuery
context = window
logger = context.JK.logger
rest = context.JK.Rest();
VideoUploaderActions = @VideoUploaderActions
@VideoUploaderStore = Reflux.createStore(
{
listenables: VideoUploaderActions
logger: context.JK.logger
preparingUpload: false
uploading: false
paused: false
recordingId: null
videoUrl: null
bytesSent: 0
bytesTotal: 1
errorReason: null
errorDetail: null
state: null
done: false
init: ->
# Register with the app store to get @app
this.listenTo(context.AppStore, this.onAppInit)
@state = {uploading: @uploading, recordingId: @recordingId, videoUrl: @videoUrl, paused: @paused, preparingUpload: @preparingUpload, bytesSent: @bytesSent, bytesTotal: @bytesTotal, errorReason: @errorReason, errorDetail: @errorDetail, done: @done }
onAppInit: (app) ->
@app = app
triggerState: () ->
@state = {uploading: @uploading, recordingId: @recordingId, videoUrl: @videoUrl, paused: @paused, preparingUpload: @preparingUpload, bytesSent: @bytesSent, bytesTotal: @bytesTotal, errorReason: @errorReason, errorDetail: @errorDetail, done: @done}
@trigger(@state)
getState: () ->
@state
onPause: () ->
if @uploading
@uploading = false
@paused = true
context.jamClient.pauseVideoUpload()
@triggerState()
onResume: () ->
if @paused && @recordingId?
@uploading = true
@paused = false
context.jamClient.resumeVideoUpload()
@triggerState()
else
if @uploading
@app.layout.notify({title: 'Already uploading', text: "A video is already being uploaded."})
else
@app.layout.notify({title: 'Nothing to resume', text: "No upload to resume."})
onCancel: () ->
if @uploading
@uploading = false
context.jamClient.cancelVideoUpload()
@triggerState()
onDelete: (recordingId) ->
context.jamClient.deleteVideo(recordingId);
onNewVideo: (recordingId) ->
@onCancel(recordingId)
@done = false
@paused = false
@bytesSent = 0
@bytesTotal = 1
@errorReason = null
@errorDetail = null
@videoUrl = null
@triggerState()
onShowUploader: (recordingId) ->
if @childWindow?
logger.debug("showUploader popup being closed automatically")
@childWindow.close()
@childWindow = null
@childWindow = window.open("/popups/video/upload/" + recordingId, 'Video Uploader', 'scrollbars=yes,toolbar=no,status=no,height=155,width=350')
onUploaderClosed: () ->
if @childWindow?
@childWindow = null
onUploadVideo: (recordingId) ->
if @uploading || @preparingUpload
logger.debug("ignoring upload request")
return
@preparingUpload = true
@onNewVideo()
rest.getRecording({id:recordingId})
.done((response) =>
claim = response.my
privateStatus = 'private'
privateStatus = 'public' if claim.is_public
if claim?
videoInfo = {
"snippet": {
"title": claim.name,
"description": claim.description,
"tags": ["JamKazam"],
"categoryId": 10 # music
},
"status": {
"privacyStatus": privateStatus,
"embeddable": true,
"license": "youtube"
}
}
rest.getUserAuthorizations()
.done((response) =>
# http://localhost:3000/popups/video/upload/d25dbe8e-a066-4ea0-841d-16872c713fc9
youtube_auth = null
for authorization in response.authorizations
if authorization.provider == 'google_login'
youtube_auth = authorization.token
break
if youtube_auth?
logger.debug("calling uploadVideo(#{recordingId}, #{youtube_auth}, #{videoInfo})")
result = context.jamClient.uploadVideo(recordingId, youtube_auth, JSON.stringify(videoInfo),
"VideoUploaderStore.clientUploadCallback",
"VideoUploaderStore.clientDoneCallback",
"VideoUploaderStore.clientFailCallback")
if result.error
@preparingUpload = false
@triggerState()
@app.layout.notify({title: 'Unable to upload video', text: 'Application error: ' + result.error})
else
@preparingUpload = false
@videoUrl = null
@uploading = true
@recordingId = recordingId
@triggerState()
else
@preparingUpload = false
@triggerState()
@app.layout.notify({title: 'No Authorization Yet for YouTube', text: 'Youtube authorization still needed'})
)
.fail((jqXHR) =>
@preparingUpload = false
@triggerState()
@app.layout.notifyServerError(jqXHR, 'Unable to fetch user authorizations')
)
else
@preparingUpload = false
@triggerState()
@app.layout.notify({title: "You do not have a claim to this recording", text: "If this is in error, contact support@jamkazam.com."})
)
.fail((jqXHR) =>
@preparingUpload = false
@triggerState()
@app.layout.notifyServerError(jqXHR, 'Unable to fetch recording information')
)
clientUploadCallback: (bytesSent, bytesTotal) ->
logger.debug("bytesSent: #{bytesSent} bytesTotal: #{bytesTotal}")
# backend will report 0 bytes total sometimes as the upload is failing. just ignore it; we'll get an error message soon
return if bytesTotal == 0
VideoUploaderStore.bytesSent = Number(bytesSent)
VideoUploaderStore.bytesTotal = Number(bytesTotal)
VideoUploaderStore.triggerState()
clientDoneCallback: (video_id) ->
console.log
logger.debug("client uploaded video successfully to #{video_id}")
VideoUploaderStore.uploading = false
VideoUploaderStore.videoUrl = "https://www.youtube.com/watch?v=#{video_id}"
rest.addRecordingVideoData(VideoUploaderStore.recordingId, {video_id: video_id})
.fail(() =>
VideoUploaderStore.app.layout.notify({title: 'Sync Error', text:'Unable to notify server about uploaded video'})
)
VideoUploaderStore.recordingId = null
VideoUploaderStore.done = true
VideoUploaderStore.triggerState()
clientFailCallback: (reason, detail) =>
logger.warn("client failed to video upload #{reason}, #{detail}")
VideoUploaderStore.uploading = false
VideoUploaderStore.errorReason = reason
VideoUploaderStore.errorDetail = detail
# if reason == "create_video_failed" && errorDetail = "401"
# then don't trigger state, instead ask server for a fresh token
#
VideoUploaderStore.triggerState()
}
)
@VideoUploaderStore

View File

@ -271,6 +271,13 @@
}
function handleRecordingStopped(recordingId, result) {
if(recordingId == "video") {
return;
}
logger.debug("handleRecordingStopped " + recordingId, result)
var success = result.success;
var reason = result.reason;
var detail = result.detail;
@ -315,6 +322,11 @@
}
function handleRecordingAborted(recordingId, result) {
if(recordingId == "video") {
return;
}
var success = result.success;
var reason = result.reason;
var detail = result.detail;

View File

@ -815,6 +815,14 @@ context.JK.SyncViewer = class SyncViewer
$uploadState.addClass('is-native-client') if gon.isNativeClient
$track
openVideo: (e) =>
$link = $(e.target)
recordingId = $link.closest('.details').attr('data-recording-id')
if !context.jamClient.openVideo(recordingId)
@app.layout.notify({title: 'No Video Found', text: 'Did you delete this recording earlier from your computer?'})
return false
exportRecording: (e) =>
$export = $(e.target)
if context.SessionStore.inSession()
@ -899,6 +907,10 @@ context.JK.SyncViewer = class SyncViewer
# create a virtual mix so that the UI is consistent
$wrapper.append(this.createMix('fake', recordingInfo))
if recordingInfo.video
$wrapper.find('a.open-video').click(this.openVideo).removeClass('hidden')
$wrapper.find('a.export').click(this.exportRecording)
$wrapper.find('a.delete').click(this.deleteRecording)

View File

@ -0,0 +1,13 @@
@import "client/common";
#deleteVideoConfirmDialog {
min-height: 180px;
width: 400px;
.buttons {
float:right;
clear:both;
margin-top:20px;
}
}

View File

@ -0,0 +1,58 @@
@import "client/common";
body.video-upload {
position: relative;
color: $ColorTextTypical;
#minimal-container {
padding-bottom: 20px;
height:240px;
}
.video-uploader {
padding-left: 30px;
padding-right:30px;
}
h3 {
margin-top:20px;
font-size:16px;
font-weight:bold;
margin-bottom:20px;
text-align:center;
line-height:125%;
}
.control-holder {
margin: 20px 0 20px;
text-align:center;
padding-bottom:20px;
position: absolute;
bottom: 0;
width: 100%;
left: 0;
}
.progress-bar {
background-color:#ED3618;
border:solid 1px #000;
height:20px;
display:block;
@include border_box_sizing;
margin:20px 0;
position:relative;
}
.percentage-progress {
position:absolute;
right:-32px;
}
.video-url {
text-align:center;
display:block;
margin:20px 0;
}
}

View File

@ -192,7 +192,7 @@
@include border_box_sizing;
@include vertical-align-column;
.export {
.export, .open-video {
float:right;
margin-right:10px;
font-size:12px;

View File

@ -8,6 +8,46 @@
position:relative;
}
.feed-entry {
.video-container {
width: 100%;
padding-bottom: 53.33%;
margin: 30px 0 10px;
&.no-embed {
padding-bottom:0;
}
.recording-controls {
width:100%;
}
}
.delete-video {
display:block;
text-align:center;
font-size:12px;
text-decoration:underline;
}
.video-thumbnail {
width:100%;
//padding-top:53.33%;
//margin:25px 0;
@include border_box_sizing;
position:relative;
img.video {
width:100%;
}
img.play {
position:absolute;
width:15%;
left: 42.5%;
}
}
}
.landing-details .recording-controls, .landing-details .session-controls {
background-color:#242323;

View File

@ -1,7 +1,7 @@
class ApiRecordingsController < ApiController
before_filter :api_signed_in_user, :except => [ :add_like ]
before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim, :add_timeline ]
before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim, :add_timeline, :add_video_data, :delete_video_data ]
before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ]
before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ]
@ -384,6 +384,32 @@ class ApiRecordingsController < ApiController
render :json => {}, :status => 200
end
def add_video_data
@recording.add_video_data(params)
video_id = params[:video_id]
video_url = "https://www.youtube.com/watch?v=#{video_id}"
body = ""
body << "Youtube URL: #{video_url}\n\n"
body << "User: " + current_user.admin_url + "\n\n"
body << "Recording Landing: #{recording_detail_url(@recording.id)}\n"
AdminMailer.alerts({
subject:"Video Uploaded by #{current_user.name}",
body:body
}).deliver
render :json => {}, :status => 200
end
def delete_video_data
@recording.add_video_data({:video_id => nil})
render :json => {}, :status => 200
end
private
def lookup_recording

View File

@ -2,8 +2,8 @@ require 'sanitize'
class
ApiUsersController < ApiController
before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete,
before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data, :google_auth]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :authorizations,
:liking_create, :liking_destroy, # likes
:following_create, :following_show, :following_destroy, # followings
:recording_update, :recording_destroy, # recordings
@ -36,6 +36,17 @@ ApiUsersController < ApiController
respond_with @user, responder: ApiResponder, :status => 200
end
def authorizations
@user = current_user
respond_with @user, responder: ApiResponder, :status => 200
end
def google_auth
@user = current_user
respond_with @user, responder: ApiResponder, :status => 200
end
def profile_show
@profile = User.includes([{musician_instruments: :instrument},
{band_musicians: :user},

View File

@ -22,4 +22,10 @@ class PopupsController < ApplicationController
@video_id = params[:id]
render :layout => "minimal"
end
def video_upload
@recording_id = params[:recording_id]
gon.recording_id = @recording_id
render :layout => "minimal"
end
end

View File

@ -88,7 +88,9 @@ class SessionsController < ApplicationController
provider = auth_hash[:provider]
if provider == 'twitter'
if provider == 'google_login'
elsif provider == 'twitter'
@user_authorization = current_user.build_twitter_authorization(auth_hash)
if !@user_authorization.save
# this is a very poorly styled page, but it's better than a server error.
@ -164,22 +166,31 @@ class SessionsController < ApplicationController
return
end
#authorization = UserAuthorization.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
authorization = UserAuthorization.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
# Always make and save a new authorization. This is because they expire, and honestly there's no cost
# to just making and saving it.
user_auth_hash = {
:provider => auth_hash[:provider],
:uid => auth_hash[:uid],
:token => auth_hash[:credentials][:token],
:token => auth_hash[:credentials][:token],
:refresh_token => auth_hash[:credentials][:refresh_token],
:token_expiration => Time.at(auth_hash[:credentials][:expires_at]),
:secret => auth_hash[:credentials][:secret]
}
#if authorization.nil?
if authorization.nil?
authorization = current_user.user_authorizations.build(user_auth_hash)
authorization.save
#end
else
authorization.token = auth_hash[:credentials][:token]
authorization.token_expiration = Time.at(auth_hash[:credentials][:expires_at])
authorization.save
end
render 'oauth_complete', :layout => "landing"
end

View File

@ -20,7 +20,7 @@ node :mix do |claimed_recording|
end
child(:recording => :recording) {
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?, :mix_state, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?, :mix_state, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :video
node :timeline do |recording|
recording.timeline ? JSON.parse(recording.timeline) : {}

View File

@ -78,7 +78,7 @@ glue :recording do
'recording'
end
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?, :mix_state, :when_will_be_discarded?
attributes :id, :band, :owner_id, :created_at, :duration, :comment_count, :like_count, :play_count, :has_mix?, :mix_state, :when_will_be_discarded?, :video, :external_video_id
node do |recording|
{

View File

@ -1,6 +1,6 @@
object @recording
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :music_session_id, :music_session, :video
attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :music_session_id, :music_session, :video, :external_video_id
node :fan_access do |recording|
recording.non_active_music_session.fan_access
@ -69,7 +69,7 @@ child(:comments => :comments) {
node :my do |recording|
claim = recording.claim_for_user(current_user) || recording.candidate_claimed_recording
if claim
{ name: claim.name, description: claim.description, genre: claim.genre.id, genre_name: claim.genre.description, id: claim.id }
{ name: claim.name, description: claim.description, genre: claim.genre.id, genre_name: claim.genre.description, id: claim.id, is_public: claim.is_public }
else
nil
end

View File

@ -0,0 +1,6 @@
object @user
child :user_authorizations => :authorizations do
attributes :uid, :provider, :token, :secret
end

View File

@ -0,0 +1,16 @@
object @user
node :auth do
auth = UserAuthorization.refreshing_google_auth(@user)
if auth
{
uid: auth.uid,
provider: auth.provider,
token: auth.token,
secret: auth.secret
}
else
nil
end
end

View File

@ -117,6 +117,8 @@ script type="text/template" id="template-sync-viewer-recording-wrapper-details"
| DELETE
a.export href="#"
| EXPORT
a.open-video.hidden href="#"
| VIDEO
script type="text/template" id="template-sync-viewer-hover-recorded-track"
.help-hover-recorded-tracks

View File

@ -159,6 +159,9 @@
var editRecordingDialog = new JK.EditRecordingDialog(JK.app);
editRecordingDialog.initialize();
var deleteVideoDialog = new JK.DeleteVideoConfirmDialog(JK.app);
deleteVideoDialog.initialize();
var recordingFinishedDialog = new JK.RecordingFinishedDialog(JK.app);
recordingFinishedDialog.initialize();
JK.recordingFinishedDialog = recordingFinishedDialog

View File

@ -0,0 +1,15 @@
#deleteVideoConfirmDialog.dialog layout='dialog' layout-id='delete-video-confirm-dialog'
.content-head
= image_tag "content/icon_alert.png", {:width => 24, :height => 24, :class => 'content-icon' }
h1 Delete video from JamKazam?
.dialog-inner
.dialog-info
| This will delete this video from JamKazam, but not from YouTube. To delete this video from YouTube, you must go to your YouTube account as we cannot do this for you.
//.delete-from-disk
// input.delete-from-disk type="checkbox" value="delete-from-disk"
.buttons
.left
a.button-grey.cancel-btn layout-action='cancel' CANCEL
.right
a.button-orange.delete-btn DELETE
br clear='all'

View File

@ -43,3 +43,4 @@
= render 'dialogs/genreSelectorDialog'
= render 'dialogs/recordingSelectorDialog'
= render 'dialogs/soundCloudPlayerDialog'
= render 'dialogs/deleteVideoConfirmDialog'

View File

@ -26,14 +26,12 @@
%textarea#claim-recording-description.w100{:name => "description"}
-if (Rails.application.config.video_available=="full") || (current_user && current_user.admin)
.save-video.field.left{:purpose => "save_video"}
%input{:name => "save_video", :type => "checkbox"}/
%input{:name => "save_video", :type => "checkbox", :checked => "checked"}/
%label{:for => "save_video"} Save Video to Computer
.hidden.field.left{:purpose => "upload_to_youtube"}
.field.left{:purpose => "upload_to_youtube"}
%span
%input{:name => "upload_to_youtube", :type => "checkbox", :checked => "checked"}/
%label{:for => "upload_to_youtube"} Upload Video to YouTube
%span
= render(:partial => "shared/google_login")
.field.left{:purpose => "is_public"}
%input{:checked => "checked", :name => "is_public", :type => "checkbox"}/
%label{:for => "is_public"} Public Recording

View File

@ -0,0 +1,3 @@
- provide(:page_name, 'video-upload popup')
- provide(:title, 'Video Upload')
= react_component 'PopupVideoUploader', {}

View File

@ -1,5 +1,7 @@
<% provide(:description, 'Finished authorization') %>
This window will close momentarily and you can then continue.
<script>
window.opener._oauth_callback();
</script>

View File

@ -106,6 +106,9 @@
= '{% } %}'
= '{% }) %}'
.video-wrapper.hidden
.video-container
%br{:clear => "all"}/
%br/

View File

@ -239,6 +239,7 @@ if defined?(Bundler)
config.estimated_slow_mixdown_time = 80
config.num_packaging_nodes = 2
config.email_social_alias = 'social@jamkazam.com'
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_recurly_notice = 'recurly-alerts@jamkazam.com'

View File

@ -95,6 +95,7 @@ SampleApp::Application.configure do
config.video_available= ENV['VIDEO_AVAILABILITY'] || "full"
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = ENV['ALERT_EMAIL'] || 'alerts-dev@jamkazam.com'
config.email_social_alias = ENV['ALERT_EMAIL'] || 'social-dev@jamkazam.com'
config.guard_against_fraud = true
config.react.variant = :development

View File

@ -1,6 +1,8 @@
Rails.application.config.middleware.use OmniAuth::Builder do
provider :facebook, Rails.application.config.facebook_app_id, Rails.application.config.facebook_app_secret, {name: "facebook", :scope => 'email,user_location'}
provider :google_oauth2, Rails.application.config.google_client_id, Rails.application.config.google_secret, {name: "google_login", approval_prompt: '', scope: 'userinfo.email, userinfo.profile, https://www.google.com/m8/feeds'}
# add these back later if needed
# userinfo.email, userinfo.profile, https://www.google.com/m8/feeds,
provider :google_oauth2, Rails.application.config.google_client_id, Rails.application.config.google_secret, {name: "google_login", prompt: 'consent', scope: 'userinfo.email, https://www.googleapis.com/auth/youtube.upload, https://www.googleapis.com/auth/youtube'}
provider :twitter, Rails.application.config.twitter_app_id, Rails.application.config.twitter_app_secret, {x_auth_access_type: 'write' }
end

View File

@ -148,6 +148,7 @@ SampleApp::Application.routes.draw do
match '/youtube/player', to: 'popups#youtube_player'
match '/how-to-use-video', to: 'popups#how_to_use_video'
match '/configure-video', to: 'popups#configure_video'
match '/video/upload/:recording_id', to: 'popups#video_upload'
end
scope '/corp' do
@ -194,7 +195,7 @@ SampleApp::Application.routes.draw do
# music sessions
match '/sessions/:id/participants/legacy' => 'api_music_sessions#participant_create_legacy', :via => :post # can be removed when new Create Session comes in
match '/sessions/:id/participants' => 'api_music_sessions#participant_create', :via => :post
match '/participants/:id' => 'api_music_sessions#participant_show', :via => :get, :as => 'api_session_participant_detail'
match '/participants/:id' => 'api_music_sessions#participant_show', :via => :tt, :as => 'api_session_participant_detail'
match '/participants/:id' => 'api_music_sessions#participant_delete', :via => :delete
match '/sessions/scheduled' => 'api_music_sessions#scheduled', :via => :get
match '/sessions/scheduled_rsvp' => 'api_music_sessions#scheduled_rsvp', :via => :get
@ -298,12 +299,14 @@ SampleApp::Application.routes.draw do
match '/users' => 'api_users#index', :via => :get
match '/users' => 'api_users#create', :via => :post
match '/users/:id' => 'api_users#show', :via => :get, :as => 'api_user_detail'
match '/users/:id/authorizations' => 'api_users#authorizations', :via => :get
#match '/users' => 'api_users#create', :via => :post
match '/users/:id' => 'api_users#update', :via => :post
match '/users/:id' => 'api_users#delete', :via => :delete
match '/users/:id/calendar.ics' => 'api_users#calendar', :via => :get, :as => 'api_users_calendar_feed'
match '/users/confirm/:signup_token' => 'api_users#signup_confirm', :via => :post, :as => 'api_signup_confirmation'
match '/users/complete/:signup_token' => 'api_users#complete', as: 'complete', via: 'post'
match '/users/authorizations/google' => 'api_users#google_auth', :via => :get
match '/users/:id/set_password' => 'api_users#set_password', :via => :post
# recurly
@ -525,6 +528,8 @@ SampleApp::Application.routes.draw do
match '/recordings/:id/likes' => 'api_recordings#add_like', :via => :post, :as => 'api_recordings_add_like'
match '/recordings/:id/discard' => 'api_recordings#discard', :via => :post, :as => 'api_recordings_discard'
match '/recordings/:id/timeline' => 'api_recordings#add_timeline', :via => :post, :as => 'api_recordings_timeline'
match '/recordings/:id/video_data' => 'api_recordings#add_video_data', :via => :post, :as => 'api_recordings_video_data'
match '/recordings/:id/video_data' => 'api_recordings#delete_video_data', :via => :delete, :as => 'api_recordings_video_data_delete'
# Recordings - recorded_tracks
match '/recordings/:id/tracks/:track_id' => 'api_recordings#show_recorded_track', :via => :get, :as => 'api_recordings_show_recorded_track'

View File

@ -882,9 +882,9 @@ module JamWebsockets
end
def runaway_heartbeat(heartbeat, context)
heartbeat_count = @heartbeat_tracker[context.user.id] || 0
heartbeat_count = @heartbeat_tracker[context.client.client_id] || 0
heartbeat_count += 1
@heartbeat_tracker[context.user.id] = heartbeat_count
@heartbeat_tracker[context.client.client_id] = heartbeat_count
if heartbeat_count > (context.client_type == 'browser' ? @maximum_minutely_heartbeat_rate_browser : @maximum_minutely_heartbeat_rate_client)
@log.warn("user #{context.user} sending too many heartbeats: #{heartbeat_count}") if heartbeat_count % 100 == 0