На днях мы выпустили Coub API. Теперь можно делать приложения, смотреть ленту, лайкать, рекобить, то есть практически все, что можно сделать на сайте, можно делать через API. Но самое главное — теперь можно из сторонних приложений через API создавать кобы.
В этом туториале я покажу, как можно сделать простейший клиент коба на Ruby on Rails. Приложение позволяет залогиниться через коб и сгенерить такой коб с любым текстом:
Рабочая версия этого приложения лежит по адресу fantozzi.dev2.workisfun.ru, код приложения из этого туториала можно посмотреть на Гитхабе: github.com/igorgladkoborodov/memegenerator
OAuth
Коб использует стандартный протокол для авторизации OAuth 2.0. Он используется в очень многих сервисах, которые предоставляют внешний API (Фейсбук, например), по нему очень много документации и библиотек для любой платформы.
Работает авторизация примерно так: приложение со своим уникальным ключом заходит на специальную страницу на coub.com, там Коб спрашивает, согласен ли пользователь дать приложению доступ. Если пользователь разрешает, то Коб возвращает пользователя обратно в приложение, и отдает вместе с запросом токен пользователя, который потом уже используется при всех API-запросах пользователя. То же самое происходит, например, при авторизации через Фейсбук или Твиттер.
Мы будем писать на RoR и для авторизации через OAuth для рельсов все уже давно написано, мы будем использовать для этого гем omniauth-oauth2 и официальный кобовский гем omniauth-coub.
Создание приложения и авторизация
Создаем приложение с красноречивым названием memegenerator и прикручиваем его к Pow (или кто чем пользуется):
$ cd ~/apps/
$ rails new memegenerator
$ ln -s ~/apps/memegenerator/ ~/.pow/memegenerator
Проверяем в браузере, что у нас по адресу memegenerator.dev живет пустое рельсовое приложение.
2. Регистрируем наше новое приложение по адресу coub.com/dev/applications/
В поле Website указываем урл нашего тестового приложения, в поле Callback URL пишем
http://memegenerator.dev/auth/coub/callback
После создания приложения Коб даст нам Application ID и Secret, они нам понадобятся дальше:
3. Устанавливаем гем omniauth-coub:
Gemfile:
gem "omniauth-coub"
$ bundle install
4. Добавляем коб в провайдеры omniauth:
config/initializers/omniauth.rb:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :coub, ENV["COUB_KEY"], ENV["COUB_SECRET"], scope: "logged_in,create"
end
COUB_KEY и COUB_SECRET — это Application ID и Secret из прошлого шага, можно добавить их в ENV переменные или пока для теста вставить строки прямо тут, хотя оставлять в коде ключи нежелательно, ну вы понимаете.
Если у вас Pow, то добавить переменные можно в файле .powenv в корне приложения:
.powenv:
export COUB_KEY="[Application ID]"
export COUB_SECRET="[Secret]"
В scope вы можете указать, какими правами будет обладать приложение. Наше приложение нужно только для создания кобов, поэтому лишнего мы ничего не будем просить, только разрешение на авторизацию и создание коба: logged_in, create. Полный список режимов доступа можно посмотреть в документации к API.
5. Создаем модель пользователя с методом from_omniauth, который создает или находит в базе пользователя по данным, которые передал нам сервер авторизации на Кобе.
Что происходит в этом пункте и в паре следующих пунктов, хорошо объяснено в одном из эпизодов RailsCasts.
$ rails g model user provider:string uid:string auth_token:string name:string
$ rake db:migrate
app/models/user.rb:
class User < ActiveRecord::Base
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
user.auth_token = auth.credentials.token
user.name = auth.extra.raw_info.name
user.save!
end
end
end
6. Создаем контроллер сессий. Через него мы создаем и удаляем сессию.
Метод create — это то место, куда возвращается пользователь с его токеном и данными авторизации. Тут мы создаем пользователя через метод from_omniauth, который мы написали в прошлом шаге, и сохраняем его токен, записываем его в куку, чтобы при перезагрузке браузера можно было вернуть эту сессию.
$ rails g controller sessions
app/controllers/sessions_controller.rb:
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
cookies.permanent[:auth_token] = user.auth_token
redirect_to root_url
end
def destroy
cookies.delete(:auth_token)
redirect_to root_url
end
def index
end
end
Морда приложения пускай пока поживет в контроллере сессий, поэтому тут метод index.
7. Чтобы иметь доступ к текущему пользователю, добавляем в ApplicationController метод current_user, который ищет пользователя в базе данных, если у нас есть кука с его токеном.
app/controllers/application_controller.rb:
helper_method :current_user
def current_user
@current_user ||= User.find_by_auth_token(cookies[:auth_token]) if cookies[:auth_token]
end
8. Выводим на морде ссылку на логин или показываем текущего пользователя с ссылкой на выход.
app/views/sessions/index.html.erb:
<% if current_user %>
<%= current_user.name %>
<%= link_to "Log out", logout_path, method: :delete %>
<% else %>
<%= link_to "Auth with Coub", "/auth/coub" %>
<% end %>
По пути /auth/coub гем omniauth-oauth2 перебросит на страницу авторизации на coub.com.
9. Прописываем роуты:
config/routes.rb:
Rails.application.routes.draw do
root "sessions#index"
get "/auth/:provider/callback" => "sessions#create"
delete "logout" => "sessions#destroy"
end
С авторизацией все. Заходим на memegenerator.dev, проверяем. Должно выглядеть примерно вот так:
Теперь у нас в базе есть пользователь с токеном, который может делать запросы через Coub API.
Запросы к API
Имея токен можно делать запросы к API. Это обычные запросы по протоколу HTTP, как в браузере. GET запросы можно в браузере же и тестировать. Каждый запрос кроме своих параметров должен содержать параметр access_token с токеном пользователя, который нам до этого выдал сервер авторизации.
Например, чтобы лайкнуть коб, надо выполнить примерно такой запрос:
POST http://coub.com/api/v2/likes?id=[coub_id]&channel_id=[channel_id]&access_token=[users_token]
Некоторые запросы можно делать и без токена, например, инфа о кобе (без токена доступны только публичные кобы). Это GET запрос, эту ссылку можно открыть просто в браузере:
GET http://coub.com/api/v2/coubs/4yp6r
Все эти запросы хорошо задокументированы, полный список можно посмотреть тут: coub.com/dev/docs/Coub+API/Overview
Клиент
Каждый раз вручную делать запросы и добавлять токен неудобно, поэтому добавим модели User метод client, через который будем делать запросы:
app/models/user.rb:
def client
@client ||= Faraday.new(:url => "http://coub.com/api/v2/", :params => {:access_token => auth_token})
end
Gemfile:
gem "faraday"
$ bundle install
Запросы мы делаем через Faraday, это HTTP клиент на Ruby.
Запустим консоль, потестим запросы к апи:
$ rails c
> user = User.first
> user.client.get "coubs/4yp6r"
Ответы выдаются в формате JSON, поэтому, если нам хочется прочитать, что вернул сервер, надо ответ распарсить стандартной JSON библиотекой:
> coub_info = JSON.parse(user.client.get("coubs/4yp6r").body)
> coub_info["id"]
=> 9090841
У нас есть айдишник коба, давайте его лайкнем:
> user.client.post "likes?id=#{coub_info["id"]}"
Генерим видео
У нас есть видео, кадр из фильма, нам надо на него три раза положить текст в разное время. Для работы с видео через консоль есть программа ffmpeg, в ней через консоль можно делать с видео практически что угодно.
На маке через Homebrew он ставится вот так:
$ brew install ffmpeg --with-freetype
Накладываем текст через ffmpeg фильтром drawtext:
$ ffmpeg -i template.mp4 -vf "drawtext=enable='between(t,1,2)':text=Blah:fontfile=PFDinTextCondPro-XBlack.ttf:fontsize=40:fontcolor=white:x=(w-tw)/2:y=(h*0.9-th)" output.mp4
Эта строчка означает:
1. ffmpeg -i template.mp4 -vf: берем видео файла template.mp4. Файл для этого туториала можно скачать вот тут.
2. drawtext=enable: накладываем текст
3. between(t,1,2): с первой по вторую секунду
4. fontfile=PFDinTextCondPro-XBlack.ttf: используем файл с шрифтом в формате TTF
5. text=Blah: пишем текст Blah
6. fontsize=40: размер шрифта
7. fontcolor=white: белого цвета
8. x=(w-tw)/2:y=(h*0.9-th): положение текста в центре внизу (w и h — это размер видео, tw и th — это размер блока с текстом)
9. output.mp4: записываем все в этот файл
Чтобы написать три текста, надо просто через запятую три drawtext написать:
$ ffmpeg -i template.mp4 -vf "drawtext=enable='between(t,1,2)':text=Text1:fontfile=PFDinTextCondPro-XBlack.ttf:fontsize=40:fontcolor=white:x=(w-tw)/2:y=(h*0.9-th), drawtext=enable='between(t,3,5)':text=Text2:fontfile=PFDinTextCondPro-XBlack.ttf:fontsize=40:fontcolor=white:x=(w-tw)/2:y=(h*0.9-th), drawtext=enable='between(t,6.3,8)':text=Text3:fontfile=PFDinTextCondPro-XBlack.ttf:fontsize=40:fontcolor=white:x=(w-tw)/2:y=(h*0.9-th)" output.mp4
В темповой папке можно сложить файл template.mp4 и файл шрифта (можно взять любой TTF из папки со шрифтами) и попробовать в консоли запустить, оно должно сгенерить правильное видео.
Закачиваем видео
Видео у нас есть, теперь надо из него сделать коб.
Коб через API закачивается в три этапа:
1. Сначала инициализируем закачку запросом POST coubs/init_upload, в ответе мы получаем id коба и его permalink.
$ rails c
> user = User.first
> init_response = JSON.parse(user.client.post("coubs/init_upload").body)
> coub_id = init_response["id"]
> permalink = init_response["id"]
2. Закачиваем видео запросом POST coubs/:id/upload_video. Файл передается в теле запроса, в хедере Content-Type надо передать video/mp4:
> user.client.post do |r|
r.url "coubs/#{coub_id}/upload_video"
r.headers["Content-Type"] = "video/mp4"
r.body = File.open("tmp/output.mp4", "r").read
end
Если мы хотим загрузить отдельный саундтрек к кобу, то это можно сделать отдельным запросом coubs/:id/upload_audio. Нам в этот раз этого не нужно, поэтому мы опускаем этот запрос.
3. Финализируем создание коба запросом POST coubs/:id/finalize_upload, в параметрах передаем тайтл, настройки приватности, теги, включен ли звук.
> user.client.post "coubs/#{coub_id}/finalize_upload", title: "Test coub", original_visibility_type: "private", tags: "tag1, tag2, tag3", sound_enabled: true
После закачки коба, он будет какое-то время процесситься: видео на серверах Коба будет конвертироваться в несколько форматов для разных платформ, будут генериться превьюшки и куча всяких таких ресурсоемких вещей. Прогресс конвертирования можно проверить GET запросом coubs/:id/finalize_status. Он отдает примерно такой JSON { percent_done: 20, done: false}.
> user.client.get "coubs/#{coub_id}/finalize_status"
Ок. Мы протестировали это в консоли, теперь все это надо собрать в приложение.
Модель Coub
Создаем модель Coub:
$ rails g model coub user:belongs_to title:string visibility_type:string tags:string permalink:string coub_id:string text1:string text2:string text3:string
$ rake db:migrate
2. Делаем метод generate_video_file, который геренит видео из видео-шаблона и трех текстов, находящихся в полях text1, text2, text3. Шаблон видео и шрифт кладем в ассеты. Готовое видео кладем в папку tmp.
app/models/coub.rb:
def escape_ffmpeg_text(text)
text.to_s.gsub("'", "\\\\\\\\\\\\'").gsub(":", "\\\\\\\\:").mb_chars.upcase # Crazy ffmpeg escaping
end
def ffmpeg_drawtext(text, from, to)
font_file = File.join(Rails.root, "app", "assets", "fonts", "PFDinTextCondPro-XBlack.ttf")
"drawtext=enable='between(t,#{from},#{to})':text=#{escape_ffmpeg_text(text)}:fontfile=#{font_file}:fontsize=40:fontcolor=white:x=(w-tw)/2:y=(h*0.9-th)"
end
def generate_video_file
self.video_file = File.join(Rails.root, "tmp", "output-#{Time.now.to_i}.mp4")
template_file = File.join(Rails.root, "app", "assets", "videos", "template.mp4")
`ffmpeg -i #{template_file} -vf "#{ffmpeg_drawtext(text1, 1, 2)}, #{ffmpeg_drawtext(text2, 3, 5)}, #{ffmpeg_drawtext(text3, 6.3, 8)}" #{video_file}`
return video_file
end
3. Делаем метод, который в три этапа закачивает видео на Коб:
app/models/coub.rb:
def upload_video
self.title ||= text2
self.visibility_type ||= "private"
self.tags ||= ""
init_response = JSON.parse(client.post("coubs/init_upload").body)
self.coub_id = init_response["id"]
self.permalink = init_response["permalink"]
save
client.post do |r|
r.url "coubs/#{coub_id}/upload_video"
r.headers["Content-Type"] = "video/mp4"
r.body = File.open(video_file, "r").read
end
client.post "coubs/#{coub_id}/finalize_upload",
title: title,
original_visibility_type: visibility_type,
tags: tags,
sound_enabled: true
end
def generate_and_upload_video
generate_video_file
upload_video
end
4. Прописываем, что коб принадлежит пользователю и заодно пишем метод client для удобного доступа к клиенту через пользователя:
app/models/coub.rb:
belongs_to :user
def client
@client ||= user.client
end
5. Метод url отдает урл коба по пермалинку:
app/models/coub.rb:
def url
"http://coub.com/view/#{permalink}"
end
Проверим, все ли работает:
$ rails c
> coub = Coub.new
> coub.user = User.first
> coub.text1 = 'Text 1'
> coub.text2 = 'Text 2'
> coub.text3 = 'Text 3'
> coub.tags = 'tag1, tag2, tag3'
> coub.visibility_type = 'unlisted'
> coub.generate_and_upload_video
> coub.url
=> "http://coub.com/view/5dbyc"
По этому урлу можно зайти и посмотреть на синий экран “Your coub is being processed”.
Осталось сделать для всего этого контроллер:
1. Создаем контроллер coubs. Он будет состоять из двух методов: index (это будет новая морда, вместо sessions#index) и create. При создании коба мы сразу редиректим на него.
$ rails g controller coubs
app/controllers/coubs_controller.rb:
class CoubsController < ApplicationController
def index
end
def create
@coub = Coub.create(coub_params.merge(:user => current_user))
@coub.generate_and_upload_video
redirect_to @coub.url
end
private
def coub_params
params.require(:coub).permit(:text1, :text2, :text3, :visibility_type, :tags)
end
end
2. Перетаскиваем index.html.erb из sessions в coubs и прикручиваем туда форму:
app/views/coubs/index.html.erb:
<% if current_user %>
<p>You’re logged in as <%= current_user.name %>. <%= link_to "Log out", logout_path, method: :delete %></p>
<%= form_for Coub.new, url: {action: "create"} do |f| %>
<%= f.text_field :text1, placeholder: "To me", maxlength: 30, size: 50 %><br />
<%= f.text_field :text2, placeholder: "Your Coub API", maxlength: 30, size: 50 %><br />
<%= f.text_field :text3, placeholder: "Is a piece of shit", maxlength: 30, size: 50 %><br />
<%= f.select :visibility_type, options_for_select(["public", "friends", "unlisted", "private"]) %><br />
<%= f.text_field :tags, placeholder: "first tag, second tag" %><br />
<%= f.submit "Create Coub" %>
<% end %>
<% else %>
<p>Please <a href="/auth/coub">log in via Coub</a>.</p>
<% end %>
Все, теперь заходим в браузер, и проверяем, что все работает:
В этом туториале я описал только принцип работы API. Разумеется, для настоящего приложения надо еще много чего дописать: валидации, проверки ответов API, обработки ошибок, интерфейс нарисовать, тесты. Чуть-чуть допиленная версия этого скрипта лежит вот тут http://fantozzi.dev2.workisfun.ru, там все то же самое, но видео готовится и закачивается асинхронно через delayed_job.
Исходники этого туториала лежат в гитхабе: https://github.com/igorgladkoborodov/memegenerator/.
Если есть какие-то вопросы по этому туториалу или по API, пишите на igor@coub.com.
Автор: workisfun