Uploading Videos to YouTube with Rails
In my previous articles, I’ve already explained how to integrate uploading in your app and how to make uploading process asynchronous (as well as upload multiple files). Today, we will take another step forward and create an app that allows users to upload videos to YouTube.
We’ll utilize the OAuth2 protocol, as well as youtube_it
gem to work with the YouTube API (if you wish to learn more about YT API take a look at my article, YouTube on Rails). At the end, I’ll cover some potential gotchas you might face (believe me, some of them are quite annoying).
The source code for the demo app is available on GitHub.
A working demo can be found at http://sitepoint-yt-uploader.herokuapp.com/.
Let’s get started!
Preparations
Rails 4.1.4 will be used for this demo, but the same solution can be implemented with Rails 3.
Before diving into the code, let’s stop for a second and talk about the app we are going to build. The user scenario is simple:
-
A user visits the main page and sees a list of already uploaded videos, along with big button encouraging the user to upload a video.
-
The user (hopefully) clicks this button and is taken to another page where they are asked to authenticate (we will talk about authentication in more detail later).
-
After successful authentication, a form is presented allowing the user to choose a video from their hard drive. The user can provide a title and description for the video, as well.
-
After submitting the form, the video is uploaded to YouTube. Our app gets the unique video id and stores it in the database (this unique id will be used to show the video on the main page).
-
The user is redirected to the main page where they see the newly uploaded video.
Basically, we have three tasks to solve:
* Do some ground work (creating controllers, models, views, some basic design)
* Create some kind of authentication
* Implement video uploading mechanism .
Let’s start with the ground work. Create a new Rails app without a default testing suite:
$ rails new yt_uploder -T
We are going to use Twitter Bootstrap to build our basic design, so drop this gem into your Gemfile:
[...]
gem 'bootstrap-sass'
[...]
Then rename application.css (in the stylesheets directory) to application.css.scss and change its contents to:
@import 'bootstrap';
@import 'bootstrap/theme';
Change the layout to take advantage of Bootstrap’s styles:
layouts/application.html.erb
[...]
<body>
<div class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'YT Uploader', root_path, class: 'navbar-brand' %>
</div>
<ul class="nav navbar-nav">
<li><%= link_to 'Videos', root_path %></li>
<li><%= link_to 'Add Video', new_video_path %></li>
</ul>
</div>
</div>
<div class="container">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>">
<%= value %>
</div>
<% end %>
<%= yield %>
</div>
</body>
[...]
We are using some routes that do not exist, so let’s fix that:
routes.rb
[...]
resources :videos, only: [:new, :index]
root to: 'videos#index'
[...]
Also, create the corresponding controller (we will create its methods later):
videos_controller.rb
class VideosController < ApplicationController
def new
end
def index
end
end
Lastly, create the views (we are going to flesh them out later):
videos/new.html.erb
<div class="page-header">
<h1>Upload Video</h1>
</div>
videos/index.html.erb
<div class="jumbotron">
<h1>YouTube Uploader</h1>
<p>Upload your video to YouTube and share it with the world!</p>
<p><%= link_to 'Upload video now!', new_video_path, class: 'btn btn-primary btn-lg' %></p>
</div>
Okay, now we have something to work with.
Authenticating the User
At this point, we are ready to think about authentication. Obviously, to be able to upload a video to the user’s channel, we need access. How can we accomplish this?
The Google API supports OAuth 2 protocol that enables a third-party application to obtain limited access to its services (you can read more about accessing Google APIs with OAuth 2 here).
The cool thing about this protocol is that the user does not grant a third-party application access to his whole account. Rather, it is given access only to the required services (this is controlled by the scope). Also, the third-party application never receives the user’s password – instead it gets the token that is used to issue API calls. This token can be used to only perform actions that were listed in the scope of the access and it has a limited lifespan. Moreover, the user can manually revoke access for any previously authorized third-party application from the account settings page. OAuth 2 is a very popular protocol supported by various websites (like Twitter, Facebook, Vkontakte and many others).
For Rails, there is the omniauth-google-oauth2 gem created by Josh Ellithorpe. It presents a strategy to authenticate with Google using OAuth2 and OmniAuth. OmniAuth, in turn, is a a library that standardizes multi-provider authentication for web applications created by Michael Bleigh and Intridea Inc. OmniAuth allows you to use as many different authentication strategies as you want so that users can authenticate via Google, Facebook, Twitter, etc. You only need is hook up the corresponding strategy (there are numerous available) or create your own.
Okay, how do we put this all together? Actually, it’s quite easy. First of all, add the required gem to the Gemfile:
Gemfile
[...]
gem 'omniauth-google-oauth2'
[...]
Now create a couple of routes:
routes.rb
get '/auth/:provider/callback', to: 'sessions#create'
get '/auth/failure', to: 'sessions#fail'
The /auth/:provider/callback
route is the callback where the user is redirected after successful
authentication. The /auth
part is defined by OmniAuth. :provider
, in our case, is google_oauth2
(as stated by the omniauth-google-oauth2’s documentation). In an app with many different strategies, :provider
would take other values (like twitter
or facebook
). /auth/failure
, as you probably guessed, is used when an error occurrs during the authentication phase.
Now, set up the actual OmniAuth strategy:
initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], scope: 'userinfo.profile,youtube'
end
We are using the google_oauth2
strategy here. The client id and client secret can be obtained by registering your app using the Google Console. Visit (console.developers.google.com)[https://console.developers.google.com/], click “Create Project”, and enter your project’s name. After it is created, open the “Consent Screen” page (from the left menu) and fill in the “Product Name” (I’ve called it “Sitepoint YT Upload Demo”) – this is what your users will see when authenticating via the app. All other fields are optional. Be sure to click “Save”.
Now open the “APIs” page. Here you can set with APIs your app will access. Enable the Google+ API and the YouTube Data API v3. Open the “Credentials” page and click the “Create new Client ID” button. In the popup choose “Web application”. For the “Authorized JavaScript origins”, enter your app’s URL (for this demo I entered “http://sitepoint-yt-uploader.herokuapp.com”).
In the “Authorized redirect URI”, enter your app’s URL plus /auth/google_oauth2/callback
(remember our routes file and the callback route /auth/:provider/callback
?). For this demo, I’ve entered http://sitepoint-yt-uploader.herokuapp.com/auth/google_oauth2/callback
. Click “Create Client ID” and you will see a newly created Client ID for web application. Here note the two values: Client ID and Client Secret. These are the values you need to paste into the omniauth.rb
initializer.
What about the scope
? As I mentioned, scope
lists the services that the app requires. The whole list of possible values can be on Google’s OAuth 2 Playground. There are other options that can be passed to the provider
method. The whole list is available at omniauth-google-oauth2’s page.
The last thing we todo here is catch any errors that happen during authentication and redirect the user to the previously created /auth/failure
route:
initializers/omniauth.rb
[...]
OmniAuth.config.on_failure do |env|
error_type = env['omniauth.error.type']
new_path = "#{env['SCRIPT_NAME']}#{OmniAuth.config.path_prefix}/failure?message=#{error_type}"
[301, {'Location' => new_path, 'Content-Type' => 'text/html'}, []]
end
[...]
Phew, we are done here!
Present the users with the actual authentication link:
videos/new.html.erb
<div class="page-header">
<h1>Upload Video</h1>
</div>
<p>Please <%= link_to 'authorize', '/auth/google_oauth2' %> via your Google account to continue.</p>
After clicking this link, the user will be redirected to Google’s authentication page to login. Google will review the list of permissions that our app is requesting (in our demo, these permissions will be accessing basic account information and managing YouTube account) .
Upon approving this request, the user is redirected back to our application (to the callback route that we’ve set up in the Google Console). Our callback method also gets the authentication hash data that may contain various information depending on the strategy and requested permissions. In our case, it contains the user’s name, photo, profile URL, token, and some other data (take a look at the sample auth hash here).
We need a controller to handle this callback:
sessions_controller.rb
class SessionsController < ApplicationController
def create
auth = request.env['omniauth.auth']
user = User.find_or_initialize_by(uid: auth['uid'])
user.token = auth['credentials']['token']
user.name = auth['info']['name']
user.save
session[:user_id] = user.id
flash[:success] = "Welcome, #{user.name}!"
redirect_to new_video_path
end
def fail
render text: "Sorry, but the following error has occured: #{params[:message]}. Please try again or contact
administrator."
end
end
request.env['omniauth.auth']
contains the authentication hash. The three values we want are the user’s unique id (so that we can find this user later in the table when he authenticates once again), the user’s full name and, most importantly, the token to issue requests to Google API. Also, we are storing the user’s id in the session to authenticate him in our app.
We also need a method to check if the user is authenticated:
application_controller.rb
[...]
private
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
helper_method :current_user
[...]
This method can now be used in the view:
videos/new.html.erb
<div class="page-header">
<h1>Upload Video</h1>
</div>
<% if current_user %>
<%# TODO: create form partial %>
<% else %>
<p>Please <%= link_to 'authorize', '/auth/google_oauth2' %> via your Google account to continue.</p>
<% end %>
We haven’t created the User
model yet, so let’s do it now:
$ rails g model User name:string token:string uid:string
$ rake db:migrate
Great! At this point, our users can authenticate via Google and provide our app with the access token to issue API requests. The next step is to implement the video uploading functionality.
Uploading Videos to YouTube
To start off, we need a Video
model. It will contain the following fields (not to mention the default id
,
created_at
and updated_at
):
title
(string
) – provided by the userdescription
(text
) – provided by the useruid
(string
) – will contain the video’s unique identifier we talked about earlier. This id is generated by YouTube when the video is uploaded. We will use this id to fetch some info via the YouTube API as well as generate the link.user_id
(integer
) – a key field to associate the video with a user (one user can have many videos).
Create the migration and apply it:
$ rails g model Video uid:string title:string description:text user:references
$ rake db:migrate
Don’t forget to set up a one-to-many relation on the user side:
models/user.rb
[...]
has_many :videos
[...]
Now, we are ready to proceed to the video’s form:
videos/new.html.erb
<div class="page-header">
<h1>Upload Video</h1>
</div>
<% if current_user %>
<%= render 'form' %>
<% else %>
<p>Please <%= link_to 'authorize', '/auth/google_oauth2' %> via your Google account to continue.</p>
<% end %>
Unfortunately, things get a bit complicated here. It appears that we need to create not one, but two forms to be able to upload a video to YouTube. Why is that?
The YouTube API requires us to first send the video data, like title and description, without the actual video file. In return, it responds with a special upload token and URL. We then have to submit another form with the token and the file to this URL. Pretty messy, isn’t it?
Of course, we could ask our users to submit two forms, but we are not going to do that. Users should not be
bothered with such complexity. Let’s use a bit of AJAX instead. I’ll proceed step-by-step so that you understand what is going on.
Tweak the controller a bit:
videos_controller.rb
[...]
def new
@pre_upload_info = {}
end
[...]
This @pre_upload_info
hash will store the video’s info – title and description. We are going to use it in the first form.
Now, create two forms in the partial:
videos/_form.html.erb
<%= form_tag '', id: 'video_pre_upload' do %>
<div class="form-group">
<%= text_field_tag :title, @pre_upload_info[:title], required: true,
placeholder: 'Title', class: 'form-control' %>
</div>
<div class="form-group">
<%= text_area_tag :description, @pre_upload_info[:description], required: true,
placeholder: 'Description', class: 'form-control' %>
</div>
<% end %>
<%= form_tag '', id: 'video_upload', multipart: true do %>
<%= hidden_field_tag :token, '' %>
<div class="form-group">
<%= file_field_tag :file, required: true %>
</div>
<% end %>
<button id="submit_pre_upload_form" class="btn btn-lg btn-primary">Upload</button>
<%= image_tag 'ajax-loader.gif', class: 'preloader', alt: 'Uploading...', title: 'Uploading...' %>
As you can see, there is only one button that we’ll equipp with some JavaScript in a moment. None of the forms has the action URL – it will come from JavaScript, as well. The first form (#video_pre_upload
) will be sent the first to obtain the upload token and URL for the second (#video_upload
) form.
We also use an AJAX loader image generated from ajaxload.info/ or take an already generated one). The loader should not be displayed upon page load, so apply this style:
stylesheets/application.css.scss
.preloader {
display: none;
}
We are also using placeholders for inputs. They are not supported by older browsers, so you may want to use the jquery-placeholder plugin by Mathias Bynens for backward compatibility.
Also if you are using Turbolinks it may be a good idea to add the jquery-turbolinks gem into your app.
Okay, now it is time for some jQuery:
videos/_form.html.erb
<script>
$(document).ready(function() {
var submit_button = $('#submit_pre_upload_form');
var video_upload = $('#video_upload');
submit_button.click(function () {
$.ajax({
type: 'POST',
url: '<%= get_upload_token_path %>',
data: $('#video_pre_upload').serialize(),
dataType: 'json',
success: function(response) {
video_upload.find('#token').val(response.token);
video_upload.attr('action', response.url.replace(/^http:/i, window.location.protocol)).submit();
submit_button.unbind('click').hide();
$('.preloader').css('display', 'block');
}
});
});
});
</script>
We are binding a click
event handler to the button. When this button is clicked, submit the first form to the get_upload_token
route (which we will create in a moment) expecting to get JSON in response.
Upon successful response, get the token and URL and set form values for #token
and #video_upload
‘s action, respectively. Also, note the response.url.replace(/^http:/i, window.location.protocol)
. This is optional, but if your page can be accessed with both http
and https
protocols, you will need this piece of code. This is because the URL that is returned by the YouTube API uses http and modern browsers will prevent sending the form using https
.
After submitting the form, hide the button and show the PacMan (loader) to the user. At this point, we are done with the view and can move on to the routes and controller.
Create two new routes:
routes.rb
[...]
post '/videos/get_upload_token', to: 'videos#get_upload_token', as: :get_upload_token
get '/videos/get_video_uid', to: 'videos#get_video_uid', as: :get_video_uid
[...]
The first one – get_upload_token
– is used in our partial when sending the first form. The second route will be used in just a moment.
On to the controller:
videos_controller.rb
[...]
def get_upload_token
temp_params = { title: params[:title], description: params[:description], category: 'Education',
keywords: [] }
if current_user
youtube = YouTubeIt::OAuth2Client.new(client_access_token: current_user.token,
dev_key: ENV['GOOGLE_DEV_KEY'])
upload_info = youtube.upload_token(temp_params, get_video_uid_url)
render json: {token: upload_info[:token], url: upload_info[:url]}
else
render json: {error_type: 'Not authorized.', status: :unprocessable_entity}
end
end
[...]
This is the method to request the upload token and URL. Note, in the temp_params
hash we can provide some
more video info: category and keywords. For this demo, I use the “Education” category but in a real app you may present the user with a dropdown list to choose one of the available categories.
Here is where the youtube_it gem comes into play so let’s drop it into the Gemfile:
Gemfile
[...]
gem 'youtube_it', github: 'bodrovis/youtube_it'
[...]
I am using my forked version of this gem because 2.4.0
has json
locked to an older version.
We are required to pass the developer key when requesting an upload token. This key can be also obtained using the Google Console. Open up your project that we created in the previous section, navigate to “APIs & Auth” – “Credentials”, and click “Create new key”. In the popup, choose “Browser key”, fill in “Accept requests from these HTTP referers”, and click “Create”. The newly generated “API key” is what you are looking for.
With the help of youtube_it
, we request the token and URL, sending a JSON response to the client.
Also note that, in the upload_token
method we provide get_video_uid_url
– this is the callback URL where the user will be redirected after the video is uploaded. As you remember, we’ve already created this route, so we just need to add a corresponding method:
videos_controller.rb
[...]
def get_video_uid
video_uid = params[:id]
v = current_user.videos.build(uid: video_uid)
youtube = YouTubeIt::OAuth2Client.new(dev_key: ENV['GOOGLE_DEV_KEY'])
yt_video = youtube.video_by(video_uid)
v.title = yt_video.title
v.description = yt_video.description
v.save
flash[:success] = 'Thanks for sharing your video!'
redirect_to root_url
end
[...]
A GET parameter id
, containing the video’s unique id, is added to the callback URL, which is stored in the database. We can also fetch other video info here, like title, description, duration, etc. (read more about it in my YouTube on Rails article).
Cool! One small piece is left: the index
page – we need to display all uploaded videos. This is the easiest part.
videos_controller.rb
[...]
def index
@videos = Video.all
end
[...]
videos/index.html.erb
<div class="jumbotron">
<h1>YouTube Uploader</h1>
<p>Upload your video to YouTube and share it with the world!</p>
<p><%= link_to 'Upload video now!', new_video_path, class: 'btn btn-primary btn-lg' %></p>
</div>
<div class="page-header">
<h1>Videos</h1>
</div>
<ul class="row" id="videos-list">
<% @videos.each do |video| %>
<li class="col-sm-3">
<div class="thumbnail">
<%= link_to image_tag("https://img.youtube.com/vi/#{video.uid}/mqdefault.jpg", alt: video.title,
class: 'img-rounded'),
"https://www.youtube.com/watch?v=#{video.uid}", target: '_blank' %>
<div class="caption">
<h4><%= video.title %></h4>
<p><%= truncate video.description, length: 100 %></p>
</div>
</div>
</li>
<% end %>
</ul>
We use image_tag("https://img.youtube.com/vi/#{video.uid}/mqdefault.jpg")
to generate the video’s thumbnail and "https://www.youtube.com/watch?v=#{video.uid}"
to generate a link to watch the video. As you see, the video’s uid comes in really handy.
Some styles:
stylesheets/application.css.scss
[...]
#videos-list {
margin: 0;
padding: 0;
li {
padding: 0;
list-style-type: none;
word-wrap: break-word;
.caption {
text-align: center;
}
}
}
And that’s it! Go ahead and upload some videos with your shiny, new app!
P.S.: A Couple of Gotchas
You should be aware of two gotchas. The first one is related to authentication. When a user who has not enabled YouTube in their channel tries to authenticate with your app (that requests access to YouTube API) an error will be returned (luckily we have a mechanism to catch those errors). So, you might want check for this exact error and instruct your users to visit www.youtube.com, create a channel, and then try to authenticate again.
The second is related to uploading. As you know, YouTube needs some time after uploading to analyze the video and generate thumbnails; the longer the video, the more time it takes to analyze it. So, right after the user has uploaded a video, the default thumbnail will be used on the main page. There are at least two solutions to this problem:
- You can just warn users about this fact on the “New video” page, explaining that the thumbnail will appear after about 3-5 minutes.
- You can create some kind of a background process that will periodically try to access the thumbnail and show the video on the main page only when the thumbnail is available (again, the user should be warned about this fact).
Conclusion
Finally, we’ve reached the end of this article. That was a lot to talk about, eh? Have you ever tried to implement similar functionality in your apps? How did you solve this task? Share your thoughts in the comments!
Thanks for reading and see you again soon!