YouTube on Rails

Ilya Bodrov

yt-rails

YouTube. The third most visited resource in the world. We have all visited it and, probably, uploaded videos to it. But how can we, as Rails developers, fetch information about YouTube videos? How can we display them on our website? Are there any libraries that can help us with that? Let’s investigate, shall we?

In this article, I will show you how to create a web application that allows users to add and watch videos from YouTube. Upon adding, the video’s info will be fetched automatically via the YouTube API. This app will also display these added videos using the typical YouTube player with the help of the YouTube IFrame API. Lastly, there are some potential problems and gotchas that you might face.

The source code is available on GitHub.

The working demo can be found on Heroku.

Preparing a Demo Application

For this article I will be using Rails 3.2.17, but you can implement practically the same solution with Rails 4.

Start by creating a new Rails app without the default testing suite:

$ rails new yt_videos -T

First of all, we should grab the gems that will be used in this demo. We are going to use youtube_it, a convenient gem written by Kyle J. Ginavan, to work with the YouTube API. For styling, let’s use our old friend Twitter Bootstrap and, specifically, the bootstrap-sass gem. Go ahead and add these two lines into your Gemfile:

Gemfile

gem 'bootstrap-sass', '~> 3.1.1.0'
gem 'youtube_it', '~> 2.4.0'

Then run

$ bundle install

Now add

//= require bootstrap

to the application.js and

@import "bootstrap";

to the application.css.scss to include the Bootstrap styles and scripts. Of course, in a real application you would choose only the required components.

We should think about what our Video model will look like. Of course, it should contain a link to the video that the user has added. The YouTube API only supplies the unique video identifier (which we will learn to extract, shortly), so we’ll need to grab it. Also, we’ll want to store some info about the video. Here is the list of all attributes for our ActiveRecord model:

  • id – integer, primary key, indexed, unique
  • link – string
  • uid – string. It is also a good idea to add an index here to enforce uniqueness. For this demo, we don’t want users to add the same videos multiple times
  • title – string. This will contain the video’s title extracted from YouTube
  • author – string. Author’s name extracted from YouTube
  • duration – string. We will store the video’s duration, formatted like so: “00:00:00″
  • likes – integer. Likes count for a video
  • dislikes – integer. Dislikes count for a video.

That will be enough for the purposes of this demo, but keep in mind that the YouTube API allows you to fetch a lot more information.

Run this command to create a model and a corresponding migration:

$ rails g model Video link:string title:string author:string duration:string likes:integer dislikes:integer

Now set up the routes:

routes.rb

resources :videos, only: [:index, :new, :create]
root to: 'videos#index'

We will not need the edit, update, show and destroy actions for the purposes of this demo. Also, do not forget to remove the public/index.html file if you are working with Rails 3.

The last thing to do is add a block for rendering flash messages into the layout:

application.html.erb

[...]

<div class="container">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <button type="button" class="close" data-dismiss="alert">&times;</button>
      <%= value %>
    </div>
  <% end %>
</div>

[...]

Adding Videos

When a user visits our website, the first thing he probably should see is the “Add video” button.

videos/index.html.erb

<div class="jumbotron">
  <div class="container">
    <h1>Share your videos with the world!</h1>
    <p>Click the button below to share your video from YouTube.</p>
    <p>
      <%= link_to 'Add video now!', new_video_path, class: 'btn btn-primary btn-lg' %>
    </p>
  </div>
</div>

Thanks to Bootstrap, this block will look pretty nice. The next step is to implement the new action.

But let’s stop for a second and check the models/video.rb file. If you are using Rails 3, all the attributes (except for id, updated_at and created_at) were made accessible by default. The only thing we need from the user is the link to the video he wants to add. Other attributes will be populated automatically, so let’s make the appropriate modifications:

models/video.rb

[...]

attr_accessible :link

[...]

For Rails 4, you should only permit the link attribute in the controller:

params.require(:video).permit(:link)

Okay, now we can move on. Add these two methods into the controller:

controllers/videos_controller.rb

[...]

def new
  @video = Video.new
end

def create
  @video = Video.new(params[:video])
  if @video.save
    flash[:success] = 'Video added!'
    redirect_to root_url
  else
    render 'new'
  end
end

[...]

Nothing fancy going on here.

The view:

videos/new.html.erb

<div class="container">
  <h1>New video</h1>

  <%= form_for @video do |f| %>
    <%= render 'shared/errors', object: @video %>

    <div class="form-group">
      <%= f.label :link %>
      <%= f.text_field :link, class: 'form-control', required: true %>
      <span class="help-block">A link to the video on YouTube.</span>
    </div>

    <%= f.submit class: 'btn btn-default' %>
  <% end %>
</div>

