YouTube on Rails
This article is out of date, as YouTube has released version 3 of their API. If you need a tutorial on using version 3, check out this post instead.
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, uniquelink
– stringuid
– 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 timestitle
– string. This will contain the video’s title extracted from YouTubeauthor
– string. Author’s name extracted from YouTubeduration
– string. We will store the video’s duration, formatted like so: “00:00:00”likes
– integer. Likes count for a videodislikes
– 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">×</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 to100%
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) ratiovideoId
– uID of the video to loadwmode
– this parameter controls layering and transparency of Flash object (learn more). If we do not set it toopaque
, the player will overlay modal windows (for example the ones created with jQuery UI) and look terribleautoplay
– set to1
, the video will play on load. In this demo, we do not want this to happenmodestbranding
– if this is set to1
, 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 pausedevents
– here we specify two events to handle. When the player has loaded (onReady), setytPlayerLoaded
totrue
. 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!