YouTube API v3 on Rails (phần 1)

Ai cũng biết rằng, YouTube là cộng đồng chia sẻ video lớn nhất tính đến thời điểm hiện tại. Việc tích hợp Youtube vào một application rất có ích trong việc tiết kiệm dung lượng lưu trữ, tăng chất lượng video, tăng tính tương tác cộng đồng và mở ra nhiều lựa chọn mới cho việc thiết kế giao diện một app.

Để hỗ trợ cho việc này, ngày 20/04/2015 Google đã tung ra YouTube API v3 giúp cho việc giao tiếp với YouTube trở nên thuận lợi hơn. Cùng với đó, cộng đồng phát triển Rails đã tung ra gem yt, thay thế cho gem youtube_it dành cho API v2.

Bài viết này nhằm hỗ trợ mọi người có thể tích hợp YouTube API dễ dàng vào một Rails App. Serie này dự kiến gồm hai bài viết. Phần 1 sẽ giới thiệu các cài đặt môi trường và xây dựng một ứng dụng cho phép lấy và chơi video trên YouTube. Phần 2 sẽ hướng dẫn cách upload video lên YouTube từ app và lưu trữ video vào database của app.

Chuẩn bị App

App của chúng ta sẽ có các chức năng sau:

  • User có thể đăng lên app các video có sẵn của YouTube.
  • Video sẽ được hiện lên và có thể xem được nội dung cùng một số thông tin cơ bản.
  • User có thể upload video lên YouTube thông qua app. Các video có thể được lưu lại trên database của app (sẽ được giới thiệu ở phần 2).

Bắt đầu tạo app từ các bước cơ bản:

$ rails new YtVideosV3 -T

Đưa các gem sau vào Gemfile:

Gemfile

[...]
gem 'yt'
gem 'bootstrap-sass'
gem 'autoprefixer-rails'
[...]

Gem quan trọng nhất ở đây là yt. Các gem bootstrap-sass và autoprefixer-rails để tiện cho việc trang trí..

Đừng quên chạy:

$ bundle install

Cài đặt Bootstrap:

application.scss

@import "bootstrap-sprockets";
@import "bootstrap";
@import 'bootstrap/theme';

Bây giờ chuẩn bị layout một chút:

layouts/application.html.erb

[...]
<div class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'YT APIv3', 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 %>
</div>
[...]

Tiếp theo, ta tạo một model, gọi là Video, dùng để chứa video của user. Model sẽ có những thuộc tính sau:

  • link (string) – link đến video trên YouTube
  • uid (string) – uid của video trên YouTube. Ta nên thêm index cho trường này
  • title (string) – title của video
  • published_at (datetime) – ngày video được đăng lên YouTube
  • likes (integer) – số likes của video
  • dislikes (integer) – số dislikes của video

Tạo route cho model Video:

config/routes.rb

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

Tạo controller:

videos_controller.rb

class VideosController < ApplicationController
  def index
    @videos = Video.order('created_at DESC')
  end

  def new
    @video = Video.new
  end

  def create
  end
end

Trên trang index, ta cho hiện tất cả các video được đăng lên bởi user. Ngoài ra, còn có một trang form đến thêm các video. Việc tạo ra trang show cho từng video cũng tương tự như trang index.

Trang index của app chỉ cần một nút để add video:

views/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>

Thêm video từ YouTube

Phần này sẽ nói về cách tạo một form đế thêm một video bất kì trên YouTube vào app. Việc duy nhất ta cần biết là link YouTube của video đó, các thông tin khác của video sẽ được truy vấn bằng YouTube API. Vì thế nên form có thể thiết kế rất đơn giản:

views/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-primary' %>
  <% end %>
</div>

Ta có thể thêm một partial errors nếu muốn:

views/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 %>

Create action trên controller:

videos_controller.rb

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

private

def video_params
  params.require(:video).permit(:link)
end
[...]

Thêm validation để hạn chế user post link lỗi:

models/video.rb

class Video < ActiveRecord::Base
  YT_LINK_FORMAT = /\A.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/i

  validates :link, presence: true, format: YT_LINK_FORMAT
end

Setting up YouTube API

Trước khi tiếp tục, ta cần phải config gem yt đế có thể kết nối với YouTube API. Đầu tiên, sử dụng Google Developers Console để đăng ký một application mới, đặt phương thức authenticate là OAuth 2.

Mở trang APIs và enable các API:

  • Google+ API
  • YouTube Analytics API
  • YouTube Data API v3

Nếu bạn quên mất một trong các bước tên, gem yt sẽ báo lỗi khi app cố gắng kết nối với YouTube API. Ngoài ra cần chú ý các API này đều có hạn mức sử dụng dữ liệu, cần chú ý trước khi gửi bất cứ thứ gì lên API.

