From a1fd8c3d10122c0a3a4b210f6ead9c7128a396f6 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 14 Apr 2017 14:34:18 -0500 Subject: [PATCH] add cors for real --- db/manifest | 3 +- db/up/youtube_broadcast.sql | 21 + ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/models/broadcast.rb | 18 + ruby/lib/jam_ruby/models/music_session.rb | 134 +++++- .../lib/jam_ruby/models/user_authorization.rb | 2 +- .../stylesheets/minimal/video_stream.scss | 58 +++ web/app/controllers/popups_controller.rb | 7 + web/app/views/popups/video_stream.html.slim | 3 + web/config/initializers/omniauth.rb | 2 +- web/config/routes.rb | 1 + web/lib/google_client.rb | 426 +++++++++++++----- web/lib/tasks/google.rake | 17 +- 13 files changed, 566 insertions(+), 127 deletions(-) create mode 100644 db/up/youtube_broadcast.sql create mode 100644 ruby/lib/jam_ruby/models/broadcast.rb create mode 100644 web/app/assets/stylesheets/minimal/video_stream.scss create mode 100644 web/app/views/popups/video_stream.html.slim diff --git a/db/manifest b/db/manifest index 262e98f58..165349992 100755 --- a/db/manifest +++ b/db/manifest @@ -373,4 +373,5 @@ connection_role.sql retailer_payment_split.sql teacher_distribution_fields.sql jam_track_download_rights.sql -guitar_center_integration_v1.sql \ No newline at end of file +guitar_center_integration_v1.sql +youtube_broadcast.sql \ No newline at end of file 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 aa2a39162..a4ec430c3 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/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/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/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/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 003d4982e..76f12e2e6 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