Uploading Videos to YouTube with Rails

Share this article

Media Player App Icon

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:

  1. 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.

  2. 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).

  3. 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.

  4. 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).

  5. 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 user
  • description (text) – provided by the user
  • uid (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:

  1. You can just warn users about this fact on the “New video” page, explaining that the thumbnail will appear after about 3-5 minutes.
  2. 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!

Frequently Asked Questions (FAQs) about Uploading Videos to YouTube with Rails

How can I set up OAuth 2.0 for YouTube API in Rails?

To set up OAuth 2.0 for YouTube API in Rails, you need to first create a project in the Google Developers Console. After creating the project, go to the ‘Credentials’ section and create a new OAuth 2.0 client ID. Download the JSON file and rename it to ‘client_secrets.json’. Place this file in your Rails application root directory. Then, install the ‘google-api-client’ gem and require ‘google/apis/youtube_v3’ in your application. Now, you can use the YouTube Data API v3 with OAuth 2.0 in your Rails application.

How can I handle video upload errors in Rails?

To handle video upload errors in Rails, you can use the ‘rescue’ clause in your upload method. This will catch any exceptions that occur during the upload process. You can then display an error message to the user or log the error for debugging purposes. It’s important to handle these errors gracefully to provide a good user experience.

How can I upload a video to YouTube in the background in Rails?

To upload a video to YouTube in the background in Rails, you can use a background job library like Sidekiq or Resque. These libraries allow you to perform tasks in the background, freeing up your application to handle other requests. You can create a job that handles the video upload and then queue it to be performed in the background.

How can I monitor the progress of a video upload in Rails?

Monitoring the progress of a video upload in Rails can be achieved by using JavaScript and AJAX. You can send periodic AJAX requests to the server to check the status of the upload. The server can then respond with the current progress of the upload. This information can be displayed to the user in the form of a progress bar or percentage.

How can I add metadata to a video during upload in Rails?

To add metadata to a video during upload in Rails, you can use the ‘snippet’ property of the ‘YouTube::V3::Video’ object. This property allows you to set the title, description, tags, and category of the video. You can set these values before calling the ‘insert_video’ method to upload the video.

How can I test the video upload functionality in Rails?

Testing the video upload functionality in Rails can be done using RSpec and Capybara. You can write a feature spec that simulates a user uploading a video. This spec should check that the video is successfully uploaded and that the correct success message is displayed.

How can I limit the size of videos that can be uploaded in Rails?

To limit the size of videos that can be uploaded in Rails, you can use the ‘validate’ method in your model. This method allows you to set a maximum size for the uploaded file. If a user tries to upload a file that exceeds this size, they will receive an error message.

How can I allow users to upload multiple videos at once in Rails?

To allow users to upload multiple videos at once in Rails, you can use the ‘multiple’ attribute on the file input field in your form. This allows users to select multiple files from their file system. You can then iterate over these files in your controller and upload each one individually.

How can I delete a video from YouTube in Rails?

To delete a video from YouTube in Rails, you can use the ‘delete_video’ method of the ‘YouTube::V3::YouTubeService’ object. This method requires the ID of the video you want to delete. You can get this ID from the ‘id’ property of the ‘YouTube::V3::Video’ object that was returned when you uploaded the video.

How can I update a video’s metadata on YouTube in Rails?

To update a video’s metadata on YouTube in Rails, you can use the ‘update_video’ method of the ‘YouTube::V3::YouTubeService’ object. This method requires the ID of the video you want to update and a ‘YouTube::V3::Video’ object that contains the new metadata. You can set the new metadata on the ‘snippet’ property of this object.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week