Bước cuối cùng là lấy server key cho API: Đến trang Credentials và nhấn nút "Create new key" trong mục "Public API". Sau đó chọn Server key và gõ địa chỉ IP của server để cho các request không thể được gửi từ một IP khác. Nếu không có địa chỉ IP tĩnh, ta có thể để trống phần này, chỉ cần ấn "Create"

Tạo file init cho yt:

config/initializers/yt.rb

Yt.configure do |config|
  config.api_key = 'your_server_key'
end

API key cần được bảo mật, có thể sử dụng biến môi trường để lưu trữ.

Query YouTube API

Trước khi video có thể được lưu vào database, các thông tin liên quan có thể được tải về từ YouTube. Ta có thể dùng callback để thực hiện chức năng này. Bài viết này giới thiệu một cách khác là video observers:

Gemfile

[...]
gem 'rails-observers'
[...]

cài đặt gem và chỉnh sửa config:

config/application.rb

[...]
config.active_record.observers = :video_observer
[...]

Tạo một observer trong thư mục models:

models/video_observer.rb

class VideoObserver < ActiveRecord::Observer
  def before_save(resource)
    video = Yt::Video.new url: resource.link
    resource.uid = video.id
    resource.title = video.title
    resource.likes = video.like_count
    resource.dislikes = video.dislike_count
    resource.published_at = video.published_at
  rescue Yt::Errors::NoItems
    resource.title = ''
  end
end

Phương thức before_save chỉ được chạy trước khi record được lưu. Phương thức này chấp nhận một resource là một tham số. Bên trong method, ta sử dụng hàm Yt::Video.new để lấy video thông qua API bẳng URL. Sau đó, ta chỉ cần sử dụng các hàm của yt để lấy các thông tin cần thiết.

Thêm vào rescue Yt::Errors::NoItems để tránh trường hợp bị lỗi do video không tồn tại

Bây giờ ta có thể thêm video vào app tùy ý.

Chạy video

Bây giờ ta chỉnh sửa trang index một chút để có thể chạy các video trên đó:

views/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>

<% 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">
                <%= 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">
                  <h5><%= video.title %></h5>
                  <p>Published at <%= video.published_at.strftime('%-d %B %Y %H:%M:%S') %></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 %>

in_groups_of là một hàm của Rails giúp chia một array các video ra thành các group, mỗi group là số phần tử định sẵn (ở đây là 3 phần tử). Sau đó ta render mỗi nhóm riêng biệt. Đoạn code if video để đảm bảo phần tử nil trong group (nếu có) sẽ được bỏ qua mà không gây lỗi chương trình.

Với mỗi video, ta cho hiện ảnh thumbnail, được YouTube tạo. File mqdefault.jpg có nghĩa là ta muốn lấy ảnh cỡ 320x180 và không có dải đen ở trên và dưới ảnh. Ta còn có hqdefault.jpg là ảnh 480x360 và có hai dải đen ở trên và dưới ảnh và các ảnh <1, 2, 3>.jpg là các ảnh cỡ 120x90 là các ảnh từ các cảnh khác nhau của video và có dải đen ở trên và dưới ảnh

Mỗi thumbnail là một link đến video YouTube. Link này có thể là link trực tiếp đến trang YouTube hoặc là một YouTube IFrame API hỗ trợ chơi video trực tiếp trên trang.

Để tích hợp YouTube IFrame API ta cần các add file script từ google:

layouts/application.html.erb

[...]

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

[...]

Viết thêm một chút script:

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

Đầu tiên, ta khởi tạo biến boolean ytPlayerLoaded để kiểm tra player đã được tải chưa. Sau đó, hàm makeVideoPlayer sẽ lấy một tham số - uid của video - để tạo một YouTube player hoặc thay đổi video đang được chơi. Nếu player chưa được load, ta thêm một block #ytPlayer vào #player-wrapper để thay thế player khác.

Ta dừng lại một chút để nhìn vào các tham số được truyền vào để tạo nên đối tượng này. ytPlayer là một DOM id để thay thế cho player. Tham số thứ hai là một đối tượng javascript chứa các setting cho player:

  • width – chiều rộng của player.
  • height – chiều cao của player.
  • videoId – uID của video.
  • wmode – chế độ màn hình của Flash object.
  • autoplay – nếu đặt là 1, video sẽ tự động phát sau khi load xong.
  • modestbranding – nếu đặt là 1, logo của YouTube sẽ không được hiện lên ở control bar, nhưng vẫn có thể thấy được ở góc trên bên phải khi pause video.
  • events – có hai events để handle: onReady chạy khi player được load xong, ở đây ta đặt ytPlayerLoaded thành true. Nếu có lỗi sảy ra trong khi load video thì sẽ thông báo đến người dùng.

Nếu player đã được load sẵn, ta dùng hàm loadVideoById để thay đổi video đang được chơi và pause bằng hàm pauseVideo

Bây giờ app đã có thể lấy video và chơi được video ngay trên nền app. Việc upload video lên từ app sẽ được giới thiệu ở phần 2 của serie.

Hẹn gặp lại trong tương lai gần.