As you can see, we only ask the user to enter a link to a video on YouTube. Here, also render a shared partial to display any errors, which looks like:

shared/_errors.html.erb

<% if object.errors.any? %>
  <div class="panel panel-danger">
    <div class="panel-heading">
      <h3 class="panel-title">The following errors were found while submitting the form:</h3>
    </div>

    <div class="panel-body">
      <ul>
        <% object.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  </div>
<% end %>

It is time for the fun part – extracting information about the video specified by the user. We are going to do this inside a callback before creating a record.

models/video.rb

before_create -> do
  # Our code here
end

To fetch information from the YouTube API, we will need the video’s unique identifier. Actually, this identifier is already present in the link that the user provides. The trick here is that these links can be specified in multiple formats, for example:

  • http://www.youtube.com/watch?v=0zM3nApSvMg&feature=feedrecgrecindex
  • http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/QdK8U-VIH_o
  • http://www.youtube.com/v/0zM3nApSvMg?fs=1&hl=en_US&rel=0
  • http://www.youtube.com/watch?v=0zM3nApSvMg#t=0m10s
  • http://www.youtube.com/embed/0zM3nApSvMg?rel=0
  • http://www.youtube.com/watch?v=0zM3nApSvMg
  • http://youtu.be/0zM3nApSvMg

All these URLs point to the same video with the id 0zM3nApSvMg (you can read more about it on StackOverflow).

It is also worth mentioning that a video’s uID contains 11 symbols. To fetch the uID from the link, we will use the following RegExp:

/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/i

Before we do that, though, let’s make sure we have a valid link:

models/video.rb

YT_LINK_FORMAT = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/i

validates :link, presence: true, format: YT_LINK_FORMAT

[...]

Ok, fetch the uID inside the callback:

models/video.rb

[...]

before_create -> do
  uid = link.match(YT_LINK_FORMAT)
  self.uid = uid[2] if uid && uid[2]

  if self.uid.to_s.length != 11
    self.errors.add(:link, 'is invalid.')
    false
  elsif Video.where(uid: self.uid).any?
    self.errors.add(:link, 'is not unique.')
    false
  else
    get_additional_info
  end
end

Here, there are some validations for the uid, specifically for checking its length and uniqueness. If everything is okay, call a method to fetch information about the video:

models/video.rb

[...]

private

def get_additional_info
  begin
    client = YouTubeIt::OAuth2Client.new(dev_key: 'Your_YT_developer_key')
    video = client.video_by(uid)
    self.title = video.title
    self.duration = parse_duration(video.duration)
    self.author = video.author.name
    self.likes = video.rating.likes
    self.dislikes = video.rating.dislikes
  rescue
    self.title = '' ; self.duration = '00:00:00' ; self.author = '' ; self.likes = 0 ; self.dislikes = 0
  end
end

def parse_duration(d)
  hr = (d / 3600).floor
  min = ((d - (hr * 3600)) / 60).floor
  sec = (d - (hr * 3600) - (min * 60)).floor

  hr = '0' + hr.to_s if hr.to_i < 10
  min = '0' + min.to_s if min.to_i < 10
  sec = '0' + sec.to_s if sec.to_i < 10

  hr.to_s + ':' + min.to_s + ':' + sec.to_s
end

This is where youtube_it comes into play. First, a client variable is being created – it will be used to issue queries. You might be wondering “What is a YouTube developer key?” This key is used to issue queries against the public YouTube data. Please note that some actions require user authorization, which you can read more about here.

To get your developer key, register a new app at https://code.google.com/apis/console. Open its settings, go to “APIs & auth”, then “Credentials”. Create a new public key for browser applications, which is your developer key.

After initializing a client, fetch the video using video_by, getting the information about it. Note that the video’s duration is presented in seconds, so we have to implement a parse_duration method to format it the way we want.

At this point our app allows users to add their videos. It also fetches some info and provides validation for the user input. Nice, isn’t it?

Displaying the Videos

OK, we have the videos, now to display them. Add the following to your controller:

controllers/videos_controller.rb

def index
  @videos = Video.order('created_at DESC')
end

On to the view:

videos/index.html.erb

[...]

<% if @videos.any? %>
  <div class="container">
    <h1>Latest videos</h1>

    <div id="player-wrapper"></div>

    <% @videos.in_groups_of(3) do |group| %>
      <div class="row">
        <% group.each do |video| %>
          <% if video %>
            <div class="col-md-4">
              <div class="yt_video thumbnail">
                <%= image_tag "https://img.youtube.com/vi/#{video.uid}/mqdefault.jpg", alt: video.title,
                              class: 'yt_preview img-rounded', :"data-uid" => video.uid %>

                <div class="caption">
                  <h5><%= video.title %></h5>
                  <p>Author: <b><%= video.author %></b></p>
                  <p>Duration: <b><%= video.duration %></b></p>
                  <p>
                    <span class="glyphicon glyphicon glyphicon-thumbs-up"></span> <%= video.likes %>
                    <span class="glyphicon glyphicon glyphicon-thumbs-down"></span> <%= video.dislikes %>
                  </p>
                </div>
              </div>
            </div>
          <% end %>
        <% end %>
      </div>
    <% end %>
  </div>
