Merge feature/video_mvp to develop and resolve conflicts.

This commit is contained in:
Steven Miers 2014-11-03 18:25:33 -06:00
commit fe9bd594b4
23 changed files with 878 additions and 97 deletions

View File

@ -484,6 +484,7 @@ message RecordingMasterMixComplete {
optional string notification_id = 4;
optional string created_at = 5;
optional string claimed_recording_id = 6;
<<<<<<< HEAD
}
message RecordingStreamMixComplete {
@ -493,6 +494,8 @@ message RecordingStreamMixComplete {
optional string notification_id = 4;
optional string created_at = 5;
optional string claimed_recording_id = 6;
=======
>>>>>>> feature/video_mvp
}
message DownloadAvailable {

View File

@ -76,6 +76,17 @@ module JamRuby
quick_mixes.find{|quick_mix| quick_mix.completed && !quick_mix.cleaned }
end
def mix_state
mix.state if mix
end
def mix_error
mix.error if mix
end
def stream_mix
quick_mixes.find{|quick_mix| quick_mix.completed && !quick_mix.cleaned }
end
# this can probably be done more efficiently, but David needs this asap for a video
def grouped_tracks
@ -121,8 +132,6 @@ module JamRuby
unless claimed_recordings.length > 0
destroy
end
end
def not_still_finalizing_previous
@ -446,7 +455,6 @@ module JamRuby
# Further joining and criteria for the unioned object:
arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \
.where('recorded_items_all.user_id' => user.id) \
.where('recorded_items_all.fully_uploaded = ?', false) \

View File

@ -12,7 +12,7 @@ module JamRuby
validates_uniqueness_of :uid, scope: :provider
# token, secret, token_expiration can be missing
def self.goog_auth(user)
def self.google_auth(user)
self
.where(:user_id => user.id)
.where(:provider => 'google_login')

View File

@ -37,13 +37,16 @@ gem 'compass-rails', '1.1.3' # 1.1.4 throws an exception on startup about !init
gem 'rabl', '0.11.0' # for JSON API development
gem 'gon', '~>4.1.0' # for passthrough of Ruby variables to Javascript variables
gem 'eventmachine', '1.0.3'
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-twitter'
gem 'omniauth-google-oauth2', '0.2.1'
gem 'google-api-client'
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'
gem 'sendgrid', '1.2.0'
@ -107,7 +110,7 @@ end
group :test, :cucumber do
gem 'simplecov', '~> 0.7.1'
gem 'simplecov-rcov'
gem 'capybara'
gem 'capybara', '2.4.4'
#if ENV['JAMWEB_QT5'] == '1'
# # necessary on platforms such as arch linux, where pacman -S qt5-webkit is your easiet option
# gem "capybara-webkit", :git => 'git://github.com/thoughtbot/capybara-webkit.git'
@ -115,15 +118,17 @@ group :test, :cucumber do
gem "capybara-webkit"
#end
gem 'capybara-screenshot', '0.3.22' # 1.0.0 broke compat with rspec. maybe we need newer rspec
gem 'selenium-webdriver'
gem 'cucumber-rails', :require => false #, '1.3.0', :require => false
gem 'guard-spork', '0.3.2'
gem 'spork', '0.9.0'
gem 'launchy', '2.1.0'
gem 'launchy', '2.1.1'
gem 'rack-test'
# gem 'rb-fsevent', '0.9.1', :require => false
# gem 'growl', '1.0.3'
gem 'poltergeist'
gem 'resque_spec'
#gem 'thin'
end

View File

@ -122,13 +122,17 @@
var description = $('#recording-finished-dialog form textarea[name=description]').val();
var genre = $('#recording-finished-dialog form select[name=genre]').val();
var is_public = $('#recording-finished-dialog form input[name=is_public]').is(':checked')
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')
rest.claimRecording({
id: recording.id,
name: name,
description: description,
genre: genre,
is_public: is_public
is_public: is_public,
save_video: save_video,
upload_to_youtube: upload_to_youtube
})
.done(function () {
$dialog.data('result', {keep:true});
@ -151,6 +155,12 @@
var $is_public_errors = context.JK.format_errors('is_public', errors);
if ($is_public_errors) $('#recording-finished-dialog form input[name=is_public]').closest('div.field').addClass('error').end().after($is_public_errors);
var $save_video_errors = context.JK.format_errors('save_video', errors);
if ($save_video_errors) $('#recording-finished-dialog form input[name=save_video]').closest('div.field').addClass('error').end().after($save_video_errors);
var $upload_to_youtube_errors = context.JK.format_errors('upload_to_youtube', errors);
if ($upload_to_youtube_errors) $('#recording-finished-dialog form input[name=upload_to_youtube]').closest('div.field').addClass('error').end().after($upload_to_youtube_errors);
var recording_error = context.JK.get_first_error('recording_id', errors);
if (recording_error) context.JK.showErrorDialog(app, "Unable to claim recording.", recording_error);
@ -223,8 +233,9 @@
function initializeButtons() {
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"]'));
}
function initialize() {

View File

@ -1,8 +1,9 @@
#recording-finished-dialog {
width:1000px;
height:auto;
div[purpose=description], div[purpose=is_public] {
div[purpose=description] {
margin-top:20px;
margin-bottom: 10px;
}
label[for=is_public], label[for=playback-mode-preview-all], label[for=playback-mode-preview-me] {
@ -23,11 +24,21 @@
margin-top:20px;
}
div[purpose=is_public] .icheckbox_minimal {
div[purpose=is_public], div[purpose=upload_to_youtube], div[purpose=save_video] {
.icheckbox_minimal {
display:inline-block;
position:relative;
top:3px;
margin-right:3px;
top:1px;
margin-top:3px;
margin-bottom:1px;
margin-right:2px;
}
label {
display: inline-block;
margin-bottom:4px;
margin-right:2px;
}
clear: left;
}
}

View File

@ -1,8 +1,8 @@
class ApiRecordingsController < ApiController
before_filter :api_signed_in_user, :except => [ :add_like ]
before_filter :look_up_recording, :only => [ :show, :stop, :claim, :discard, :keep ]
before_filter :parse_filename, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep ]
before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ]
before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ]
respond_to :json
@ -210,6 +210,135 @@ class ApiRecordingsController < ApiController
end
end
# POST /api/recordings/:id/videos/:video_id/upload_sign
def video_upload_sign
length = params[:length]
@youtube_client.upload_sign(current_user, @recorded_video.url, length)
end
# POST /api/recordings/:id/videos/:video_id/upload_complete
def video_upload_start
length = params[:length]
@youtube_client.get_upload_status(current_user, @recorded_video.url, length)
end
# POST /api/recordings/:id/videos/:video_id/upload_complete
def video_upload_complete
if @youtube_client.complete_upload(@recorded_video)
render :status => 200
else
render :status => 422
end
end
def upload_next_part_stream_mix
length = params[:length]
md5 = params[:md5]
@quick_mix.upload_next_part(length, md5)
if @quick_mix.errors.any?
response.status = :unprocessable_entity
# this is not typical, but please don't change this line unless you are sure it won't break anything
# this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's
# state doesn't cause errors to shoot out like normal.
render :json => { :errors => @quick_mix.errors }, :status => 422
else
result = {
:part => @quick_mix.next_part_to_upload,
:offset => @quick_mix.file_offset.to_s
}
render :json => result, :status => 200
end
end
def upload_sign_stream_mix
render :json => @quick_mix.upload_sign(params[:md5]), :status => 200
end
def upload_part_complete_stream_mix
part = params[:part]
offset = params[:offset]
@quick_mix.upload_part_complete(part, offset)
if @quick_mix.errors.any?
response.status = :unprocessable_entity
respond_with @quick_mix
else
render :json => {}, :status => 200
end
end
def upload_complete_stream_mix
@quick_mix.upload_complete
if @quick_mix.errors.any?
response.status = :unprocessable_entity
respond_with @quick_mix
return
else
render :json => {}, :status => 200
end
end
def upload_next_part_stream_mix
length = params[:length]
md5 = params[:md5]
@quick_mix.upload_next_part(length, md5)
if @quick_mix.errors.any?
response.status = :unprocessable_entity
# this is not typical, but please don't change this line unless you are sure it won't break anything
# this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's
# state doesn't cause errors to shoot out like normal.
render :json => { :errors => @quick_mix.errors }, :status => 422
else
result = {
:part => @quick_mix.next_part_to_upload,
:offset => @quick_mix.file_offset.to_s
}
render :json => result, :status => 200
end
end
def upload_sign_stream_mix
render :json => @quick_mix.upload_sign(params[:md5]), :status => 200
end
def upload_part_complete_stream_mix
part = params[:part]
offset = params[:offset]
@quick_mix.upload_part_complete(part, offset)
if @quick_mix.errors.any?
response.status = :unprocessable_entity
respond_with @quick_mix
else
render :json => {}, :status => 200
end
end
def upload_complete_stream_mix
@quick_mix.upload_complete
if @quick_mix.errors.any?
response.status = :unprocessable_entity
respond_with @quick_mix
return
else
render :json => {}, :status => 200
end
end
def upload_next_part_stream_mix
@ -267,7 +396,13 @@ class ApiRecordingsController < ApiController
end
private
def parse_filename
def lookup_recording
@recording = Recording.find(params[:id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recording.has_access?(current_user)
end
def lookup_recorded_track
@recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user)
end
@ -277,9 +412,9 @@ class ApiRecordingsController < ApiController
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user)
end
def look_up_recording
@recording = Recording.find(params[:id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recording.has_access?(current_user)
def lookup_recorded_video
@recorded_video = RecordedVideo.find_by_recording_id_and_client_video_source_id!(params[:id], params[:video_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_video.recording.has_access?(current_user)
end
end
end # class

View File

@ -6,7 +6,7 @@ class GmailController < ApplicationController
render :nothing => true, :status => 404
return
end
authorization = UserAuthorization.goog_auth(current_user)
authorization = UserAuthorization.google_auth(current_user)
if authorization.empty?
render :nothing => true, :status => 404
return

View File

@ -1,58 +0,0 @@
<!-- Invitation Dialog -->
<div class="dialog recordingFinished-overlay ftue-overlay tall" layout="dialog" layout-id="recordingFinished" id="recording-finished-dialog">
<div class="content-head">
<%= image_tag "content/recordbutton-off.png", {:height => 20, :width => 20, :class => 'content-icon'} %>
<h1>recording finished</h1>
</div>
<div class="dialog-inner">
Fill out the fields below and click the "SAVE" button to save this recording to your library. If you do not want to
keep the recording, click the "DISCARD" button.
<br/>
<br/>
<form class="left w40 mr20">
<div class="left w50 mr20">
<div class="field w100">
<label for="name">Recording name:</label><br/>
<input type="text" name="name" id="claim-recording-name" class="recording-name" class="w100"/>
</div>
</div>
<div class="right w40 genre-selector">
<div class="field">
<!-- genre box -->
<label for="genre">Genre:</label><br/>
<select name="genre"></select>
</div>
</div>
<div class="field w100 left" purpose="description">
<label for="description">Description:</label>
<textarea class="w100" name="description" id="claim-recording-description"></textarea>
</div>
<div class="field left" purpose="is_public">
<input type="checkbox" checked="checked" name="is_public"/><label for="is_public">Public Recording</label> <!--<a href="#"><<img src="images/shared/icon_help.png" width="12" height="12" /></a>-->
</div>
</form>
<div class="left w50 ml30">
Preview Recording:
<%= render "clients/play_controls" %>
<br/>
<br/>
</div>
<br clear="left"/><br/>
<div class="right">
<a href="#" class="button-grey" id="discard-session-recording">DISCARD</a>&nbsp;<a href="#" class="button-orange" id="keep-session-recording">SAVE</a>
</div>
<br clear="all"/>
</div>
</div>

View File

@ -0,0 +1,48 @@
/ Invitation Dialog
#recording-finished-dialog.dialog.recordingFinished-overlay.ftue-overlay.tall{:layout => "dialog", "layout-id" => "recordingFinished"}
.content-head
= image_tag "content/recordbutton-off.png", {:height => 20, :width => 20, :class => 'content-icon'}
%h1 recording finished
.dialog-inner
Fill out the fields below and click the "SAVE" button to save this recording to your library. If you do not want to
keep the recording, click the "DISCARD" button.
%br/
%br/
%form.left.w40.mr20
.left.w50.mr20
.field.w100
%label{:for => "name"} Recording name:
%br/
%input#claim-recording-name.w100{:name => "name", :type => "text"}/
.right.w40.genre-selector
.field
/ genre box
%label{:for => "genre"} Genre:
%br/
%select{:name => "genre"}
.field.w100.left{:purpose => "description"}
%label{:for => "description"} Description:
%textarea#claim-recording-description.w100{:name => "description"}
.field.left{:purpose => "save_video"}
%input{:checked => "checked", :name => "save_video", :type => "checkbox"}/
%label{:for => "save_video"} Save Video to Computer
.field.left{:purpose => "upload_to_youtube"}
%input{:checked => "checked", :name => "upload_to_youtube", :type => "checkbox"}/
%label{:for => "upload_to_youtube"} Upload Video to YouTube
.field.left{:purpose => "is_public"}
%input{:checked => "checked", :name => "is_public", :type => "checkbox"}/
%label{:for => "is_public"} Public Recording
/ <a href="#"><<img src="images/shared/icon_help.png" width="12" height="12" /></a>
.left.w50.ml30
Preview Recording:
\#{render "clients/play_controls"}
%br/
%br/
%br{:clear => "left"}/
%br/
.right
%a#discard-session-recording.button-grey{:href => "#"}> DISCARD
\&nbsp;
%a#keep-session-recording.button-orange{:href => "#"} SAVE
%br{:clear => "all"}/

View File

@ -143,6 +143,7 @@ if defined?(Bundler)
# google api keys
config.google_client_id = '785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8.apps.googleusercontent.com'
config.google_secret = 'UwzIcvtErv9c2-GIsNfIo7bA'
config.google_email = '785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8@developer.gserviceaccount.com'
if Rails.env == 'production'
config.desk_url = 'https://jamkazam.desk.com'

View File

@ -401,6 +401,8 @@ SampleApp::Application.routes.draw do
match '/recordings/:id/comments' => 'api_recordings#add_comment', :via => :post, :as => 'api_recordings_add_comment'
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'
# Recordings - recorded_tracks
match '/recordings/:id/tracks/:track_id' => 'api_recordings#show_recorded_track', :via => :get, :as => 'api_recordings_show_recorded_track'
match '/recordings/:id/tracks/:track_id/download' => 'api_recordings#download', :via => :get, :as => 'api_recordings_download'
match '/recordings/:id/tracks/:track_id/upload_next_part' => 'api_recordings#upload_next_part', :via => :get
@ -408,10 +410,17 @@ SampleApp::Application.routes.draw do
match '/recordings/:id/tracks/:track_id/upload_part_complete' => 'api_recordings#upload_part_complete', :via => :post
match '/recordings/:id/tracks/:track_id/upload_complete' => 'api_recordings#upload_complete', :via => :post
match '/recordings/:id/stream_mix/upload_next_part' => 'api_recordings#upload_next_part_stream_mix', :via => :get
# Recordings - stream_mix
match '/recordings/:id/stream_mix/upload_sign' => 'api_recordings#upload_sign_stream_mix', :via => :get
match '/recordings/:id/stream_mix/upload_part_complete' => 'api_recordings#upload_part_complete_stream_mix', :via => :post
match '/recordings/:id/stream_mix/upload_complete' => 'api_recordings#upload_complete_stream_mix', :via => :post
# Recordings - recorded_videos
match '/recordings/:id/tracks/:video_id/upload_sign' => 'api_recordings#video_upload_sign', :via => :get
match '/recordings/:id/videos/:video_id/upload_start' => 'api_recordings#video_upload_start', :via => :post
match '/recordings/:id/videos/:video_id/upload_complete' => 'api_recordings#video_upload_complete', :via => :post
# Claimed Recordings
match '/claimed_recordings' => 'api_claimed_recordings#index', :via => :get
match '/claimed_recordings/:id' => 'api_claimed_recordings#show', :via => :get

387
web/lib/youtube_client.rb Normal file
View File

@ -0,0 +1,387 @@
require 'faraday'
#require 'thin'
require 'launchy'
require 'cgi'
require 'json'
require 'google/api_client'
require 'google/api_client/client_secrets'
require 'google/api_client/auth/installed_app'
require 'socket' # Provides TCPServer and TCPSocket classes
# require 'youtube_client'; c = YouTubeClient.new
# Youtube API functionality:
module JamRuby
class YouTubeClient
attr_accessor :client
attr_accessor :api
attr_accessor :request
attr_accessor :server
attr_accessor :socket
attr_accessor :config
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'
)
youtube = client.discovered_api('youtube', 'v3')
# client.authorization = nil
# result = client.execute
# :key => config.youtube_developer_key,
# :api_method => youtube.videos.list,
# :parameters => {:id => '<YOUR_VIDEO_ID>', :part => 'snippet'}
# result = JSON.parse(result.data.to_json)
end
# Return a login URL that will show a web page with
def get_login_url(username=nil)
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 << "&client_id=#{CGI.escape(self.config.google_email)}"
#uri << "&client_secret=#{CGI.escape(self.config.google_secret)}"
uri << "&response_type=code"
uri << "&access_type=online"
uri << "&prompt=consent"
uri << "&state=4242"
uri << "&redirect_uri=#{redirect_uri}"
if username.present?
uri << "&login_hint=#{(username)}"
end
uri
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 upload_sign(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
# Authorization: Bearer AUTH_TOKEN
# Content-Length: 278
# Content-Type: application/json; charset=UTF-8
# X-Upload-Content-Length: 3000000
# X-Upload-Content-Type: video/*
# {
# "snippet": {
# "title": "My video title",
# "description": "This is a description of my video",
# "tags": ["cool", "video", "more keywords"],
# "categoryId": 22
# },
# "status": {
# "privacyStatus": "public",
# "embeddable": True,
# "license": "youtube"
# }
# }
auth = UserAuthorization.google_auth(user).first
if auth.nil? || auth.token.nil?
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.response :logger
faraday.adapter Faraday.default_adapter
end
video_json=video_data.to_json
# result = conn.post do |req|
# req.url('/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status')
# req.headers['authorization']="bearer #{(auth.token)}"
# req.headers['content-type']='application/json;charset=utf-8'
# req.headers['x-Upload-Content-Length']="#{length}"
# req.headers['x-upload-content-type']="video/*"
# req.body = video_json
# end
# access_token=#{CGI.escape(auth.token)}
result = conn.post("/upload/youtube/v3/videos?access_token=#{CGI.escape(auth.token)}&uploadType=resumable&part=snippet,status,contentDetails",
video_json,
{
# 'client_id'=>"#{(self.config.google_email)}",
# 'client_secret'=>config.google_secret,
#'Authorization'=>"bearer #{(auth.token)}",
'content-type'=>'application/json;charset=utf-8',
'x-Upload-Content-Length'=>"#{length}",
'x-upload-content-type'=>"video/*"
}
)
#puts result.inspect
# 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
# Content-Length: 0
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
end
# TODO: how to test for this:
# If reason is "youtubeSignupRequired"
# If the user's youtube account is unlinked, they'll have to go here.
# http://m.youtube.com/create_channel. With v3, there is no automated way to do this.
raise msg
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/*"
}
end
# This has everything one needs to start the upload to youtube:
end
# https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol#Check_Upload_Status
def get_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}"
end
# PUT UPLOAD_URL HTTP/1.1
# Authorization: Bearer AUTH_TOKEN
# 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|
# puts "response: #{response.class}: #{response.code} / #{response.headers} / #{response.body}"
# Result looks like this:
# 308 Resume Incomplete
# Content-Length: 0
# Range: bytes=0-999999
case(response.code)
when 200..207
result_hash = {
"offset" => 0,
"length" => length,
"status" => response.code
}
when 308
range_str = response.headers['Range']
if range_str.nil?
range = 0..length
else
range = range_str.split("-")
end
result_hash = {
"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
result_hash
end
end
# @return true if file specified by URL uploaded, false otherwise
def verify_upload(user, upload_url, length)
status_hash=get_upload_status(user, upload_url, length)
(status_hash['status']>=200 && status_hash['status']<300)
end
def complete_upload(recorded_video)
if (verify_upload(recorded_video.user, recorded_video.url, recorded_video.length))
recorded_video.update_attribute(:fully_uploaded, true)
else
false
end
end
# This will also sign in and prompt for login as necessary;
# currently requires the server to be running at localhost:3000
def signin_flow()
config = Rails.application.config
self.client = Google::APIClient.new(
:application_name => 'JamKazam',
:application_version => '1.0.0'
)
flow = Google::APIClient::InstalledAppFlow.new(
:client_id => config.google_client_id,
:client_secret => config.google_secret,
:redirect_uri=>redirect_uri,
:scope => 'email profile'
)
self.client.authorization = flow.authorize
end
# Must manually confirm to obtain refresh token:
# 4/ZwtU8nNgiEiu2JlJMrmnnw.Qo7Zys7XjRoZPm8kb2vw2M2j2ZEskgI
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.response :logger
faraday.adapter Faraday.default_adapter
end
wait_for_callback do |refresh_token|
Rails.logger.info("The refresh_token is #{refresh_token}")
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
}
end
def get_access_token(refresh_token)
refresh_token = "4/g9uZ8S4lq2Bj1J8PPIkgOFKhTKmCHSmRe68iHA75hRg.gj8Nt5bpVYQdPm8kb2vw2M23tnRnkgI"
#refresh_token = "4/ZwtU8nNgiEiu2JlJMrmnnw.Qo7Zys7XjRoZPm8kb2vw2M2j2ZEskgI"
config = Rails.application.config
conn = Faraday.new(:url => 'https://accounts.google.com',:ssl => {:verify => false}) do |faraday|
faraday.request :url_encoded
faraday.response :logger
faraday.adapter Faraday.default_adapter
end
wait_for_callback do |access_token|
Rails.logger.info("The access_token is #{access_token}")
#self.server.stop()
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,
#'response_type'=>"code",
'grant_type'=>"refresh_token",
#'access_type'=>"offline",
'redirect_uri'=>redirect_uri
}
Rails.logger.info("REsult: #{result.inspect}\n\n")
end
def wait_for_callback(port=3000)
shutdown()
self.server = Thread.new {
Rails.logger.info("STARTING SERVER THREAD...")
tcp_server = TCPServer.new('localhost', port)
self.socket = tcp_server.accept
if self.socket
request = self.socket.gets
Rails.logger.info("REQUEST: #{request}")
params=CGI.parse(request)
code = params['code'].first
# Whack the end part:
access_code = code ? code.split(" ").first : ""
status = (access_code.present?) ? 'OK' : 'EMPTY'
Rails.logger.info("access_code is #{status}")
token=exchange_for_token(access_code)
yield(token)
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"
self.socket.print "\r\n"
self.socket.print response
self.socket.close
socket=nil
end
}
end
def exchange_for_token(access_code)
#puts "EXCHANGING token for code: [#{access_code}] #{access_code.class}"
conn = Faraday.new(:url =>"https://accounts.google.com",:ssl => {:verify => false}) do |faraday|
faraday.request :url_encoded
# faraday.request :multipart
faraday.response :logger
faraday.adapter Faraday.default_adapter
end
exchange_parms={
'grant_type'=>'authorization_code',
'code'=>(access_code),
'client_id'=>(config.google_email),#CGI.escape(config.google_client_id),
'client_secret'=>(config.google_secret),
'redirect_uri'=>(redirect_uri),
}
result = conn.post('/o/oauth2/token', exchange_parms)
if result.body.nil? || result.body.blank?
raise "Result not in correct form: [#{result.body}]"
end
body_hash = JSON.parse(result.body)
#puts "RESULT #{result.body.class}: #{result.body}"
#puts "EXCHANGING for token: [#{body_hash['access_token']}]"
body_hash['access_token']
end
# shutdown
def shutdown()
Rails.logger.info("Stopping oauth server...")
#Thread.kill(self.server)
if (self.socket)
begin
self.socket.close
rescue IOError
# Expected for most cases:
Rails.logger.info("Socket already closed.")
end
self.socket = nil
end
# if (self.server)
# Thread.kill(self.server)
# self.server = nil
# end
end
end # class
end # module

View File

@ -272,9 +272,23 @@ FactoryGirl.define do
factory :track, :class => JamRuby::Track do
sound "mono"
sequence(:client_track_id) { |n| "client_track_id_seq_#{n}"}
end
factory :video_source, :class => JamRuby::VideoSource do
#client_video_source_id "test_source_id"
sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"}
end
factory :recording, :class => JamRuby::Recording do
association :owner, factory: :user
association :music_session, factory: :active_music_session
factory :recording_with_track do
before(:create) { |recording|
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner)
}
end
end
factory :recorded_track, :class => JamRuby::RecordedTrack do
instrument JamRuby::Instrument.first
@ -289,16 +303,13 @@ FactoryGirl.define do
association :recording, factory: :recording
end
factory :recording, :class => JamRuby::Recording do
association :owner, factory: :user
association :music_session, factory: :active_music_session
factory :recording_with_track do
before(:create) { |recording|
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner)
}
end
factory :recorded_video, :class => JamRuby::RecordedVideo do
sequence(:recording_id) { |n| "recording_id-#{n}"}
sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"}
fully_uploaded true
length 1
association :user, factory: :user
association :recording, factory: :recording
end
factory :claimed_recording, :class => JamRuby::ClaimedRecording do

View File

@ -0,0 +1,53 @@
require 'spec_helper'
require 'youtube_client'
describe "OAuth", :slow=>true, :js=>true, :type=>:feature, :capybara_feature=>true do
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
@previous_run_server = Capybara.run_server
Capybara.run_server = false
@user=FactoryGirl.create(:user, :email=>"jamkazamtest@gmail.com")
end
before(:each) do
@youtube_client = YouTubeClient.new()
end
after(:each) do
@youtube_client.shutdown if @youtube_client
@youtube_client=nil
@user.user_authorizations.destroy_all
#page.driver.remove_cookie(:remember_token)
end
after(:all) do
@user.destroy
Capybara.run_server = @previous_run_server
end
it "client should not authorize a wrong password" do
expect {
authorize_google_user(@youtube_client, @user, "f00bar")
}.to raise_error
@user.reload
@user.user_authorizations.count.should eq(0)
end
it "client should authorize a google user" do
authorize_google_user(@youtube_client, @user, "stinkyblueberryjam")
save_screenshot("working.png")
@user.reload
@user.user_authorizations.count.should eq(1)
google_auth = UserAuthorization.google_auth(@user).first
google_auth.should_not be_nil
google_auth.token.should_not be_nil
end
end

View File

@ -0,0 +1,89 @@
require 'spec_helper'
require 'youtube_client'
require 'rest_client'
describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature => true do
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
@previous_run_server = Capybara.run_server
Capybara.run_server = false
@user=FactoryGirl.create(:user, :email => "jamkazamtest@gmail.com")
@youtube_client = YouTubeClient.new()
authorize_google_user(@youtube_client, @user, "stinkyblueberryjam")
google_auth = UserAuthorization.google_auth(@user).first # Consider returning this from above now that it is reliable
end
after(:all) do
@user.destroy
@youtube_client.shutdown
Capybara.run_server = @previous_run_server
end
it "should retrieve upload url" do
length = 3276
upload_hash=@youtube_client.upload_sign(@user, "test_video.mp4", length)
upload_hash.should_not be_nil
upload_hash.length.should be >=1
upload_hash['method'].should eq("PUT")
upload_hash['url'].should_not be_nil
upload_hash['Authorization'].should_not be_nil
upload_hash['Content-Length'].should_not be_nil
upload_hash['Content-Length'].should eq(length)
upload_hash['Content-Type'].should_not be_nil
@youtube_client.verify_upload(@user, upload_hash['url'], length).should be_false
end
it "upload url should allow uploading" do
vid_path = Rails.root.join('spec', 'files', 'test_video.mp4')
length = File.size?(vid_path)
upload_hash=@youtube_client.upload_sign(@user, "test_video.mp4", length)
#puts upload_hash.inspect
upload_hash.should_not be_nil
upload_hash.length.should be >=1
upload_hash['method'].should eq("PUT")
upload_hash['url'].should_not be_nil
upload_hash['Authorization'].should_not be_nil
upload_hash['Content-Length'].should_not be_nil
upload_hash['Content-Length'].should eq(length)
upload_hash['Content-Type'].should_not be_nil
# Upload this file as the client would:
RestClient.put(upload_hash['url'], File.read(vid_path))
@youtube_client.verify_upload(@user, upload_hash['url'], length).should be_true
#@youtube_client.get_upload_status(@user, upload_hash['url'], length)
end
it "sets upload flag when complete" do
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
@connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session)
@video_source = FactoryGirl.create(:video_source, :connection => @connection)
@recording = FactoryGirl.create(:recording, owner: @user, band: nil, duration:1)
vid_path = Rails.root.join('spec', 'files', 'test_video.mp4')
length = File.size?(vid_path)
upload_hash=@youtube_client.upload_sign(@user, "test_video.mp4", length)
upload_hash.should_not be_nil
upload_hash['url'].should_not be_nil
RestClient.put(upload_hash['url'], File.read(vid_path))
recorded_video = FactoryGirl.create(:recorded_video,
recording: @recording,
user: @recording.owner,
fully_uploaded: false,
url: upload_hash['url'],
length: length
)
@recording.recorded_videos << recorded_video
@youtube_client.verify_upload(@user, upload_hash['url'], length).should be_true
@youtube_client.complete_upload(recorded_video).should be_true
recorded_video.fully_uploaded.should be_true
end
end

Binary file not shown.

View File

@ -192,10 +192,11 @@ bputs "before register capybara"
end
config.before(:each) do
if example.metadata[:js]
if example.metadata[:js] && (Capybara.current_driver.nil? || Capybara.current_driver.empty? || Capybara.current_driver==:poltergeist)
page.driver.resize(1920, 1080)
page.driver.headers = { 'User-Agent' => 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0' }
end
end
config.before(:each, :js => true) do
@ -212,7 +213,6 @@ bputs "before register capybara"
Capybara.reset_sessions!
reset_session_mapper
end
config.after(:each) do

View File

@ -73,6 +73,45 @@ def wipe_s3_test_bucket
end
end
def authorize_google_user(youtube_client, user, google_password)
youtube_client.wait_for_callback(2112) do |access_token|
#puts("Authorizing with token #{access_token}")
user_auth_hash = {
:provider => "google_login",
:uid => user.email,
:token => access_token,
:token_expiration => nil,
:secret => ""
}
authorization = user.user_authorizations.build(user_auth_hash)
authorization.save
end
url = youtube_client.get_login_url(user.email)
#puts("Login URL: #{url}")
visit url
sleep(1)
# save_screenshot("initial.png")
# Fill in password (the username is filled in with a hint in URL):
# fill_in "Usernm", with: user.email
fill_in "Passwd", with: google_password
#save_screenshot("password.png")
find('#signIn').trigger(:click)
# Wait for submit to enable and then click it:
sleep(5)
#save_screenshot("signin.png")
#save_screenshot("submit.png")
find('#submit_approve_access').trigger(:click)
#save_screenshot("log2.png")
sleep(5)
#save_screenshot("submitted.png")
youtube_client
end
def sign_in(user)
visit signin_path
@ -117,6 +156,7 @@ def sign_in_poltergeist(user, options = {})
end
visit signin_path
page.should have_selector('#landing-inner form.signin-form')
within('#landing-inner form.signin-form') do
fill_in "Email Address:", with: user.email

View File

@ -0,0 +1,28 @@
For access to the youtube and google APIs, we need an access_token
To obtain an access token, one must actually log into google using a browser running javascript. This redirects to the URL specified, as long as it is specified in the oauth configuration.
Getting an access token for the purposes of automated testing is tricky, but possible using Capybara with a javascript-enabled driver. (Note, web/spec/support/utilities.rb utilizes the JK youtube client to perform the intricate bits):
1) Obtain the login URL. It's ugly, but we can get it from the YouTubeClient. It contains the callback URL, as well as a "hint" that will fill in the username for us.
2) Start a web server on an enabled callback server, such as localhost:3000
3) Obtain the URL using a known test user
4) Visit the URL in a capybara test
4a) Fill in password with the right value
4b) Click the login button
4c) The approve page should load. Wait for the approve button to be enabled. This is usually a second or two after the page loads, but not immediately.
4d) Click the approve button
5) After google approves, some javascript will redirect to our test web server, which contains a code. This is not the access_token, but a one-time code that can be exchanged for an access_token, again POSTing to google's auth server. You can see it in gory detail in YouTubeClient.exchange_for_token.
6) If all goes well, the test web server will call back the invoker with a real access token.
7) For testing purposes, stick the access token in the user.user_authorizations table for the user for which we are testing.
Notes:
* When authenticating, client_id is required by several of the APIs. However, this doesn't work for /o/oauth2/token. What actually works is the "email" value from the developer console. This is now saved in the app as well.
The tests in question use the following credentials:
u: jamkazamtest@gmail.com
p: stinkyblueberryjam
Also, a server is started on port 2112, as 3000 was already being used on the build server.

BIN
web/submit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
web/submitted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
web/working.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB