diff --git a/Gemfile b/Gemfile index 3f9c8256..6fd584d2 100644 --- a/Gemfile +++ b/Gemfile @@ -82,3 +82,9 @@ gem 'slack-notifier' # For landing background image uploading. gem 'paperclip', '~> 4.2' + +# For uploading recordings to Youtube. +gem 'yt', '~> 0.28.0' + +# Simple HTTP client. +gem 'faraday' diff --git a/Gemfile.lock b/Gemfile.lock index 5544207c..4178690f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -229,6 +229,8 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) xml-simple (1.1.5) + yt (0.28.5) + activesupport PLATFORMS ruby @@ -240,6 +242,7 @@ DEPENDENCIES byebug coffee-rails (~> 4.2) dotenv-rails + faraday font-awesome-sass http_accept_language jbuilder (~> 2.5) @@ -267,6 +270,7 @@ DEPENDENCIES tzinfo-data uglifier (>= 1.3.0) web-console + yt (~> 0.28.0) RUBY VERSION ruby 2.3.4p301 diff --git a/README.md b/README.md index a90afdf2..4bf0dd8e 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ For a overview of how GreenLight works, see the following video ## Installation on the BigBlueButton server -We designed GreenLight to install on a [BigBlueButton 1.1-beta](http://docs.bigbluebutton.org/1.1/install.html) (or later) server. This means you don't need a separate server to run GreenLight. +We designed GreenLight to install on a [BigBlueButton 1.1-beta](http://docs.bigbluebutton.org/install/green-light.html) (or later) server. This means you don't need a separate server to run GreenLight. -For more informaiton see [Installing GreenLight](http://docs.bigbluebutton.org/1.1/green-light.html). +For more informaiton see [Installing GreenLight](http://docs.bigbluebutton.org/install/green-light.html). # Source Code diff --git a/app/assets/javascripts/recordings.coffee b/app/assets/javascripts/recordings.coffee index 6d4f1549..ec39d595 100644 --- a/app/assets/javascripts/recordings.coffee +++ b/app/assets/javascripts/recordings.coffee @@ -37,6 +37,18 @@ class @Recordings COLUMN[c] = i++ constructor: -> + recordingsObject = this + canUpload = {} + + # Determine which recordings can be uploaded to Youtube. + $.ajax({ + method: 'GET', + async: false, + url: recordingsObject.getRecordingsURL() + '/can_upload' + }).success((res_data) -> + canUpload = res_data + ) + # configure the datatable for recordings this.table = $('#recordings').dataTable({ data: [], @@ -131,6 +143,14 @@ class @Recordings trigger = recordingActions.find('.recording-update-trigger') trigger.removeClass(classes.join(' ')) trigger.addClass(cls) + + upload_btn = recordingActions.find('.cloud-upload') + + if canUpload[row.id] + upload_btn.attr('data-popover-body', '.mail_youtube_popover') + else + upload_btn.attr('data-popover-body', '.mail_popover') + return recordingActions.html() return data } @@ -151,6 +171,22 @@ class @Recordings options.title = I18n.play_recording $('#recordings').tooltip(options) + options.selector = '.youtube-tooltip' + options.title = I18n.upload_youtube + $('#recordings').tooltip(options) + + options.selector = '.upload-tooltip' + options.title = I18n.share + $('#recordings').tooltip(options) + + options.selector = '.mail-tooltip' + options.title = I18n.mail_recording + $('#recordings').tooltip(options) + + options.selector = '.disabled-tooltip' + options.title = I18n.youtube_disabled + $('#recordings').tooltip(options) + $(document).one "turbolinks:before-cache", => @getTable().api().clear().draw().destroy() @@ -229,6 +265,7 @@ class @Recordings setupActionHandlers: -> table_api = this.table.api() recordingsObject = this + selectedUpload = null @getTable().on 'click', '.recording-update', (event) -> btn = $(this) @@ -273,12 +310,73 @@ class @Recordings showAlert(I18n.recording_deleted, 4000); ) + @getTable().on 'click', '.upload-button', (event) -> + btn = $(this) + row = table_api.row($(this).closest('tr')).data() + url = recordingsObject.getRecordingsURL() + id = row.id + + title = $('#video-title').val() + privacy_status = $('input[name=privacy_status]:checked').val() + + if title == '' + title = row.name + + $.ajax({ + method: 'POST', + url: url+'/'+id + data: {video_title: title, privacy_status: privacy_status} + success: () -> + cloud = selectedUpload.find('.cloud-blue') + check = selectedUpload.find('.green-check') + spinner = selectedUpload.find('.load-spinner') + + spinner.hide() + check.show() + setTimeout ( -> + cloud.show() + check.hide() + ), 4000 + }) + + selectedUpload.find('.cloud-blue').hide() + selectedUpload.find('.load-spinner').show() + + @getTable().on 'click', '.mail-recording', (event) -> + btn = $(this) + row = table_api.row($(this).closest('tr')).data() + url = recordingsObject.getRecordingsURL() + id = row.id + + # Take the username from the header. + username = $('#title-header').text().replace('Welcome ', '').trim() + + recording_url = row.playbacks[0].url + webcams_url = getHostName(recording_url) + '/presentation/' + id + '/video/webcams.webm' + subject = username + I18n.recording_mail_subject + body = I18n.recording_mail_body + "\n\n" + recording_url + "\n\n" + I18n.email_footer_1 + "\n" + I18n.email_footer_2 + + mailto = "mailto:?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body); + window.open(mailto); + + @getTable().on 'click', '.youtube-upload', (event) -> + row = table_api.row($(this).closest('tr')).data() + $('#video-title').attr('value', row.name) + + @getTable().on 'click', '.cloud-upload', (event) -> + selectedUpload = $(this) + @getTable().on 'draw.dt', (event) -> $('time[data-time-ago]').timeago(); getTable: -> @table + getHostName = (url) -> + parser = document.createElement('a'); + parser.href = url; + parser.origin; + getRecordingsURL: -> if $(".page-wrapper.rooms").data('main-room') base_url = Meeting.buildRootURL()+'/'+$('body').data('resource')+'/'+Meeting.getInstance().getAdminId() diff --git a/app/assets/stylesheets/main/landing.scss b/app/assets/stylesheets/main/landing.scss index 53dc2a34..664d8524 100644 --- a/app/assets/stylesheets/main/landing.scss +++ b/app/assets/stylesheets/main/landing.scss @@ -94,3 +94,23 @@ width: 100%; text-align: center; } + +.youtube-red { + color: red; +} + +.cloud-blue { + color: cornflowerblue; +} + +.green-check { + color: limegreen; +} + +.top-buffer { + margin-top: 8px; +} + +.tooltip-wrapper { + display: inline-block; +} diff --git a/app/controllers/bbb_controller.rb b/app/controllers/bbb_controller.rb index 344c75d8..9ab37f79 100644 --- a/app/controllers/bbb_controller.rb +++ b/app/controllers/bbb_controller.rb @@ -170,6 +170,41 @@ class BbbController < ApplicationController render_bbb_response bbb_res end + # POST /rooms/:room_id/recordings/:record_id + # POST /rooms/:room_id/:id/recordings/:record_id + def youtube_publish + # If we can't get the client, then they don't have a Youtube account. + begin + client = Yt::Account.new(access_token: current_user.token) + video = client.upload_video(get_webcams_url(params[:record_id]), + title: params[:video_title], + description: t('youtube_description', url: 'https://bigbluebutton.org/'), + privacy_status: params[:privacy_status]) + rescue + # In this case, they don't have a youtube channel connected to their account, so prompt to create one. + redirect_to 'https://m.youtube.com/create_channel' + end + end + + # GET /rooms/:room_id/recordings/can_upload + def can_upload + upload_data = {} + bbb_get_recordings[:recordings].each{ |recording_data| + next if recording_data[:recordID] == "" + # The recording is uploadable if it contains webcam data and they are logged in thorugh Google. + uploadable = Faraday.head(get_webcams_url(recording_data[:recordID])).status == 200 && + Rails.application.config.omniauth_google && + current_user.provider == 'google' + upload_data[recording_data[:recordID]] = uploadable + } + render json: upload_data + end + + def get_webcams_url(recording_id) + uri = URI.parse(ENV['BIGBLUEBUTTON_ENDPOINT']) + uri.scheme + '://' + uri.host + '/presentation/' + recording_id + '/video/webcams.webm' + end + private def load_room! diff --git a/app/lib/bbb_api.rb b/app/lib/bbb_api.rb index ef1a0312..5b37d072 100644 --- a/app/lib/bbb_api.rb +++ b/app/lib/bbb_api.rb @@ -141,6 +141,7 @@ module BbbApi end res[:recordings].each do |recording| + next if recording.key?(:error) pref_preview = {} recording[:length] = recording[:playback][:format].is_a?(Hash) ? recording[:playback][:format][:length] : recording[:playback][:format].first[:length] # create a playbacks attribute on recording for playback formats diff --git a/app/models/user.rb b/app/models/user.rb index ec106233..aa51beae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,7 @@ class User < ApplicationRecord user.username = self.send("#{auth_hash['provider']}_username", auth_hash) rescue nil user.email = self.send("#{auth_hash['provider']}_email", auth_hash) rescue nil user.name = auth_hash['info']['name'] + user.token = auth_hash['credentials']['token'] rescue nil user.save! user end diff --git a/app/views/landing/_rooms_center_panel.html.erb b/app/views/landing/_rooms_center_panel.html.erb index 39b6618a..ab475039 100644 --- a/app/views/landing/_rooms_center_panel.html.erb +++ b/app/views/landing/_rooms_center_panel.html.erb @@ -15,7 +15,7 @@ <% content_for :title do %>