<% end %>

The #player-wrapper is an empty block where the YouTube player will be shown. The in_groups_of method groups our records by 3 and to displays them in rows. Please note, if there is not enough elements to form a group, Rails replaces each missing element with nil, so we have to add a if video condition.

Another important thing to mention is the video preview image. To get a preview image for a video, use one of the following links:

  • https://img.youtube.com/vi/mqdefault.jpg – 320×180 image with no black stripes above and below the picture;
  • https://img.youtube.com/vi/hqdefault.jpg – 480×360 image with black stripes above and below the picture;
  • https://img.youtube.com/vi/<1,2,3>.jpg – 120×90 image with different scenes from the video with black stripes above and below the picture.

Also, when rendering video preview images we are storing video’s uID using HTML5 data-* attribute. This attribute will be used shortly.

To display the actual videos on our website, the YouTube IFrame API will be used. It creates the YouTube player with certain parameters, allowing the user to change the video, pause and stop it, etc. You can read more about it here. Hook up the required javascript files like this:

layouts/application.html.erb

[...]

  <script src="https://www.google.com/jsapi"></script>
  <script src="https://www.youtube.com/iframe_api"></script>
</head>

[...]

Now, let’s write some CoffeeScript code in a new yt_player.coffee file:

javascripts/yt_player.coffee

jQuery ->
  # Initially the player is not loaded
  window.ytPlayerLoaded = false

  makeVideoPlayer = (video) ->
    if !window.ytPlayerLoaded
      player_wrapper = $('#player-wrapper')
      player_wrapper.append('<div id="ytPlayer"><p>Loading player...</p></div>')

      window.ytplayer = new YT.Player('ytPlayer', {
        width: '100%'
        height: player_wrapper.width() / 1.777777777
        videoId: video
        playerVars: {
          wmode: 'opaque'
          autoplay: 0
          modestbranding: 1
        }
        events: {
          'onReady': -> window.ytPlayerLoaded = true
          'onError': (errorCode) -> alert("We are sorry, but the following error occured: " + errorCode)
        }
      })
    else
      window.ytplayer.loadVideoById(video)
      window.ytplayer.pauseVideo()
    return
  return

First of all, we initialize the ytPlayerLoaded boolean variable which checks whether the player has been loaded. After that, create a makeVideoPlayer function which takes one argument – the video’s uID – and creates a YouTube player or changes the video being played. If the player is not yet loaded, we append a new #ytPlayer block to the #player-wrapper which is eventually replaced by the player. With all that in place, finally create the YouTube player object assigning it to the ytplayer (we will use it to call API functions).

Let’s stop for a second a talk a bit about the arguments that are being passed when creating this object. ytPlayer is the DOM id of the block that should be replaced with the player. The second argument is a javascript object containing settings for the player:

  • width – width of the player. Our site has responsive layout so we set it to 100%
  • height – height of the player. We have to calculate it based on the width. The typical resolution for YouTube is 16:9 which means 1.7(7) ratio
  • videoId – uID of the video to load
  • wmode – this parameter controls layering and transparency of Flash object (learn more). If we do not set it to opaque, the player will overlay modal windows (for example the ones created with jQuery UI) and look terrible
  • autoplay – set to 1, the video will play on load. In this demo, we do not want this to happen
  • modestbranding – if this is set to 1, the YouTube logo will not be shown in the control bar, but still will be visible in the upper right corner when the video is paused
  • events – here we specify two events to handle. When the player has loaded (onReady), set ytPlayerLoaded to true. If an error occurrs while loading the player or video (for example, this video was deleted), alert the user.

If the player has already been loaded, we use loadVideoById function to change the video being played and then pause it using pauseVideo (for demonstration purposes).

Okay, our function is ready. We want to load the player and display the latest video as soon as user opens our website. How do we achieve this? We are going to use a special function presented by Google API:

javascripts/yt_player.coffee

[...]

_run = ->
  # Runs as soon as Google API is loaded
  $('.yt_preview').first().click()
  return

google.setOnLoadCallback _run

[...]

This google.setOnLoadCallback _run means that the _run function should be called as soon as the API has finished loading. If we do not use this callback and try to load the player as soon as DOM is ready, an error occurs stating that YT.Player is not a function – this is because API has not loaded yet.

