diff --git a/admin/app/admin/fake_purchaser.rb b/admin/app/admin/fake_purchaser.rb index e318413ac..b57566ea1 100644 --- a/admin/app/admin/fake_purchaser.rb +++ b/admin/app/admin/fake_purchaser.rb @@ -33,6 +33,7 @@ ActiveAdmin.register_page "Fake Purchaser" do jam_track_right.jam_track = jam_track jam_track_right.is_test_purchase = true jam_track_right.version = jam_track.version + jam_track_right.can_download = true jam_track_right.save! count = count + 1 end diff --git a/db/manifest b/db/manifest index 388a6683a..5d4ca82e9 100755 --- a/db/manifest +++ b/db/manifest @@ -375,3 +375,4 @@ teacher_distribution_fields.sql jam_track_download_rights.sql guitar_center_integration_v1.sql mobile_recording_support.sql +youtube_broadcast.sql diff --git a/db/up/youtube_broadcast.sql b/db/up/youtube_broadcast.sql new file mode 100644 index 000000000..3b571d944 --- /dev/null +++ b/db/up/youtube_broadcast.sql @@ -0,0 +1,21 @@ +CREATE TABLE broadcasts ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + music_session_id VARCHAR(64) NOT NULL REFERENCES music_sessions(id) ON DELETE CASCADE, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + broadcast_id VARCHAR NOT NULL, + stream_id VARCHAR, + broadcast_status VARCHAR, + stream_status VARCHAR, + stream_name VARCHAR, + stream_address VARCHAR, + broadcast_data VARCHAR, + stream_data VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_broadcast_broadcast_id ON broadcasts USING BTREE(broadcast_id); +CREATE INDEX idx_broadcast_status ON broadcasts USING BTREE(broadcast_status); +CREATE INDEX idx_stream_status ON broadcasts USING BTREE(stream_status); + +CREATE INDEX idx_broadcast_music_session_id ON broadcasts USING BTREE(music_session_id); diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index a18db1c57..6f007d871 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -155,6 +155,7 @@ require "jam_ruby/models/friendship" require "jam_ruby/models/active_music_session" require "jam_ruby/models/music_session_comment" require "jam_ruby/models/session_info_comment" +require "jam_ruby/models/broadcast" require "jam_ruby/models/music_session" require "jam_ruby/models/music_session_liker" require "jam_ruby/models/music_session_user_history" diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 4e9fb0a0e..2bf514679 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -527,6 +527,12 @@ module JamRuby @storage_format == 'Drumma' end + def is_clevie_storage? + assert_storage_set + @storage_format == 'Clevie' + end + + def assert_storage_set raise "no storage_format set" if @storage_format.nil? end @@ -534,7 +540,7 @@ module JamRuby def parse_metalocation(metalocation) # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml - if is_drumma_storage? + if is_drumma_storage? || is_clevie_storage? suffix = '/meta.yml' @@ -898,6 +904,9 @@ module JamRuby elsif is_drumma_storage? jam_track.vendor_id = metadata[:id] jam_track.licensor = JamTrackLicensor.find_by_name!('Drumma Boy') + elsif is_clevie_storage? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name!('Steely & Clevie') end jam_track.slug = metadata['slug'] if jam_track.slug.nil? @@ -2247,6 +2256,8 @@ module JamRuby tim_tracks_s3_manager elsif is_drumma_storage? drumma_s3_manager + elsif is_clevie_storage? + clevie_s3_manager elsif is_helbing_storage? helbing_s3_manager else @@ -2262,6 +2273,10 @@ module JamRuby @drumma_s3_manager ||= S3Manager.new('jamkazam-drumma', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def clevie_s3_manager + @clevie_s3_manager ||= S3Manager.new('jamkazam-clevie', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + def tency_s3_manager @tency_s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end @@ -2335,6 +2350,11 @@ module JamRuby @storage_format == 'Drumma' end + def is_clevie_storage? + assert_storage_set + @storage_format == 'Clevie' + end + def is_tency_storage? assert_storage_set @storage_format == 'Tency' @@ -2446,6 +2466,19 @@ module JamRuby end end + def iterate_clevie_song_storage(&blk) + song_storage_manager.list_directories.each do |song| + @@log.debug("searching through song directory '#{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + + end + end + def iterate_helbing_song_storage(&blk) count = 0 song_storage_manager.list_directories('mapped').each do |song| @@ -2479,6 +2512,10 @@ module JamRuby iterate_drumma_song_storage do |metadata, metalocation| blk.call(metadata, metalocation) end + elsif is_clevie_storage? + iterate_clevie_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end elsif is_helbing_storage? iterate_helbing_song_storage do |metadata, metalocation| blk.call(metadata, metalocation) @@ -3487,6 +3524,17 @@ module JamRuby meta = YAML.load(data) meta[:genres] = ['r&b'] if !meta[:genres] meta + elsif is_clevie_storage? + + data = {} + begin + data = clevie_s3_manager.read_all(metalocation) + rescue AWS::S3::Errors::NoSuchKey + return {} + end + meta = YAML.load(data) + meta[:genres] = ['reggae'] if !meta[:genres] + meta else begin data = s3_manager.read_all(metalocation) diff --git a/ruby/lib/jam_ruby/models/broadcast.rb b/ruby/lib/jam_ruby/models/broadcast.rb new file mode 100644 index 000000000..68ac588ac --- /dev/null +++ b/ruby/lib/jam_ruby/models/broadcast.rb @@ -0,0 +1,18 @@ + +module JamRuby + class Broadcast < ActiveRecord::Base + + @@log = Logging.logger[Broadcast] + + STATUS_COMPLETED = 'completed' + STATUS_ABANDONED = 'abandoned' + STATUS_REVOKED = 'revoked' + + DONE_STATUSES = [STATUS_COMPLETED, STATUS_ABANDONED, STATUS_REVOKED] + belongs_to :music_session, :class_name => 'JamRuby::MusicSsession' + + def self.current_broadcast(music_session) + Broadcast.where(music_session_id: music_session.id).where('broadcast_status not in (?)', Broadcast::DONE_STATUSES).first + end + end +end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 79f85a392..d583a1b46 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -54,6 +54,7 @@ module JamRuby has_many :rsvp_slots, :class_name => "JamRuby::RsvpSlot", :foreign_key => "music_session_id", :dependent => :destroy has_many :music_notations, :class_name => "JamRuby::MusicNotation", :foreign_key => "music_session_id" has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession" + has_many :broadcasts, :class_name => "JamRuby::Broadcast" validates :genre, :presence => true validates :description, :presence => true, :no_profanity => true @@ -78,6 +79,135 @@ module JamRuby SEPARATOR = '|' + def current_broadcast + Broadcast.current_broadcast(self) + end + + def create_broadcast(google_client, user, broadcast_options) + + broadcast = current_broadcast + + if broadcast.nil? + broadcast = create_youtube_broadcast(google_client, user, broadcast_options) + else + refresh_youtube_broadcast(google_client, user, broadcast) + # check against Youtube the real state of broadcast, to see if we need a new one? + end + + broadcast + end + + def create_stream(google_client, user, broadcast_options) + + broadcast = create_broadcast(google_client, user, broadcast_options) + + stream = current_stream(broadcast) + + if stream.nil? + create_youtube_stream(google_client, user, broadcast, broadcast_options) + bind_broadcast(google_client, user, broadcast) + else + bind_broadcast(google_client, user, broadcast) + end + end + + def current_stream(broadcast) + broadcast.stream_id + end + + def refresh_youtube_broadcast(google_client, user, broadcast) + broadcast_data = google_client.get_broadcast(user, broadcast.broadcast_id) + broadcast.broadcast_status = broadcast_data["status"]["lifeCycleStatus"] + broadcast.broadcast_data = broadcast_data.to_json + end + + # https://developers.google.com/youtube/v3/live/docs/liveStreams#resource + def create_youtube_stream(google_client, user, broadcast, broadcast_options) + + # https://developers.google.com/youtube/v3/live/docs/liveStreams/insert + # required + # snippet.title + # cdn.format + # cdn.ingestionType (deprecated - use resolution/framerate) + + stream_options = {} + stream_options[:snippet] ||= {} + stream_options[:snippet][:title] ||= name + stream_options[:snippet][:isDefaultStream] = false + #broadcast_options[:snippet][:scheduledEndTime] = end_time.utc.iso8601 + + stream_options[:cdn] ||= {} + stream_options[:cdn][:frameRate] ||= '30fps' + stream_options[:cdn][:resolution] ||= '360p' + stream_options[:cdn][:ingestionType] ||= 'rtmp' + + stream_options[:contentDetails] ||= {} + stream_options[:contentDetails][:isReusable] = false + + stream_options = google_client.create_stream(user, stream_options) + + broadcast.stream_id = stream_options["id"] + broadcast.stream_status = stream_options["status"]["streamStatus"] + broadcast.stream_name = stream_options["cdn"]["ingestionInfo"]["streamName"] + broadcast.stream_address = stream_options["cdn"]["ingestionInfo"]["ingestionAddress"] + broadcast.stream_data = stream_options.to_json + broadcast.save! + broadcast + end + + def create_youtube_broadcast(google_client, user, broadcast_options) + + start_time, end_time = youtube_times + broadcast_options ||= {} + broadcast_options[:snippet] ||= {} + broadcast_options[:snippet][:title] ||= name + broadcast_options[:snippet][:description] ||= description + broadcast_options[:snippet][:scheduledStartTime] = start_time.utc.iso8601 + #broadcast_options[:snippet][:scheduledEndTime] = end_time.utc.iso8601 + + broadcast_options[:status] ||= {} + broadcast_options[:status][:privacyStatus] ||= (fan_access ? 'public' : 'private') + + broadcast_options[:contentDetails] ||= {} + + # if false, this causes a 'request not authorized error' + # From: https://developers.google.com/youtube/v3/live/docs/liveBroadcasts + # If your channel does not have permission to disable recordings, and you attempt to insert a broadcast with the recordFromStart property set to false, the API will return a Forbidden error. + #broadcast_options[:contentDetails][:recordFromStart] ||= false + + broadcast_data = google_client.create_broadcast(user, broadcast_options) + + broadcast = Broadcast.new + broadcast.music_session_id = self.id + broadcast.user_id = user.id + broadcast.broadcast_id = broadcast_data["id"] + broadcast.broadcast_status = broadcast_data["status"]["lifeCycleStatus"] + broadcast.broadcast_data = broadcast_data.to_json + broadcast.save! + broadcast + end + + def bind_broadcast(google_client, user, broadcast) + + bind_data = google_client.bind_broadcast(user, broadcast.broadcast_id, broadcast.stream_id) + broadcast.broadcast_data = bind_data.to_json + broadcast.save! + broadcast + end + + def youtube_times + start = scheduled_start_time + + if start < Time.now + start = Time.now + end_time = start + safe_scheduled_duration + return [start, end_time] + else + return [start, scheduled_end_time] + end + + end + def check_scheduling_info_changed @scheduling_info_changed = scheduled_start_changed? true @@ -574,7 +704,9 @@ module JamRuby end def scheduled_end_time - + start = scheduled_start_time + duration = safe_scheduled_duration + start + duration end def timezone_id diff --git a/ruby/lib/jam_ruby/models/user_authorization.rb b/ruby/lib/jam_ruby/models/user_authorization.rb index 9710e7082..a78bad549 100644 --- a/ruby/lib/jam_ruby/models/user_authorization.rb +++ b/ruby/lib/jam_ruby/models/user_authorization.rb @@ -50,7 +50,7 @@ module JamRuby self .where(:user_id => user.id) .where(:provider => 'google_login') - .where(['token_expiration IS NULL OR token_expiration > ?', Time.now]) + .where(['token_expiration IS NULL OR (token_expiration > ? OR refresh_token is not null)', Time.now]) .limit(1) end diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 7e2da352e..b40f20262 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -1752,6 +1752,7 @@ this.isSessVideoShared = isSessVideoShared; this.SessStopVideoSharing = SessStopVideoSharing; this.SessStartVideoSharing = SessStartVideoSharing; + this.getOpenVideoSources = getOpenVideoSources; // Clipboard this.SaveToClipboard = SaveToClipboard; diff --git a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee index d9c0ad6ce..9b1623a91 100644 --- a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee @@ -4,6 +4,8 @@ logger = context.JK.logger NoVideoRecordActive = 0 WebCamRecordActive = 1 ScreenRecordActive = 2 +WebCam2RecordActive = 3 +DesktopRecordActive = 4 mixins = [] @@ -53,20 +55,42 @@ if accessOpener if @inputType != 'audio-only' - if $root.find('#recording-selection').val() == 'video-window' + selection = $root.find('#recording-selection').val() + + if selection == 'video-window' recordVideo = ScreenRecordActive - else + else if selection == 'webcam-only' recordVideo = WebCamRecordActive + else if selection == 'webcam-only-2' + recordVideo = WebCam2RecordActive + else + recordVideo = DesktopRecordActive recordChat = $root.find('#include-chat').is(':checked') # if the video window isn't open, but a video option was selected... - if recordVideo != NoVideoRecordActive && !VideoStore.videoShared + window.opener.VideoActions.refreshVideoState.trigger() + + if recordVideo != NoVideoRecordActive && !VideoStore.anyVideoOpen + #if recordVideo != NoVideoRecordActive && !VideoStore.videoShared logger.debug("prevent video from opening", VideoStore) context.JK.prodBubble($root.find('.control'), 'video-window-not-open', {}, {positions:['bottom']}) return + + if recordVideo == WebCamRecordActive && !VideoStore.openVideoSources.webcam1 + context.JK.prodBubble($root.find('.control'), 'no-webcam-1', {}, {positions:['bottom']}) + return + + if recordVideo == WebCam2RecordActive && !VideoStore.openVideoSources.webcam2 + context.JK.prodBubble($root.find('.control'), 'no-webcam-2', {}, {positions:['bottom']}) + return + + if recordVideo == DesktopRecordActive && !VideoStore.openVideoSources.screen_capture + context.JK.prodBubble($root.find('.control'), 'no-screen-capture', {}, {positions:['bottom']}) + return + logger.debug("@inputType, @udiotye", recordChat, recordVideo) window.opener.RecordingActions.startRecording(recordVideo, recordChat) @@ -120,6 +144,8 @@ if accessOpener diff --git a/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee index 662610fa9..a50ac1cb9 100644 --- a/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee @@ -14,4 +14,5 @@ context = window configureVideoPopupClosed: {} checkPromptConfigureVideo: {} setVideoEnabled: {} + refreshVideoState: {} }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee index fa1f7282c..d0923cda7 100644 --- a/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee @@ -137,6 +137,22 @@ BackendToFrontendFPS = { @state.currentFrameRate = frameRates this.trigger(@state) + onRefreshVideoState:()-> + @logger.debug("onRefreshVideoState") + openVideoSources = context.jamClient.getOpenVideoSources() + @logger.debug("onRefreshVideoState", openVideoSources) + + # possible keys, all bool values + #"session_window", "webcam1", "webcam2", "screen_capture" + + # ex: with mac webcam open only: session_window: 2, webcam1: 1} + # no webcam open: Object {} + + @openVideoSources = openVideoSources + @anyVideoOpen = Object.keys(openVideoSources).length > 0 + @state.anyVideoOpen = Object.keys(openVideoSources).length > 0 + this.trigger(@state) + onSelectDevice: (device, caps) -> # don't do anything if no video capabilities diff --git a/web/app/assets/stylesheets/minimal/video_stream.scss b/web/app/assets/stylesheets/minimal/video_stream.scss new file mode 100644 index 000000000..f0d276c5e --- /dev/null +++ b/web/app/assets/stylesheets/minimal/video_stream.scss @@ -0,0 +1,58 @@ +@import "client/common"; + +body.video-stream { + + position: relative; + color: $ColorTextTypical; + + #minimal-container { + padding-bottom: 20px; + height:240px; + } + + .video-stream { + 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; + } +} \ No newline at end of file diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb index 6be8dce62..d24a0c987 100644 --- a/web/app/controllers/landings_controller.rb +++ b/web/app/controllers/landings_controller.rb @@ -168,7 +168,7 @@ class LandingsController < ApplicationController instrument = params[:instrument].downcase.sub('-', ' ') instrument = Instrument.find_by_id(instrument) instrument_id = instrument.id if instrument - instrument_name = instrument.description + instrument_name = instrument .description query, next_ptr, instrument_count = JamTrack.index({instrument: instrument_id}, current_user) end @jam_track = JamTrack.find_by_slug(params[:plan_code]) diff --git a/web/app/controllers/popups_controller.rb b/web/app/controllers/popups_controller.rb index 8f21de6b5..5ea8af974 100644 --- a/web/app/controllers/popups_controller.rb +++ b/web/app/controllers/popups_controller.rb @@ -29,6 +29,13 @@ class PopupsController < ApplicationController render :layout => "minimal" end + + def video_stream + @session_id = params[:session_id] + gon.session_id= @session_id + render :layout => "minimal" + end + def jamtrack_player enable_olark @jamtrack_id = params[:jam_track_id] diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index c80c8f165..1f9e20dde 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -371,6 +371,18 @@ script type="text/template" id="template-help-video-window-not-open" p You've selected to record video, but the video window is not open. p Click the VIDEO button in the main window and try again. +script type="text/template" id="template-help-no-webcam-1" + .video-window-not-open + p You've selected to record your primary webcam, but it is not open. + +script type="text/template" id="template-help-no-webcam-2" + .video-window-not-open + p You've selected to record your secondary webcam, but it is not open. + +script type="text/template" id="template-help-no-screen-capture" + .video-window-not-open + p You've selected to record your desktop, but that feature is not enabled. + script type="text/template" id="template-help-vid-record-chat-input" .vid-record-chat-input p Any chat inputs in the session will also be included in the video if checked. diff --git a/web/app/views/popups/video_stream.html.slim b/web/app/views/popups/video_stream.html.slim new file mode 100644 index 000000000..08a9dd556 --- /dev/null +++ b/web/app/views/popups/video_stream.html.slim @@ -0,0 +1,3 @@ +- provide(:page_name, 'video-stream popup') +- provide(:title, 'Video Stream') += react_component 'PopupVideoStreamer', {} \ No newline at end of file diff --git a/web/config/initializers/omniauth.rb b/web/config/initializers/omniauth.rb index a1bd1d83b..262399ccd 100644 --- a/web/config/initializers/omniauth.rb +++ b/web/config/initializers/omniauth.rb @@ -2,7 +2,7 @@ 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'} # 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 :google_oauth2, Rails.application.config.google_client_id, Rails.application.config.google_secret, {name: "google_login", prompt: 'consent', scope: ['youtube', 'youtube.force-ssl', 'youtube.upload','userinfo.email','userinfo.profile']} provider :twitter, Rails.application.config.twitter_app_id, Rails.application.config.twitter_app_secret, {x_auth_access_type: 'write' } provider :stripe_connect, Rails.application.config.stripe[:client_id], Rails.application.config.stripe[:secret_key], {} end diff --git a/web/config/routes.rb b/web/config/routes.rb index 5831ceabe..94718c3aa 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -178,6 +178,7 @@ Rails.application.routes.draw do get '/video/upload/:recording_id', to: 'popups#video_upload' get '/jamtrack-player/:jam_track_id', to: 'popups#jamtrack_player' get '/jamtrack/download/:jam_track_id/mixdowns/:jam_track_mixdown_id', to: 'popups#jamtrack_download' + get '/video/stream/:session_id', to: 'popups#video_stream' end scope '/corp' do diff --git a/web/lib/google_client.rb b/web/lib/google_client.rb index 1406f67f2..649de6737 100644 --- a/web/lib/google_client.rb +++ b/web/lib/google_client.rb @@ -5,13 +5,18 @@ require 'json' require 'google/api_client' require 'google/api_client/client_secrets' require 'google/api_client/auth/installed_app' -require 'socket' +require 'socket' + +#Google::Apis.logger.level = Logger::DEBUG + +YOUTUBE_API_SERVICE_NAME = 'youtube' +YOUTUBE_API_VERSION = 'v3' # Youtube OAuth and API functionality: module JamRuby class GoogleClient attr_accessor :client - attr_accessor :api + attr_accessor :api attr_accessor :request attr_accessor :server attr_accessor :socket @@ -19,21 +24,56 @@ module JamRuby attr_accessor :redirect_uri def initialize() - Rails.logger.info("Initializing client...") self.config = Rails.application.config self.redirect_uri='http://localhost:2112/auth/google_login/callback' self.client = Google::APIClient.new( - :application_name => 'JamKazam', - :application_version => '1.0.0' + :application_name => 'JamKazam', + :application_version => '1.0.0' ) - #youtube = client.discovered_api('youtube', 'v3') end - + + def youtube + @youtube ||= client.discovered_api('youtube', 'v3') + end + + def create_authorization(user_auth, scope, autorefresh) + + authorization = Signet::OAuth2::Client.new( + :authorization_uri => "https://accounts.google.com/o/oauth2/auth", + :token_credential_uri => "https://accounts.google.com/o/oauth2/token", + :client_id => @config.google_client_id, + :client_secret => @config.google_secret, + #:redirect_uri => credentials.redirect_uris.first, + :scope => scope + ) + authorization.access_token = user_auth.token + authorization.refresh_token = user_auth.refresh_token + authorization.expires_at = user_auth.token_expiration + + if autorefresh && (user_auth.token_expiration < (Time.now - 15)) # add 15 second buffer to this time, because OAUth server does not respond with timestamp, but 'expires_in' which is just offset seconds + + # XXX: what to do when this fails? + authorization.refresh! + user_auth.token = authorization.access_token + user_auth.token_expiration = authorization.issued_at + authorization.expires_in + user_auth.save + end + authorization + end + + def create_client + Google::APIClient.new( + :application_name => 'JamKazam', + :application_version => '1.0.0', + ) + end + # Return a login URL that will show a web page with def get_login_url(username=nil) + puts "GET LOGIN URL" uri = "https://accounts.google.com/o/oauth2/auth" - uri << "?scope=#{CGI.escape('https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload https://gdata.youtube.com email profile ')}" # # https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload + uri << "?scope=#{CGI.escape('https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube https://gdata.youtube.com email profile ')}" # # https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload uri << "&client_id=#{CGI.escape(self.config.google_email)}" uri << "&response_type=code" uri << "&access_type=online" @@ -46,15 +86,168 @@ module JamRuby uri end + # create youtube broadcast + def create_broadcast(user, broadcast_options) + auth = UserAuthorization.google_auth(user).first + if auth.nil? || auth.token.nil? + raise JamPermissionError, "No current google token found for user #{user}" + end + + broadcast_data = { + "snippet" => broadcast_options[:snippet], + "status" => broadcast_options[:status], + "contentDetails" => broadcast_options[:contentDetails] + } + + begin + #secrets = Google::APIClient::ClientSecrets.new({"web" => {"access_token" => auth.token, "refresh_token" => auth.refresh_token, "client_id" => @config.google_client_id, "client_secret" => @config.google_secret}}) + + + my_client = create_client + + my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) + + puts "BROADCAST DATA: #{broadcast_data}" + #y = my_client.discovered_api('youtube', 'v3') + response = my_client.execute!(:api_method => youtube.live_broadcasts.insert, + :parameters => {:part => 'contentDetails,status,snippet'}, + :body_object => broadcast_data) + + body = JSON.parse(response.body) + puts "CREATE BROADCAST RESPONSE: #{body}" + return body + rescue Google::APIClient::ClientError => e + # ex: +=begin + ex = { + "error": { + "errors": [ + { + "domain": "youtube.liveBroadcast", + "reason": "liveStreamingNotEnabled", + "message": "The user is not enabled for live streaming.", + "extendedHelp": "https://www.youtube.com/features" + } + ], + "code": 403, + "message": "The user is not enabled for live streaming." + } + } + + ex = { + "error": { + "errors": [ + { + "domain": "youtube.liveBroadcast", + "reason": "insufficientLivePermissions", + "message": "Request is not authorized", + "extendedHelp": "https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/insert#auth_required" + } + ], + "code": 403, + "message": "Request is not authorized" + } + } +=end + + puts e.result.body + raise e + end + end + + def bind_broadcast(user, broadcast_id, stream_id) + auth = UserAuthorization.google_auth(user).first + if auth.nil? || auth.token.nil? + raise JamPermissionError, "No current google token found for user #{user}" + end + + begin + my_client = create_client + + my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) + #y = my_client.discovered_api('youtube', 'v3') + response = my_client.execute!(:api_method => youtube.live_broadcasts.bind, + :parameters => {:part => 'id,contentDetails,status,snippet', :id => broadcast_id, :streamId => stream_id }) + + body = JSON.parse(response.body) + puts "BIND RESPONSE: #{body}" + return body + rescue Google::APIClient::ClientError => e + puts e.result.body + raise e + end + end + + def get_broadcast(user, broadcast_id) + auth = UserAuthorization.google_auth(user).first + if auth.nil? || auth.token.nil? + raise JamPermissionError, "No current google token found for user #{user}" + end + + begin + my_client = create_client + + my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) + #y = my_client.discovered_api('youtube', 'v3') + response = my_client.execute!(:api_method => youtube.live_broadcasts.list, + :parameters => {:part => 'id,contentDetails,status,snippet', :id => broadcast_id }) + + body = JSON.parse(response.body) + puts "BIND RESPONSE: #{body}" + return body["items"][0] # returns array of items. meh + rescue Google::APIClient::ClientError => e + puts e.result.body + raise e + end + end + + def create_stream(user, stream_options) + auth = UserAuthorization.google_auth(user).first + if auth.nil? || auth.token.nil? + raise JamPermissionError, "No current google token found for user #{user}" + end + + broadcast_data = { + "snippet" => stream_options[:snippet], + "cdn" => stream_options[:cdn], + "contentDetails" => stream_options[:contentDetails] + } + + begin + my_client = create_client + + my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) + + puts "STREAM DATA: #{broadcast_data}" + #y = my_client.discovered_api('youtube', 'v3') + response = my_client.execute!(:api_method => youtube.live_streams.insert, + :parameters => {:part => 'id,contentDetails,cdn,status,snippet'}, + :body_object => broadcast_data) + + body = JSON.parse(response.body) + puts "CREATE STREAM RESPONSE: #{body}" + return body + rescue Google::APIClient::ClientError => e + puts e.result.body + raise e + end + end + + # create youtube broadcast + def update_broadcast(user, broadcast_options) + + end + + # Contacts youtube and prepares an upload to youtube. This # process is somewhat painful, even in ruby, so we do the preparation # and the client does the actual upload using the URL returned: # https://developers.google.com/youtube/v3/docs/videos/insert # https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol - def sign_youtube_upload(user, filename, length) + def sign_youtube_upload(user, filename, length) raise ArgumentError, "Length is required and should be > 0" if length.to_i.zero? - + # Something like this: # POST /upload/youtube/v3/videos?uploadType=resumable&part=snippet,status,contentDetails HTTP/1.1 # Host: www.googleapis.com @@ -79,38 +272,38 @@ module JamRuby # } auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? - raise SecurityError, "No current google token found for user #{user}" + raise SecurityError, "No current google token found for user #{user}" end video_data = { - "snippet"=> { - "title"=> filename, - "description"=> filename, - "tags"=> ["cool", "video", "more keywords"], - "categoryId"=>1 - }, - "status"=> { - "privacyStatus"=> "public", - "embeddable"=> true, - "license"=> "youtube" - } - } - - conn = Faraday.new(:url =>"https://www.googleapis.com",:ssl => {:verify => false}) do |faraday| - faraday.request :url_encoded - faraday.adapter Faraday.default_adapter + "snippet" => { + "title" => filename, + "description" => filename, + "tags" => ["cool", "video", "more keywords"], + "categoryId" => 1 + }, + "status" => { + "privacyStatus" => "public", + "embeddable" => true, + "license" => "youtube" + } + } + + conn = Faraday.new(:url => "https://www.googleapis.com", :ssl => {:verify => false}) do |faraday| + faraday.request :url_encoded + faraday.adapter Faraday.default_adapter end video_json=video_data.to_json result = conn.post("/upload/youtube/v3/videos?access_token=#{CGI.escape(auth.token)}&uploadType=resumable&part=snippet,status,contentDetails", - video_json, - { - 'content-type'=>'application/json;charset=utf-8', - 'x-Upload-Content-Length'=>"#{length}", - 'x-upload-content-type'=>"video/*" - } + video_json, + { + 'content-type' => 'application/json;charset=utf-8', + 'x-Upload-Content-Length' => "#{length}", + 'x-upload-content-type' => "video/*" + } ) - + # Response should something look like: # HTTP/1.1 200 OK # Location: https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&upload_id=xa298sd_f&part=snippet,status,contentDetails @@ -119,7 +312,7 @@ module JamRuby if (result.nil? || result.status!=200 || result.headers['location'].blank?) msg = "Failed signing with status=#{result.status} #{result.inspect}: " if result.body.present? && result.body.length > 2 - msg << result.body.inspect# JSON.parse(result.body).inspect + msg << result.body.inspect # JSON.parse(result.body).inspect end # TODO: how to test for this: @@ -130,20 +323,20 @@ module JamRuby else # This has everything one needs to start the upload to youtube: { - "method" => "PUT", - "url" => result.headers['location'], - "Authorization" => "Bearer #{auth.token}", - "Content-Length" => length, - "Content-Type" => "video/*" - } + "method" => "PUT", + "url" => result.headers['location'], + "Authorization" => "Bearer #{auth.token}", + "Content-Length" => length, + "Content-Type" => "video/*" + } end - end + end # https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol#Check_Upload_Status def youtube_upload_status(user, upload_url, length) auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? - raise SecurityError, "No current google token found for user #{user}" + raise SecurityError, "No current google token found for user #{user}" end # PUT UPLOAD_URL HTTP/1.1 @@ -151,20 +344,20 @@ module JamRuby # Content-Length: 0 # Content-Range: bytes */CONTENT_LENGTH RestClient.put(upload_url, nil, { - 'Authorization' => "Bearer #{auth.token}", - 'Content-Length'=> "0", - 'Content-Range' => "bytes */#{length}" - }) do |response, request, result| + 'Authorization' => "Bearer #{auth.token}", + 'Content-Length' => "0", + 'Content-Range' => "bytes */#{length}" + }) do |response, request, result| # Result looks like this: # 308 Resume Incomplete # Content-Length: 0 # Range: bytes=0-999999 - case(response.code) + case (response.code) when 200..207 result_hash = { - "offset" => 0, - "length" => length, - "status" => response.code + "offset" => 0, + "length" => length, + "status" => response.code } when 308 range_str = response.headers['Range'] @@ -174,15 +367,15 @@ module JamRuby range = range_str.split("-") end result_hash = { - "offset" => range.first.to_i, - "length" => range.last.to_i, - "status" => response.code + "offset" => range.first.to_i, + "length" => range.last.to_i, + "status" => response.code } else raise "Unexpected status from youtube: [#{response.code}] with headers: #{response.headers.inspect}" - end + end - result_hash + result_hash end end @@ -190,14 +383,14 @@ module JamRuby def verify_youtube_upload(user, upload_url, length) status_hash=youtube_upload_status(user, upload_url, length) (status_hash['status']>=200 && status_hash['status']<300) - end + end # Set fully_uploaded if the upload can be verified. # @return true if verified; false otherwise: - def complete_upload(recorded_video) + def complete_upload(recorded_video) if (verify_youtube_upload(recorded_video.user, recorded_video.url, recorded_video.length)) recorded_video.update_attribute(:fully_uploaded, true) - else + else false end end @@ -205,17 +398,17 @@ module JamRuby def verify_recaptcha(recaptcha_response) success = false if !Rails.application.config.recaptcha_enable - success = true + success = true else Rails.logger.info "Login with: #{recaptcha_response}" - RestClient.get("https://www.google.com/recaptcha/api/siteverify", - params: { - secret: Rails.application.config.recaptcha_private_key, - response: recaptcha_response - } + RestClient.get("https://www.google.com/recaptcha/api/siteverify", + params: { + secret: Rails.application.config.recaptcha_private_key, + response: recaptcha_response + } ) do |response, request, result| Rails.logger.info "response: #{response.inspect}" - case(response.code) + case (response.code) when 200..207 json = JSON.parse(response.to_str) if json['success'] @@ -229,34 +422,37 @@ module JamRuby end #do end # if success - end #def + end + + #def # This will also sign in and prompt for login as necessary; # currently requires the server to be running at localhost:3000 - def signin_flow() + def signin_flow() config = Rails.application.config self.client = Google::APIClient.new( - :application_name => 'JamKazam', - :application_version => '1.0.0' + :application_name => 'JamKazam', + :application_version => '1.0.0' ) - + + raise "SIGNIN FLOW!!" flow = Google::APIClient::InstalledAppFlow.new( - :client_id => config.google_client_id, - :client_secret => config.google_secret, - :redirect_uri=>redirect_uri, - :scope => 'email profile' + :client_id => config.google_client_id, + :client_secret => config.google_secret, + :redirect_uri => redirect_uri, + :scope => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload' ) self.client.authorization = flow.authorize end # Must manually confirm to obtain refresh token: - def get_refresh_token + def get_refresh_token config = Rails.application.config - conn = Faraday.new(:url => 'https://accounts.google.com',:ssl => {:verify => false}) do |faraday| - faraday.request :url_encoded - faraday.adapter Faraday.default_adapter + conn = Faraday.new(:url => 'https://accounts.google.com', :ssl => {:verify => false}) do |faraday| + faraday.request :url_encoded + faraday.adapter Faraday.default_adapter end wait_for_callback do |refresh_token| @@ -264,40 +460,40 @@ module JamRuby end result = conn.get '/o/oauth2/auth', { - 'scope'=>'email profile', - 'client_id'=>config.google_client_id, - 'response_type'=>"code", - 'access_type'=>"offline", - 'redirect_uri'=>redirect_uri - } + 'scope' => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload', + 'client_id' => config.google_client_id, + 'response_type' => "code", + 'access_type' => "offline", + 'redirect_uri' => redirect_uri + } end - def get_access_token(refresh_token) + def get_access_token(refresh_token) refresh_token = "4/g9uZ8S4lq2Bj1J8PPIkgOFKhTKmCHSmRe68iHA75hRg.gj8Nt5bpVYQdPm8kb2vw2M23tnRnkgI" - + config = Rails.application.config - conn = Faraday.new(:url => 'https://accounts.google.com',:ssl => {:verify => false}) do |faraday| - faraday.request :url_encoded - faraday.adapter Faraday.default_adapter + conn = Faraday.new(:url => 'https://accounts.google.com', :ssl => {:verify => false}) do |faraday| + faraday.request :url_encoded + faraday.adapter Faraday.default_adapter end wait_for_callback do |access_token| - Rails.logger.info("The access_token is #{access_token}") + Rails.logger.info("The access_token is #{access_token}") end result = conn.post '/o/oauth2/token', nil, { - 'scope'=>'email profile', - 'client_id'=>config.google_client_id, - 'client_secret'=>config.google_secret, - 'refresh_token'=>refresh_token, - 'grant_type'=>"refresh_token", - 'redirect_uri'=>redirect_uri - } + 'scope' => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload', + 'client_id' => config.google_client_id, + 'client_secret' => config.google_secret, + 'refresh_token' => refresh_token, + 'grant_type' => "refresh_token", + 'redirect_uri' => redirect_uri + } Rails.logger.info("REsult: #{result.inspect}\n\n") end - def wait_for_callback(port=3000) + def wait_for_callback(port=3000) shutdown() self.server = Thread.new { Rails.logger.info("STARTING SERVER THREAD...") @@ -307,7 +503,7 @@ module JamRuby if self.socket request = self.socket.gets Rails.logger.info("REQUEST: #{request}") - + params=CGI.parse(request) code = params['code'].first # Whack the end part: @@ -317,16 +513,16 @@ module JamRuby Rails.logger.info("access_code is #{status}") token=exchange_for_token(access_code) yield(token) - - response = "#{status}\n" + + response = "#{status}\n" self.socket.print "HTTP/1.1 200 OK\r\n" + - "Content-Type: text/plain\r\n" + - "Content-Length: #{response.bytesize}\r\n" + - "Connection: close\r\n" + "Content-Type: text/plain\r\n" + + "Content-Length: #{response.bytesize}\r\n" + + "Connection: close\r\n" self.socket.print "\r\n" self.socket.print response - self.socket.close + self.socket.close self.socket=nil else puts "WHY WOULD THIS EVER HAPPEN?" @@ -339,17 +535,17 @@ module JamRuby def exchange_for_token(access_code) Rails.logger.info("Exchanging token for code: [#{access_code}]") - conn = Faraday.new(:url =>"https://accounts.google.com",:ssl => {:verify => false}) do |faraday| - faraday.request :url_encoded - faraday.adapter Faraday.default_adapter + conn = Faraday.new(:url => "https://accounts.google.com", :ssl => {:verify => false}) do |faraday| + faraday.request :url_encoded + faraday.adapter Faraday.default_adapter end exchange_parms={ - 'grant_type'=>'authorization_code', - 'code'=>(access_code), - 'client_id'=>(config.google_email), - 'client_secret'=>(config.google_secret), - 'redirect_uri'=>(redirect_uri), + 'grant_type' => 'authorization_code', + 'code' => (access_code), + 'client_id' => (config.google_email), + 'client_secret' => (config.google_secret), + 'redirect_uri' => (redirect_uri), } result = conn.post('/o/oauth2/token', exchange_parms) @@ -362,7 +558,7 @@ module JamRuby end # shutdown - def shutdown() + def shutdown() Rails.logger.info("Stopping oauth server...") if (self.socket) begin diff --git a/web/lib/tasks/google.rake b/web/lib/tasks/google.rake index c3ef000eb..6b25d58b0 100644 --- a/web/lib/tasks/google.rake +++ b/web/lib/tasks/google.rake @@ -1,13 +1,14 @@ -=begin -require 'google/api_client' +Rails.logger = Logger.new(STDOUT) +require Rails.root.join('lib', 'google_client') namespace :google do + task create_broadcast: :environment do |task, args| + google_client = JamRuby::GoogleClient.new + music_session = MusicSession.first + user = User.find_by_email('seth@jamkazam.com') - task :youtube do |task, args| - client = Google::APIClient.new - yt = client.discovered_api('youtube', 'v3') - # google-api oauth-2-login --client-id='785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8.apps.googleusercontent.com' --client-secret='UwzIcvtErv9c2-GIsNfIo7bA' --scope="https://www.googleapis.com/auth/plus.me" + broadcast = music_session.create_stream(google_client, user, nil) + + puts broadcast.inspect end - end -=end \ No newline at end of file diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake index 3a79ace80..626cbe506 100644 --- a/web/lib/tasks/jam_tracks.rake +++ b/web/lib/tasks/jam_tracks.rake @@ -35,6 +35,12 @@ namespace :jam_tracks do JamTrackImporter.dry_run end + task create_clevie: :environment do |task, args| + licensor = JamTrackLicensor.new() + licensor.name = 'Steely & Clevie' + licensor.slug = 'steely-and-clevie' + licensor.save! + end task tency_dry_run: :environment do |task, args| JamTrackImporter.storage_format = 'Tency' JamTrackImporter.dry_run @@ -50,6 +56,11 @@ namespace :jam_tracks do JamTrackImporter.dry_run end + task clevie_dry_run: :environment do |task, args| + JamTrackImporter.storage_format = 'Clevie' + JamTrackImporter.dry_run + end + task paris_create_masters: :environment do |task, args| JamTrackImporter.storage_format = 'Paris' JamTrackImporter.create_masters @@ -87,6 +98,18 @@ namespace :jam_tracks do JamTrackImporter.create_master(path) end + task clevie_create_master: :environment do |task, args| + JamTrackImporter.storage_format = 'Clevie' + + path = ENV['TRACK_PATH'] + + if !path + puts "TRACK_PATH must be set to something like audio/AC DC/Back in Black or mapped/50 Cent - In Da Club - 12401" + exit(1) + end + + JamTrackImporter.create_master(path) + end task tency_delta: :environment do |task, args| JamTrackImporter.storage_format = 'Tency' @@ -211,6 +234,11 @@ namespace :jam_tracks do JamTrackImporter.synchronize_all(skip_audio_upload: false) end + task sync_clevie: :environment do |task, args| + JamTrackImporter.storage_format = 'Clevie' + JamTrackImporter.synchronize_all(skip_audio_upload: false) + end + task tency_dups: :environment do |task, args| end