Thumbnail generation in Sidekiq worker

openid
Marcin Kulik 13 years ago
parent da7a9532df
commit 27219f48b7

@ -18,6 +18,7 @@ gem 'fog'
gem 'simple_form'
gem 'redcarpet'
gem 'sidekiq'
gem 'draper'
# Gems used only for assets and not required
# in production environments by default.

@ -70,6 +70,8 @@ GEM
confstruct (0.2.1)
connection_pool (0.9.1)
diff-lcs (1.1.3)
draper (0.11.1)
activesupport (>= 2.3.10)
erubis (2.7.0)
excon (0.9.6)
execjs (1.3.0)
@ -288,6 +290,7 @@ DEPENDENCIES
carrierwave
coffee-rails
confstruct
draper
factory_girl_rails
faker
fog

@ -5,6 +5,7 @@ class Api::AsciicastsController < ApplicationController
ac = Asciicast.new(params[:asciicast])
if ac.save
SnapshotWorker.perform_async(ac.id)
render :text => asciicast_url(ac), :status => :created, :location => ac
else
render :text => ac.errors.full_messages, :status => 422

@ -1,7 +1,7 @@
class AsciicastsController < ApplicationController
PER_PAGE = 20
before_filter :load_resource, :only => [:show, :edit, :update, :destroy]
before_filter :load_resource, :only => [:edit, :update, :destroy]
before_filter :ensure_authenticated!, :only => [:edit, :update, :destroy]
before_filter :ensure_owner!, :only => [:edit, :update, :destroy]
@ -15,6 +15,7 @@ class AsciicastsController < ApplicationController
end
def show
@asciicast = AsciicastDecorator.find(params[:id])
@title = @asciicast.smart_title
respond_with @asciicast
end

@ -0,0 +1,28 @@
class ApplicationDecorator < Draper::Base
# Lazy Helpers
# PRO: Call Rails helpers without the h. proxy
# ex: number_to_currency(model.price)
# CON: Add a bazillion methods into your decorator's namespace
# and probably sacrifice performance/memory
#
# Enable them by uncommenting this line:
# lazy_helpers
# Shared Decorations
# Consider defining shared methods common to all your models.
#
# Example: standardize the formatting of timestamps
#
# def formatted_timestamp(time)
# h.content_tag :span, time.strftime("%a %m/%d/%y"),
# :class => 'timestamp'
# end
#
# def created_at
# formatted_timestamp(model.created_at)
# end
#
# def updated_at
# formatted_timestamp(model.updated_at)
# end
end

@ -0,0 +1,38 @@
class AsciicastDecorator < ApplicationDecorator
decorates :asciicast
THUMBNAIL_WIDTH = 20
THUMBNAIL_HEIGHT = 10
def thumbnail
if @thumbnail.nil?
lines = model.snapshot.split("\n")
top_lines = lines[0...THUMBNAIL_HEIGHT]
top_text = prepare_lines(top_lines).join("\n")
bottom_lines = lines.reverse[0...THUMBNAIL_HEIGHT].reverse
bottom_text = prepare_lines(bottom_lines).join("\n")
if top_text.gsub(/\s+/, '').size > bottom_text.gsub(/\s+/, '').size
@thumbnail = top_text
else
@thumbnail = bottom_text
end
end
@thumbnail
end
private
def prepare_lines(lines)
(THUMBNAIL_HEIGHT - lines.size).times { lines << '' }
lines.map do |line|
line = line[0...THUMBNAIL_WIDTH]
line << ' ' * (THUMBNAIL_WIDTH - line.size)
line
end
end
end

@ -0,0 +1,85 @@
require 'tempfile'
class SnapshotWorker
include Sidekiq::Worker
sidekiq_options :retry => false
def perform(asciicast_id)
begin
@asciicast = Asciicast.find(asciicast_id)
prepare_files
convert_to_typescript
delay = (@asciicast.duration / 2).to_i
delay = 30 if delay > 30
snapshot = capture_terminal(delay)
@asciicast.snapshot = snapshot
@asciicast.save!
rescue ActiveRecord::RecordNotFound
# oh well...
ensure
cleanup
end
end
def prepare_files
in_data_file = Tempfile.new('asciiio-data', :encoding => 'ascii-8bit')
in_data_file.write(@asciicast.stdout.read)
in_data_file.close
@in_data_path = in_data_file.path
in_timing_file = Tempfile.new('asciiio-timing', :encoding => 'ascii-8bit')
in_timing_file.write(@asciicast.stdout_timing.read)
in_timing_file.close
@in_timing_path = in_timing_file.path
@out_data_path = @in_data_path + '.ts'
@out_timing_path = @in_timing_path + '.ts'
end
def convert_to_typescript
system "bash -c './script/convert-to-typescript.sh " +
"#{@in_data_path} #{@in_timing_path} " +
"#{@out_data_path} #{@out_timing_path}'"
raise "Can't convert asciicast ##{@asciicast.id} to typescript" if $? != 0
end
def capture_terminal(delay)
capture_command =
"scriptreplay #{@out_timing_path} #{@out_data_path}; sleep 10"
command = "bash -c 'ASCIICAST_ID=#{@asciicast.id} " +
"COLS=#{@asciicast.terminal_columns} " +
"LINES=#{@asciicast.terminal_lines} " +
"COMMAND=\"#{capture_command}\" " +
"DELAY=#{delay} ./script/capture.sh'"
lines = []
status = nil
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
while !stdout.eof?
lines << stdout.readline
end
status = wait_thr.value.exitstatus
end
raise "Can't capture output of asciicast ##{@asciicast.id}" if status != 0
lines.join('')
end
def cleanup
FileUtils.rm_f([
@in_data_path,
@in_timing_path,
@out_data_path,
@out_timing_path
])
end
end