The last thing to do is to bind a click event handler to the video previews:

javascripts/yt_player.coffee

[...]

$('.yt_preview').click -> makeVideoPlayer $(this).data('uid')

[...]

And that is all! Now our users can add videos and watch them on our website.

Actually, there is one small problem left. We specified the player’s width in percent, but the player’s height is specified in pixels. That means that if a user tries to resize the window, the player will become narrower, but it will preserve the same width – this is bad. To solve this, we can bind a resize event handler to the window like this:

javascripts/yt_player.coffee

[...]

$(window).on 'resize', ->
  player = $('#ytPlayer')
  player.height(player.width() / 1.777777777) if player.size() > 0
  return

[...]

Now the player’s height will be rescaled correctly – go on and give it a try. You can also delay firing this event until the user stops resizing, otherwise it will be fired constantly. To do this, use the BindWithDelay library written by Brian Grinstead. It is super simple:

javascripts/yt_player.coffee

[...]

$(window).bindWithDelay('resize', ->
  player = $('#ytPlayer')
  player.height(player.width() / 1.777777777) if player.size() > 0
  return
, 500)

[...]

With this structure we are delaying firing this event by 500ms.

Conclusion

This brings us to the end of the article. We talked about the youtube_it gem which can be used to work with the YouTube API and about the YouTube IFrame API. I hope that information provided here was useful. Do not forget to share your opinion about this article in the comments. See you again soon!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://helio.arq.br/ hlegius

    Just a tip: avoid use before/after model’s callback to perform self modifications. Callbacks are available to call another (object) message(method) to perform additional tasks, like enqueue, mailer, etc. Despite of this, very good writing. Thanks!

  • RyDog

    I’m learning to program, and this is a great resource for me. Thanks a lot for doing this!

    • Ilya Bodrov

      Good luck on this hard but interesting journey :)

  • http://sebastien.saunier.me/ Sébastien Saunier

    Awesome in-depth blog post!

    • Ilya Bodrov

      Thank you!

  • Ilya Bodrov

    Interesting solution, thanks! But as far as I understood it does not allow to fetch likes/dislikes and some more advanced info (comments for a video, for example). Still this is a way to go to get basic data.

    • http://avsej.net Sergey Avseyev

      yeah, you are correct, it doesn’t nearly covers youtube API, but rather looks on it from the different angle

      • Ilya Bodrov

        Well, maybe one of my next articles will cover this lib :)

  • Ilya Bodrov

    Thanks for sharing that, just started watching. There is a point in what you say, really.

  • fred

    Thanks for your input. It’s not because I don’t sound as pedantic as you that I don’t know things like (design patterns) or architecture..

    PS: You can trust me about not trusting what I read without experimenting first !

    Cheers

  • Sampriti Panda

    You need to add the uid the field to the model generator. So that should be something like this.

    rails g model Video link:string title:string uid:string author:string duration:string likes:integer dislikes:integer

    • Kalman Hazins

      Nice article! The model generation can be further simplified by omitting the :string type, which is the default.

      rails g model Video link title uid author duration likes:integer dislikes:integer

      • Ilya Bodrov

        True, but I like to be more specific, moreover not all the readers might know about that fact :)

  • Cevn

    This was working for me up until a day ago, now when I click to play a video it just says an error has occurred: [object Object]. Any idea what the problem could be?

    • Ilya Bodrov

      The video you added might be removed or marked as private or embedding might be disabled.

      • Cevn

        I never figured out what happened unfortunately – switched to iframes for the player. Thanks for the help though!

  • Kalman Hazins

    Wonderful write up! You seem to have a talent for taking complicated topics and breaking them down in an easy-to-follow, clearly explained manner. Looking forward to more articles from you.

  • Kalman Hazins

    Two minor points:

    1. The format: validation is technically not needed, because you anyways check the format in the before_create callback and only assign a format if you are able to extract it with a regular expression.

    2. attr_accessible for one attribute is kinda ironic. The whole point of attr_accessible is a protection against mass assignment, i.e. assigning multiple attributes and being “lazy” (like all good developers are :)) and not assigning each attribute individually. For one attribute, you might as well just assign it directly and not bother with attr_accessible as in

    @video = Video.new(link: params[:video][:link])

    • Ilya Bodrov

      Yeah, thanks for pointing that out. attr_accessible was generated here automatically, just forgot to remove that :)

  • Ilya Bodrov

    You definetely need to use begin-rescue block to catch such errors

  • APOORV PUROHIT

    I am facing the following error
    Unable to autoload constant VideosController, expected C:/Users/apoorv/yt_videos/app/controllers/videos_controller.rb to define it

    • APOORV PUROHIT

      anyone? , help !