YouTube API v3 on Rails (phần 2)

Tiếp nối phần 1, phần này sẽ tiếp túc hướng dẫn cách upload một video lên Youtube từ app rails bằng Youtube API v3 dưới sự trợ giúp của gem yt

Upload video lên YouTube

Authenticate bằng Google+

Trước tiên, ta cần có một hệ thống xác thực hoạt động để kết nối với Google. Youtube, cũng như phần lớn các ứng dụng của Google, hiện tại đang sử dụng giao thức OAuth 2 để xác thực và giao thức này yêu cầu ta phải pass một token đặc biệt được tạo ra khi user đăng nhập vào app.

Ta sẽ sử dụng gem omniauth-google-oauth2 để thực hiện điều này. Thêm dòng sau vào Gemfile::

Gemfile

[...]
gem 'omniauth-google-oauth2'
[...]

sau đó chạy trên console:

$ bundle install

Tạo một initializer chứa các setting để nạp vào mỗi khi app khởi động:

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, 'YT_CLIENT_ID', 'YT_CLIENT_SECRET', scope: 'userinfo.profile,youtube'
end

Ở đây, ta đang đăng ký một chiến lược mới tên là google_oauth2. Các biến môi trường YT_CLIENT_ID và YT_CLIENT_SECRET có thể được lấy từ Google Developer Console, ta đã dùng để đăng ký app từ phần trước. Trở về trang Google Developer Console, chọn app, chọn mục Credentials. Nhấn nút “Create new Client ID” và chọn “Web application”. Sau đó nhập URL của site vào trường “Authorized JavaScript” (nhập http://127.0.0.1:3000 với môi trường development)

Ở mục “Authorized redirect URIs”, nhập vào URL của site cộng với “/auth/google_oauth2/callback” (ví dụ: “http://127.0.0.1:3000/auth/google_oauth2/callback”). Một Client ID cho web application sẽ được tạo. Nhập Client ID vào YT_CLIENT_ID và Client Secret vào YT_CLIENT_SECRET.

Tham số thứ ba của chiến lược là scope làm việc, định nghĩa các action và app có thể thực hiện. userinfo.profile có nghĩa là ta muốn lấy về các thông tin cơ bản của người dùng (tên, ID và các thông tin khác); youtube có nghĩa là app sẽ có thể làm việc với YouTube account của người dùng.

Thêm một số đường dẫn vào routes:

config/routes.rb

[...]
get '/auth/:provider/callback', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy', as: :logout
[...]

Đường dẫn đầu tiên là callback route để redirect user đến sau khi xác thực thành công. Đường dẫn thứ hai dành cho user sau khi đăng xuất.

Ta cũng cần phải lưu trữ lại dữ liệu của user, vì vậy ta cần phải tạo thêm một bảng mới. Bảng user sẽ có các trường sau:

  • name (string) – tên của user
  • token (string) – token để thực hiện API requests.
  • uid (string) – Youtube ID của user. Ta sẽ đặt index và unique ở trường này.

Tạo migration:

$ rails g model User name:string token:string uid:string

thêm index:

xxx_create_users.rb

[...]
create_table :users do |t|
  [...]
end
add_index :users, :uid, unique: true
[...]

Ta cũng cần một controller mới với hai action:

sessions_controller.rb

class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(request.env['omniauth.auth'])
    session[:user_id] = user.id
    flash[:success] = "Welcome, #{user.name}"
    redirect_to root_url
  end

  def destroy
    session[:user_id] = nil
    flash[:success] = "Goodbye!"
    redirect_to root_url
  end
end

request.env['omniauth.auth'] chứa tất cả các thông tin được gửi từ server đến app (còn được gọi là "auth hash"). Bây giờ ta sẽ định nghĩa method from_omniauth:

models/user.rb

class User < ActiveRecord::Base
  class << self
    def from_omniauth(auth)
      user = User.find_or_initialize_by(uid: auth['uid'])
      user.name = auth['info']['name']
      user.token = auth['credentials']['token']
      user.save!
      user
    end
  end
end

Nói thêm một chút, ở đây ta dùng method find_or_initialize_by. Method này sẽ thử tìm một user có uid được truyền vào, và trả về kết quả tìm được. Nếu không tìm thấy bản ghi phù hợp, method sẽ trả về một object mới với giá trị uid được set sẵn. Method này được tạo ra để tránh việc đăng ký nhiều object giống nhau mà không tốn thêm thời gian để viết code kiểm tra (tương đương với việc tự viết code kiểm tra, nhưng Ruby thường khuyến khích việc viết code càng ngắn gọn càng tốt).

Bây giờ ta sẽ tạo một method mới để kiểm tra xem user đã đăng nhập hay chưa. Ta sẽ gọi tên method là current_user để cho dễ nhớ:

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
[...]

Method này chỉ đơn giản là kiểm tra xem session có chứa user_id hay không, và nếu có, tìm kiếm user theo id đó. Khai báo method này với helper_method để có thể sử dụng được trên view.

Cuối cùng, ta thêm một chút vào view menu:

views/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>
      <% if current_user %>
        <li><%= link_to 'Upload Video', new_video_upload_path %></li>
      <% end %>
    </ul>
    <ul class="nav navbar-nav pull-right">
      <% if current_user %>
        <li><span><%= current_user.name %></span></li>
        <li><%= link_to 'Log Out', logout_path, method: :delete %></li>
      <% else %>
        <li><%= link_to 'Log In', '/auth/google_oauth2' %></li>
      <% end %>
    </ul>
  </div>
</div>
[...]

Thêm chút CSS vào để cho đỡ xấu:

application.scss

[...]
.nav > li > span {
  display: block;
  padding-top: 15px;
  padding-bottom: 15px;
  color: #9d9d9d;
}

Uploading

Sau khi chuẩn bị xong, bây giờ ta có thể upload video lên Youtube. Để làm được điều này, ta cần tạo thêm một form để user có thể chọn một video trong máy và đặt title và description cho nó.

Ta có thể dùng lại VideosController, nhưng ở đây ta sẽ dùng một cách khác theo phương thức REST để việc validate được dễ dàng hơn.

Tạo một controller mới:

video_uploads_controller.rb

class VideoUploadsController < ApplicationController
  def new
    @video_upload = VideoUpload.new
  end

  def create
  end
end

Lát nữa ta sẽ trở về để định nghĩa các method cho controller. Trước mắt ta sẽ thêm vào routes:

config/routes.rb

[...]
resources :video_uploads, only: [:new, :create]
[...]

và một mục nữa trên menu:

views/layouts/application.html.erb

[...]
<ul class="nav navbar-nav">
  <li><%= link_to 'Videos', root_path %></li>
  <% if current_user %>
    <li><%= link_to 'Add Video', new_video_upload_path %></li>
  <% end %>
</ul>
[...]

Form mà chúng ta sẽ sử dụng có dạng:

views/video_uploads/new.html.erb

<div class="container">
  <h1>Upload video</h1>
  <% if current_user %>
    <%= form_for @video_upload do |f| %>
      <%= render 'shared/errors', object: @video_upload %>

      <div class="form-group">
        <%= f.label :file %>
        <%= f.file_field :file, class: 'form-control', required: true %>
      </div>

      <div class="form-group">
        <%= f.label :title %>
        <%= f.text_field :title, class: 'form-control', required: true %>
      </div>

      <div class="form-group">
        <%= f.label :description %>
        <%= f.text_area :description, class: 'form-control', cols: 3 %>
      </div>

      <%= f.submit 'Upload', class: 'btn btn-primary' %>
    <% end %>
  <% else %>
    <p>Please <%= link_to 'sign in', '/auth/google_oauth2' %>.</p>
  <% end %>
</div>

Ta phải kiểm tra xem user đã đăng nhập hay chưa, nếu không việc upload sẽ trả về lỗi. Form bao gồm ba trường: file video, title và description. Ta có thể thêm các trường khác liên quan, ví dụ như tag hoặc category của video (đừng quên chỉnh sửa model và controller để có thể nhận các trường này).

Về phần validate, ta không cần phải phân chia hay tạo thêm bảng mới, video sẽ được upload thẳng vào bảng video hiện tại. Bình thường để upload lên, ta có thể đẩy trực tiếp thông tin qua API, không cần qua model, nhưng nếu làm vậy thì phần validate sẽ phải đặt trong controller, method của controller sẽ có dạng như sau:

def create
  if params[:file].present? && params[:title].present? # ... and more checks here
    # upload video
  else
    # display an error (and user won't even understand what exactly is wrong)
  end
end

Việc này có nhiều hạn chế và trái với nhiều nguyên tắc trong các convention. Thay vào đó, ta tạo thêm một class Ruby tên là VideoUpload để đặt tất cả các logic validate. Để dễ dàng hơn nữa ta sẽ cho class này kế thừa các feature của ActiveRecord. Chúng ta sẽ làm điều này với sự trợ giúp của gem active_type. Gem này sẽ giúp các Ruby object bình thường có thể phản ứng như một ActiveRecord thực thụ.

Thêm gem này vào trong Gemfile:

[...]
gem 'active_type', '0.3.1'
[...]

và chạy

$ bundle install

Bây giờ tạo class video_upload.rb trong thư mục models:

models/video_upload.rb

class VideoUpload < ActiveType::Object
  attribute :file, :string
  attribute :title, :string
  attribute :description, :text

  validates :file, presence: true
  validates :title, presence: true
end

Người sử dụng Postgres SQL và Rails 4.2 sẽ gặp phải một số rắc rối nhỏ. Chỉ cần chỉnh sửa lại một chút là có thể vượt qua dễ dàng:

models/video_upload.rb

[...]
attribute :file, :varchar
attribute :title, :varchar
[...]

Đây chỉ là một class Ruby đơn giản kế thừa từ ActiveType::Object, nhưng có khả năng không thua gì một ActiveRecord. Ta có thể sử dụng các attribute method để định nghĩa các attribute và kiểu của chúng. Các validation method được mang đến trực tiếp từ ActiveRecord và có thể được dùng tương tự.

Bây giờ ta quay về và hoàn thiện nốt controller:

video_uploads_controller.rb

def create
  @video_upload = VideoUpload.new(title: params[:video_upload][:title],
                                  description: params[:video_upload][:description],
                                  file: params[:video_upload][:file].try(:tempfile).try(:to_path))
  if @video_upload.save
    uploaded_video = @video_upload.upload!(current_user)

    # check if the video was uploaded or not

    redirect_to root_url
  else
    render :new
  end
end

Đây chỉ là một method controller đơn giản. Tất nhiên, gọi hàm save với biến @video_upload không thực sự save cái gì cả, nó chỉ có tác dụng check validate và gọi callback. Method upload! hiện giờ không tồn tại, ta sẽ định nghĩa cho nó:

models/video_upload.rb

[...]
def upload!(user)
  account = Yt::Account.new access_token: user.token
  account.upload_video self.file, title: self.title, description: self.description
end
[...]

Method này tạo một yt client mới có chứa access token và ta đã nhận từ trước. Method upload_video thực hiện công việc upload thực sự. Nó nhận file và các tham số của video, như title và description.

Hoàn thiện nốt phần logic của create:

[...]
def create
  @video_upload = VideoUpload.new(title: params[:video_upload][:title], description: params[:video_upload][:description], file: params[:video_upload][:file].try(:tempfile).try(:to_path))

  if @video_upload.save
    uploaded_video = @video_upload.upload!(current_user)

    if uploaded_video.failed?
      flash[:error] = 'There was an error while uploading your video...'
    else
      Video.create({link: "https://www.youtube.com/watch?v=#{uploaded_video.id}"})
      flash[:success] = 'Your video has been uploaded!'
    end
    redirect_to root_url
  else
    render :new
  end
end
[...]

Phần này phụ trách việc hiện lỗi trong quá trình upload. Nếu không có vấn đề gì sảy ra thì sẽ thêm thông tin của video vào database và redirect đến root_url.

Một số điều cần lưu ý

Bạn cần phải nhớ rằng YouTube cần có một khoảng thời gian để xử lý video sau khi upload lên, và video càng dài, quá trình này càng diễn ra lâu hơn. Bởi vậy nếu bạn muốn lấy về thông tin thời lượng của video ngay sau khi upload thì kết quả thường sẽ trả về 0.

Điều này cũng áp dụng cho ảnh thumbnail: chúng sẽ chỉ là một tấm ảnh default xám xịt trong vài phút đầu.

Để xử lý vấn đề này, bạn có thể cài đặt một tiến trình chạy ngầm hoặc schedule để check xem video mới upload đã được xử lý xong chưa. Nếu đã xử lý xong, ta lấy toàn bộ thông tin của video về. Bạn có thể cài đặt để ẩn các video này và chỉ hiện sau khi lấy được thông tin đầy đủ. Đừng quên nhắc nhở các tester và user về điều này nếu không muốn bị log bug vô lý 😄

Cuối cùng, đừng quên rằng YouTube có quyền từ chối video với 101 lý do: dài quá, ngắn quá, bị trùng, vi phạm bản quyền, không support codec…

Kết luận

Hai bài viết này cung cấp và hướng dẫn cách và sử dụng những tác vụ cơ bản nhất của YouTube API v3. Để thực hiện các tác vụ phức tạp hơn xin mời tham khảo hướng dẫn sử dụng của gem yt hoặc tài liệu về API của Google. Xin cảm ơn!