@ -1,31 +0,0 @@
require 'tempfile'
class ThumbnailWorker
include Sidekiq::Worker
def perform(asciicast_id)
asciicast = Asciicast.find(asciicast_id)
in_data_file = Tempfile.new('asciiio-data', :encoding => 'ascii-8bit')
in_data_file.write(asciicast.stdout.read)
in_data_file.close
in_data_path = in_data_file.path
in_timing_file = Tempfile.new('asciiio-timing', :encoding => 'ascii-8bit')
in_timing_file.write(asciicast.stdout_timing.read)
in_timing_file.close
in_timing_path = in_timing_file.path
out_data_path = in_data_path + '.ts'
out_timing_path = in_timing_path + '.ts'
if system "bash -c './script/convert-to-typescript.sh #{in_data_path} #{in_timing_path} #{out_data_path} #{out_timing_path}'"
delay = (asciicast.duration / 2).to_i
command = "scriptreplay #{out_timing_path} #{out_data_path}; sleep 10"
puts '-' * 80
system "bash -c 'ASCIICAST_ID=#{asciicast_id} COLS=#{asciicast.terminal_columns} LINES=#{asciicast.terminal_lines} COMMAND=\"#{command}\" DELAY=#{delay} THUMB_LINES=10 THUMB_COLS=20 ./script/thumbnail.sh'"
else
puts "failed"
end
end
end

@ -2,6 +2,7 @@ $:.unshift(File.expand_path('./lib', ENV['rvm_path']))
require "rvm/capistrano"
require 'bundler/capistrano'
require 'capistrano_colors'
require 'sidekiq/capistrano'
set :application, "ascii.io"

@ -0,0 +1,17 @@
SIDEKIQ_URL = 'redis://localhost:6379'
SIDEKIQ_NS = 'asciiio-sidekiq'
Sidekiq.configure_server do |config|
config.redis = {
:url => SIDEKIQ_URL,
:namespace => SIDEKIQ_NS
}
end
Sidekiq.configure_client do |config|
config.redis = {
:url => SIDEKIQ_URL,
:namespace => SIDEKIQ_NS,
:size => 1
}
end

@ -0,0 +1,6 @@
class AddSnapshotToAsciicast < ActiveRecord::Migration
def change
add_column :asciicasts, :snapshot, :text
end
end

@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20120403165915) do
ActiveRecord::Schema.define(:version => 20120406152447) do
create_table "asciicasts", :force => true do |t|
t.integer "user_id"
@ -34,10 +34,12 @@ ActiveRecord::Schema.define(:version => 20120403165915) do
t.text "description"
t.boolean "featured", :default => false
t.string "username"
t.text "snapshot"
end
add_index "asciicasts", ["created_at"], :name => "index_asciicasts_on_created_at"
add_index "asciicasts", ["featured"], :name => "index_asciicasts_on_featured"
add_index "asciicasts", ["likes_count"], :name => "index_asciicasts_on_likes_count"
add_index "asciicasts", ["recorded_at"], :name => "index_asciicasts_on_recorded_at"
add_index "asciicasts", ["user_id"], :name => "index_asciicasts_on_user_id"
add_index "asciicasts", ["user_token"], :name => "index_asciicasts_on_user_token"
@ -53,6 +55,17 @@ ActiveRecord::Schema.define(:version => 20120403165915) do
add_index "comments", ["asciicast_id"], :name => "index_comments_on_asciicast_id"
add_index "comments", ["user_id"], :name => "index_comments_on_user_id"
create_table "likes", :force => true do |t|
t.integer "asciicast_id", :null => false
t.integer "user_id", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "likes", ["asciicast_id"], :name => "index_likes_on_asciicast_id"
add_index "likes", ["user_id", "asciicast_id"], :name => "index_likes_on_user_id_and_asciicast_id"
add_index "likes", ["user_id"], :name => "index_likes_on_user_id"
create_table "user_tokens", :force => true do |t|
t.integer "user_id", :null => false
t.string "token", :null => false

@ -2,13 +2,13 @@
# Usage:
# ASCIICAST_ID=666 COLS=80 LINES=20 COMMAND="df; df; df; sleep 10"
# DELAY=1 THUMB_LINES=5 THUMB_COLS=10 ./tmux-save.sh
# DELAY=1 ./tmux-save.sh
set -e
SESSION_NAME=asciiio-thumb-$ASCIICAST_ID
SESSION_NAME=asciiio-thumb-$ASCIICAST_ID-`date +'%s'`
tmux new -s $SESSION_NAME -d -x $COLS -y $LINES "$COMMAND"
sleep $DELAY
tmux capture-pane -t $SESSION_NAME
tmux save-buffer - | tail -n $THUMB_LINES | ruby -e "ARGF.lines.each { |l| puts l[0...$THUMB_COLS] }"
tmux save-buffer -

@ -0,0 +1,4 @@
require 'spec_helper'
describe ApplicationDecorator do
end

@ -0,0 +1,5 @@
require 'spec_helper'
describe AsciicastDecorator do
before { ApplicationController.new.set_current_view_context }
end
Loading…
Cancel
Save