convertr 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +6 -2
- data/VERSION +1 -1
- data/bin/convertr +3 -3
- data/convertr.gemspec +39 -6
- data/lib/convertr.rb +33 -0
- data/lib/convertr/convertor.rb +199 -0
- data/lib/convertr/file.rb +15 -0
- data/lib/convertr/migration.rb +38 -0
- data/lib/convertr/runner.rb +53 -0
- data/lib/convertr/scheduler.rb +29 -0
- data/lib/convertr/scheduler/all_bt_first.rb +10 -0
- data/lib/convertr/scheduler/bt_600_first.rb +6 -0
- data/lib/convertr/scheduler_factory.rb +11 -0
- data/lib/convertr/task.rb +10 -0
- data/lib/tasks/convertr.rake +22 -0
- data/test/database.yml +39 -0
- data/test/factories/files.rb +5 -0
- data/test/factories/tasks.rb +6 -0
- data/test/fixtures/files.yml +49 -0
- data/test/fixtures/tasks.yml +14 -0
- data/test/helper.rb +10 -1
- data/test/settings.yml +41 -0
- data/test/test_convertor.rb +57 -0
- data/test/test_file.rb +27 -0
- data/test/test_scheduler_allbtfirst.rb +12 -0
- data/test/test_scheduler_factory.rb +17 -0
- data/test/test_task.rb +23 -0
- metadata +65 -11
- data/test/test_convertr.rb +0 -7
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
+
load 'lib/tasks/convertr.rake'
|
3
4
|
|
4
5
|
begin
|
5
6
|
require 'jeweler'
|
@@ -11,7 +12,10 @@ begin
|
|
11
12
|
gem.homepage = "http://github.com/tulaman/convertr"
|
12
13
|
gem.authors = ["Ilya Lityuga", "Alexander Svetkin"]
|
13
14
|
gem.add_development_dependency "shoulda", ">= 0"
|
14
|
-
gem.add_development_dependency "
|
15
|
+
gem.add_development_dependency "mocha", ">= 0"
|
16
|
+
gem.add_development_dependency "factory_girl", ">= 0"
|
17
|
+
gem.add_dependency "activerecord", ">= 3.0.0"
|
18
|
+
gem.requirements << "ffmpeg, any version"
|
15
19
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
20
|
end
|
17
21
|
Jeweler::GemcutterTasks.new
|
@@ -39,7 +43,7 @@ rescue LoadError
|
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
42
|
-
task :test => :check_dependencies
|
46
|
+
task :test => [:check_dependencies, 'convertr:prepare_test']
|
43
47
|
|
44
48
|
task :default => :test
|
45
49
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.1
|
data/bin/convertr
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
$LOAD_PATH << File.join(File.dirname(__FILE__), '..')
|
4
|
-
require '
|
3
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
require 'convertr'
|
5
5
|
|
6
6
|
if ARGV.any? { |arg| %w(--version -v).any? { |flag| arg == flag } }
|
7
7
|
puts "Convertr #{Convertr::Version}"
|
8
8
|
exit 0
|
9
9
|
end
|
10
10
|
|
11
|
-
Convertr::Runner.new(ARGV)
|
11
|
+
Convertr::Runner.new(ARGV).run
|
data/convertr.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{convertr}
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Ilya Lityuga", "Alexander Svetkin"]
|
12
|
-
s.date = %q{2010-10-
|
12
|
+
s.date = %q{2010-10-12}
|
13
13
|
s.default_executable = %q{convertr}
|
14
14
|
s.description = %q{Convertr works with database and handles converting tasks. It fetches files from remote sources and converts them to appropriate formats with ffmpeg}
|
15
15
|
s.email = %q{ilya.lityuga@gmail.com}
|
@@ -28,17 +28,44 @@ Gem::Specification.new do |s|
|
|
28
28
|
"bin/convertr",
|
29
29
|
"convertr.gemspec",
|
30
30
|
"lib/convertr.rb",
|
31
|
+
"lib/convertr/convertor.rb",
|
32
|
+
"lib/convertr/file.rb",
|
33
|
+
"lib/convertr/migration.rb",
|
34
|
+
"lib/convertr/runner.rb",
|
35
|
+
"lib/convertr/scheduler.rb",
|
36
|
+
"lib/convertr/scheduler/all_bt_first.rb",
|
37
|
+
"lib/convertr/scheduler/bt_600_first.rb",
|
38
|
+
"lib/convertr/scheduler_factory.rb",
|
39
|
+
"lib/convertr/task.rb",
|
40
|
+
"lib/tasks/convertr.rake",
|
41
|
+
"test/database.yml",
|
42
|
+
"test/factories/files.rb",
|
43
|
+
"test/factories/tasks.rb",
|
44
|
+
"test/fixtures/files.yml",
|
45
|
+
"test/fixtures/tasks.yml",
|
31
46
|
"test/helper.rb",
|
32
|
-
"test/
|
47
|
+
"test/settings.yml",
|
48
|
+
"test/test_convertor.rb",
|
49
|
+
"test/test_file.rb",
|
50
|
+
"test/test_scheduler_allbtfirst.rb",
|
51
|
+
"test/test_scheduler_factory.rb",
|
52
|
+
"test/test_task.rb"
|
33
53
|
]
|
34
54
|
s.homepage = %q{http://github.com/tulaman/convertr}
|
35
55
|
s.rdoc_options = ["--charset=UTF-8"]
|
36
56
|
s.require_paths = ["lib"]
|
57
|
+
s.requirements = ["ffmpeg, any version"]
|
37
58
|
s.rubygems_version = %q{1.3.7}
|
38
59
|
s.summary = %q{Useful utility for converting video files with ffmpeg}
|
39
60
|
s.test_files = [
|
40
|
-
"test/
|
41
|
-
"test/
|
61
|
+
"test/factories/files.rb",
|
62
|
+
"test/factories/tasks.rb",
|
63
|
+
"test/test_scheduler_allbtfirst.rb",
|
64
|
+
"test/test_scheduler_factory.rb",
|
65
|
+
"test/test_file.rb",
|
66
|
+
"test/helper.rb",
|
67
|
+
"test/test_task.rb",
|
68
|
+
"test/test_convertor.rb"
|
42
69
|
]
|
43
70
|
|
44
71
|
if s.respond_to? :specification_version then
|
@@ -47,13 +74,19 @@ Gem::Specification.new do |s|
|
|
47
74
|
|
48
75
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
76
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
50
|
-
s.add_development_dependency(%q<
|
77
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
78
|
+
s.add_development_dependency(%q<factory_girl>, [">= 0"])
|
79
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 3.0.0"])
|
51
80
|
else
|
52
81
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
82
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
83
|
+
s.add_dependency(%q<factory_girl>, [">= 0"])
|
53
84
|
s.add_dependency(%q<activerecord>, [">= 3.0.0"])
|
54
85
|
end
|
55
86
|
else
|
56
87
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
88
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
89
|
+
s.add_dependency(%q<factory_girl>, [">= 0"])
|
57
90
|
s.add_dependency(%q<activerecord>, [">= 3.0.0"])
|
58
91
|
end
|
59
92
|
end
|
data/lib/convertr.rb
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/railtie'
|
4
|
+
require 'convertr/runner'
|
5
|
+
require 'convertr/scheduler'
|
6
|
+
require 'convertr/scheduler_factory'
|
7
|
+
require 'convertr/convertor'
|
8
|
+
|
1
9
|
module Convertr
|
2
10
|
module Version
|
3
11
|
class << self
|
@@ -6,4 +14,29 @@ module Convertr
|
|
6
14
|
end
|
7
15
|
end
|
8
16
|
end
|
17
|
+
|
18
|
+
class Config < OpenStruct
|
19
|
+
include Singleton
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configure(config = nil)
|
23
|
+
config ||= Config.instance
|
24
|
+
yield Config.instance if block_given?
|
25
|
+
enviroment = ENV['RAILS_ENV'] || 'development'
|
26
|
+
config.db_config = YAML.load_file(config.db_config_file)[enviroment]
|
27
|
+
if config.settings_file
|
28
|
+
YAML.load_file(config.settings_file)[enviroment]['convertor'].each do |k, v|
|
29
|
+
config.send("#{k}=", v)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
self.init!
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.init!
|
36
|
+
conf = Config.instance
|
37
|
+
ActiveRecord::Base.establish_connection(conf.db_config)
|
38
|
+
require 'convertr/file'
|
39
|
+
require 'convertr/task'
|
40
|
+
$stderr.puts "Tables not found" && exit(1) unless Convertr::File.table_exists? && Convertr::Task.table_exists?
|
41
|
+
end
|
9
42
|
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'uri'
|
3
|
+
require 'net/ftp'
|
4
|
+
|
5
|
+
class Hash # {{{ small hack for syntax sugar - hash merging by +
|
6
|
+
def +(h)
|
7
|
+
self.merge(h)
|
8
|
+
end
|
9
|
+
end # }}}
|
10
|
+
|
11
|
+
class File # {{{ filename without extension
|
12
|
+
def self.filename(filepath)
|
13
|
+
File.basename(filepath, File.extname(filepath))
|
14
|
+
end
|
15
|
+
end # }}}
|
16
|
+
|
17
|
+
module Convertr
|
18
|
+
class Convertor
|
19
|
+
CONVERTOR_STOPFILE = 'stop.convertr'
|
20
|
+
CONVERTOR_PAUSEFILE = 'pause.convertr'
|
21
|
+
CONVERTOR_PAUSE_DELAY = 120
|
22
|
+
CONVERTOR_MAX_FETCH_TIME = 600
|
23
|
+
|
24
|
+
attr_accessor :scheduler, :max_tasks, :hostname, :initial_dir, :file, :task, :file_path, :work_path, :tasks
|
25
|
+
|
26
|
+
def initialize(max_tasks = 0, scheduler = nil) # инициализация конертера {{{
|
27
|
+
@max_tasks = max_tasks
|
28
|
+
@logger = Logger.new($stderr)
|
29
|
+
@initial_dir = Dir.pwd
|
30
|
+
@scheduler = Convertr::SchedulerFactory.create(scheduler)
|
31
|
+
@hostname = `hostname`.chomp
|
32
|
+
@conf = Convertr::Config.instance
|
33
|
+
@tasks = 0
|
34
|
+
end # }}}
|
35
|
+
|
36
|
+
def run # запуск конвертора {{{
|
37
|
+
loop do
|
38
|
+
break if File.exists? CONVERTOR_STOPFILE
|
39
|
+
if !File.exists?(CONVERTOR_PAUSEFILE) and @task = @scheduler.schedule_next_task(@hostname)
|
40
|
+
task.update_attributes(
|
41
|
+
:convert_status => process_task,
|
42
|
+
:convert_stopped_at => Time.now
|
43
|
+
)
|
44
|
+
break if @max_tasks > 0 && (@tasks += 1) > @max_tasks
|
45
|
+
else
|
46
|
+
sleep(CONVERTOR_PAUSE_DELAY) && next
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end # }}}
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def process_task # выполнение конкретной задачи на конвертацию {{{
|
54
|
+
@file = @task.file
|
55
|
+
@file_path = File.join(@conf.tmp_dir, @file.name)
|
56
|
+
@working_dir = File.join( File.dirname(@file_path), File.filename(@filepath) )
|
57
|
+
@logger.info("Started #@filepath")
|
58
|
+
begin
|
59
|
+
fetch_file(@file.source_location, @file.name)
|
60
|
+
FileUtils.cd @working_dir
|
61
|
+
process_profile(profile_by_bitrate(@task.bitrate))
|
62
|
+
if @task.bitrate == 600
|
63
|
+
count = calc_thumbnails_count(@file.duration)
|
64
|
+
interval = (@file.duration / count).to_i
|
65
|
+
system(make_thumbnails_cmd(count, interval, 150, 0, 2)) or raise "thumbnails generation failed #{$?}"
|
66
|
+
end
|
67
|
+
@logger.info("Done #@filepath")
|
68
|
+
rescue StandardError => e
|
69
|
+
@logger.error e.message
|
70
|
+
return 'FAILURE'
|
71
|
+
ensure
|
72
|
+
FileUtils.cd @initial_dir
|
73
|
+
end
|
74
|
+
'SUCCESS'
|
75
|
+
end # }}}
|
76
|
+
|
77
|
+
def fetch_file(source_url, filename) # скачивание файла по FTP {{{
|
78
|
+
FileUtils.mkpath(@working_dir)
|
79
|
+
dst_file = File.join(@working_dir, File.basename(filename))
|
80
|
+
tmp_file = dst_file + ".part"
|
81
|
+
started_at = Time.now
|
82
|
+
loop do
|
83
|
+
return if File.exists? dst_file
|
84
|
+
if File.exists? tmp_file
|
85
|
+
sleep(CONVERTOR_PAUSE_DELAY)
|
86
|
+
if Time.now > started_at + CONVERTOR_MAX_FETCH_TIME
|
87
|
+
FileUtils.rm_f tmp_file
|
88
|
+
else
|
89
|
+
next
|
90
|
+
end
|
91
|
+
end
|
92
|
+
FileUtils.touch tmp_file
|
93
|
+
break
|
94
|
+
end
|
95
|
+
|
96
|
+
url = URI.parse(source_url)
|
97
|
+
begin
|
98
|
+
ftp = Net::Ftp.open(url.host)
|
99
|
+
ftp.login(@conf.ftp_user, @conf.ftp_pass)
|
100
|
+
ftp.getbinaryfile(url.path, tmp_file, 1024)
|
101
|
+
rescue StandardError => e
|
102
|
+
ftp.close
|
103
|
+
FileUtils.rm_f tmp_file
|
104
|
+
raise e
|
105
|
+
end
|
106
|
+
ftp.close
|
107
|
+
|
108
|
+
FileUtils.rename tmp_file, dst_file
|
109
|
+
end # }}}
|
110
|
+
|
111
|
+
def profile_by_bitrate(bitrate) # bitrate -> profile {{{
|
112
|
+
case bitrate
|
113
|
+
when 300 then 'sd'
|
114
|
+
when 600 then 'hd'
|
115
|
+
when 1000 then 'hdp'
|
116
|
+
else
|
117
|
+
raise "Unsupported bitrate '#{bitrate}'"
|
118
|
+
end
|
119
|
+
end # }}}
|
120
|
+
|
121
|
+
def process_profile(pname) # конвертация, согласно профилю # {{{
|
122
|
+
profile = @conf.enc_profiles[pname]
|
123
|
+
outfile = File.join(@working_dir, File.filename(@file.name), "-#{pname}-0.mp4")
|
124
|
+
infile = File.join(@working_dir, File.basename(@file.name))
|
125
|
+
output_dir = File.join(@conf.output_dir, @working_dir)
|
126
|
+
FileUtils.mkpath output_dir unless File.exists? output_dir
|
127
|
+
FileUtils.chdir output_dir
|
128
|
+
FileUtils.mkdir pname unless File.exists? pname
|
129
|
+
FileUtils.chdir pname
|
130
|
+
|
131
|
+
@conf.enc_pass.each_with_index do |enc_pass_params, i|
|
132
|
+
cmd = mkcmd(enc_pass_params, pname, infile, i == 0 ? '/dev/null' : outfile, @file.aspect)
|
133
|
+
@logger.info(cmd)
|
134
|
+
system(cmd) or raise "system #{cmd} failed: #{$?}"
|
135
|
+
end
|
136
|
+
|
137
|
+
outfile2 = File.join(@working_dir, File.filename(@file.name), "-#{pname}.mp4")
|
138
|
+
cmd = "#{@conv.qtfaststart} #{outfile} #{outfile2}"
|
139
|
+
system(cmd) or FileUtils.rm_f(outfile) && raise("system #{cmd} failed: #{$?}")
|
140
|
+
FileUtils.cd '..'
|
141
|
+
FileUtils.rm_rf pname
|
142
|
+
end # }}}
|
143
|
+
|
144
|
+
def make_thumbnails_cmd(count, interval, w, h, postfix) # генерация тумбнейлов {{{
|
145
|
+
w = (h * @file.float_aspect).to_i if h && w.nil?
|
146
|
+
h = (w / @file.float_aspect).to_i if @file.aspect && w && h.nil?
|
147
|
+
tstamps = (1..count).collect {|i| i * interval}
|
148
|
+
params = ''
|
149
|
+
params += ' -c 20 ' if @task.crop?
|
150
|
+
params += ' -d ' if @task.deinterlace?
|
151
|
+
output_dir = File.join(@conf.output_dir, @working_dir)
|
152
|
+
pattern = File.join(@working_dir, File.filename(@file.name), "-%d-#{postfix}.jpg")
|
153
|
+
"#{@conf.tmaker} -i #{infile} #{params} -w #{w} -h #{h} -o #{output_dir} #{tstamps.join(' ')}"
|
154
|
+
end # }}}
|
155
|
+
|
156
|
+
def mkcmd(enc_pass, pname, infile, outfile, aspect) # подготовка команды на конвертацию {{{
|
157
|
+
float_aspect = aspect.split(':').first.to_f / aspect.split(':').last.to_f
|
158
|
+
params = @conf.enc_global + enc_pass + @conf.enc_profiles[pname]
|
159
|
+
# we must deinterlace movies taken from sat. dvd and rips don't have to be deinterlaced
|
160
|
+
params['deinterlace'] = '' if task.deinterlace? # option w/o a value
|
161
|
+
(w, h) = case
|
162
|
+
when aspect == '4:3' then [640, 480]
|
163
|
+
when aspect == '16:9' then [704, 396]
|
164
|
+
when float_aspect > 16.0 / 9.0 then [704, (704.0 / float_aspect).to_i]
|
165
|
+
when float_aspect < 16.0 / 9.0 && float_aspect > 4.0 / 3.0 then [640, (640.0 / float_aspect).to_i]
|
166
|
+
else
|
167
|
+
params['aspect'] = '1.33333333'
|
168
|
+
[640, 480]
|
169
|
+
end
|
170
|
+
# Frame size must be a multiple of 2
|
171
|
+
h -= 1 if h.odd?
|
172
|
+
if task.crop?
|
173
|
+
(params['croptop'], params['cropbottom'], params['cropleft'], params['cropright']) =
|
174
|
+
case aspect
|
175
|
+
when '16:9'
|
176
|
+
w += 32 && h += 18
|
177
|
+
[10, 8, 16, 16]
|
178
|
+
else # '4:3', '5:3' etc
|
179
|
+
w += 32 && h += 24
|
180
|
+
[20, 4, 16, 16]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
"#{@conf.ffmpeg} -y -i #{infile} -s #{w}x#{h} #{params.map {|k, v| "-#{k} #{v}"}.sort.join(' ')} #{outfile}"
|
184
|
+
end # }}}
|
185
|
+
|
186
|
+
def calc_thumbnails_count(seconds) # выбор количества тумбнейлов {{{
|
187
|
+
case (seconds / 60).to_i
|
188
|
+
when 0..10 then 3
|
189
|
+
when 10..20 then 6
|
190
|
+
when 20..30 then 9
|
191
|
+
when 30..40 then 12
|
192
|
+
when 40..50 then 15
|
193
|
+
when 50..60 then 18
|
194
|
+
when 60..70 then 21
|
195
|
+
else 24
|
196
|
+
end
|
197
|
+
end # }}}
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Convertr
|
2
|
+
class File < ActiveRecord::Base
|
3
|
+
set_table_name :files
|
4
|
+
has_many :tasks, :class_name => 'Convertr::Task'
|
5
|
+
scope :without_convertor, where('convertor is null').order('broadcast_at asc')
|
6
|
+
scope :with_convertor, lambda { |convertor|
|
7
|
+
where(:convertor => convertor).order('broadcast_at asc')
|
8
|
+
}
|
9
|
+
|
10
|
+
def float_aspect
|
11
|
+
(w, h) = aspect.split(':')
|
12
|
+
w.to_f / h.to_f
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Convertr
|
2
|
+
class Migration < ActiveRecord::Migration
|
3
|
+
def self.up
|
4
|
+
create_table :files do |t|
|
5
|
+
t.string :filename
|
6
|
+
t.string :location
|
7
|
+
t.datetime :enqueue_at
|
8
|
+
t.string :convertor
|
9
|
+
t.datetime :broadcast_at
|
10
|
+
t.integer :src_size
|
11
|
+
t.integer :duration
|
12
|
+
t.string :aspect, :limit => 10
|
13
|
+
t.integer :width
|
14
|
+
t.integer :height
|
15
|
+
t.datetime :src_deleted_at
|
16
|
+
t.datetime :convertor_deleted_at
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :tasks do |t|
|
20
|
+
t.integer :file_id
|
21
|
+
t.integer :bitrate
|
22
|
+
t.string :convert_status, :limit => 20
|
23
|
+
t.boolean :convert_started
|
24
|
+
t.boolean :convert_stopped
|
25
|
+
t.string :copy_status, :limit => 20
|
26
|
+
t.boolean :copy_started
|
27
|
+
t.boolean :copy_stopped
|
28
|
+
t.boolean :crop
|
29
|
+
t.boolean :deinterlace
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.down
|
34
|
+
drop_table :tasks
|
35
|
+
drop_table :files
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Convertr
|
4
|
+
class Runner
|
5
|
+
class OptParser # {{{
|
6
|
+
def self.parse(args)
|
7
|
+
options = Convertr::Config.instance
|
8
|
+
options.db_config_file = "~/.convertr/database.yml"
|
9
|
+
options.settings_file = "~/.convertr/settings.yml"
|
10
|
+
options.max_tasks = 0
|
11
|
+
options.force_reset_database = false
|
12
|
+
opts = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: convertr [options]"
|
14
|
+
opts.separator ""
|
15
|
+
opts.separator "Specific options:"
|
16
|
+
|
17
|
+
opts.on("-d", "--db_config_file [PATH_TO_DB_CONFIG]",
|
18
|
+
"Specify path to database.yml file") do |dbc|
|
19
|
+
options.db_config_file = dbc
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("-c", "--settings_file [PATH_TO_CONFIG]",
|
23
|
+
"Specify path to settings.yml file") do |c|
|
24
|
+
options.settings_file = c
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-m", "--max_tasks N", Integer,
|
28
|
+
"Specify the maximum tasks to complete") do |m|
|
29
|
+
options.max_tasks = m
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on("-f", "--force",
|
33
|
+
"Force recreating database tables") do |f|
|
34
|
+
options.force = f
|
35
|
+
end
|
36
|
+
end
|
37
|
+
opts.parse!(args)
|
38
|
+
options
|
39
|
+
end
|
40
|
+
end # }}}
|
41
|
+
|
42
|
+
attr_reader :db_config, :config, :options
|
43
|
+
|
44
|
+
def initialize(opts) # {{{
|
45
|
+
config = Convertr::Runner::OptParser.parse(opts)
|
46
|
+
Convertr.configure(config)
|
47
|
+
end
|
48
|
+
# }}}
|
49
|
+
def run # {{{
|
50
|
+
puts "Running"
|
51
|
+
end # }}}
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Convertr
|
2
|
+
module Scheduler
|
3
|
+
class Base
|
4
|
+
def initialize
|
5
|
+
end
|
6
|
+
|
7
|
+
def schedule_next_task(convertor)
|
8
|
+
# TODO: locking
|
9
|
+
if task = get_task_for_schedule(convertor)
|
10
|
+
schedule(task, convertor)
|
11
|
+
end
|
12
|
+
task
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def get_task_for_schedule # abstract
|
18
|
+
raise "Should be overriden"
|
19
|
+
end
|
20
|
+
|
21
|
+
def schedule(task, convertor)
|
22
|
+
Convertr::Task.transaction do
|
23
|
+
task.update_attributes(:convert_status => 'PROGRESS', :convert_started_at => Time.now)
|
24
|
+
task.file.update_attribute(:convertor => convertor)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'convertr/scheduler'
|
2
|
+
Dir[File.join(File.dirname(__FILE__), 'scheduler', '*')].each {|f| require f if File.file? f}
|
3
|
+
|
4
|
+
module Convertr
|
5
|
+
class SchedulerFactory
|
6
|
+
def self.create(name = 'AllBtFirst')
|
7
|
+
name ||= 'AllBtFirst'
|
8
|
+
Convertr::Scheduler.const_get(name).new
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Convertr
|
2
|
+
class Task < ActiveRecord::Base
|
3
|
+
set_table_name :tasks
|
4
|
+
belongs_to :file, :class_name => 'Convertr::File'
|
5
|
+
scope :not_completed, where(:convert_status => 'NONE')
|
6
|
+
scope :for_convertor, lambda { |convertor|
|
7
|
+
not_completed.includes(:file).where('files.convertor' => convertor).order('files.broadcast_at asc')
|
8
|
+
}
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
namespace :convertr do
|
2
|
+
task :load_config do
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))
|
4
|
+
require 'convertr'
|
5
|
+
Convertr.configure do |c|
|
6
|
+
c.db_config_file = 'test/database.yml'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
task :reset_database => :load_config do
|
11
|
+
require 'convertr/migration'
|
12
|
+
Convertr::Migration.down
|
13
|
+
Convertr::Migration.up
|
14
|
+
end
|
15
|
+
|
16
|
+
task :load_fixtures => :load_config do
|
17
|
+
require 'active_record/fixtures'
|
18
|
+
Fixtures.create_fixtures(File.join(Dir.pwd, 'test', 'fixtures'), [:files, :tasks])
|
19
|
+
end
|
20
|
+
|
21
|
+
task :prepare_test => [:reset_database, :load_fixtures]
|
22
|
+
end
|
data/test/database.yml
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# MySQL. Versions 4.1 and 5.0 are recommended.
|
2
|
+
#
|
3
|
+
# Install the MySQL driver:
|
4
|
+
# gem install mysql2
|
5
|
+
#
|
6
|
+
# And be sure to use new-style password hashing:
|
7
|
+
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
|
8
|
+
development:
|
9
|
+
adapter: mysql2
|
10
|
+
encoding: utf8
|
11
|
+
reconnect: false
|
12
|
+
database: videomore
|
13
|
+
pool: 5
|
14
|
+
username: root
|
15
|
+
password:
|
16
|
+
socket: /var/lib/mysql/mysql.sock
|
17
|
+
|
18
|
+
# Warning: The database defined as "test" will be erased and
|
19
|
+
# re-generated from your development database when you run "rake".
|
20
|
+
# Do not set this db to the same as development or production.
|
21
|
+
test:
|
22
|
+
adapter: mysql2
|
23
|
+
encoding: utf8
|
24
|
+
reconnect: false
|
25
|
+
database: videomore_test
|
26
|
+
pool: 5
|
27
|
+
username: root
|
28
|
+
password:
|
29
|
+
socket: /var/run/mysqld/mysqld.sock
|
30
|
+
|
31
|
+
production:
|
32
|
+
adapter: mysql2
|
33
|
+
encoding: utf8
|
34
|
+
reconnect: false
|
35
|
+
database: videomore_production
|
36
|
+
pool: 5
|
37
|
+
username: root
|
38
|
+
password:
|
39
|
+
socket: /var/run/mysqld/mysqld.sock
|
@@ -0,0 +1,49 @@
|
|
1
|
+
with_convertor1_1:
|
2
|
+
id: 1
|
3
|
+
filename: test.avi
|
4
|
+
location: /tmp
|
5
|
+
enqueue_at: <%= Time.now %>
|
6
|
+
convertor: convertor1
|
7
|
+
broadcast_at: <%= Time.now - 3600 * 24 %>
|
8
|
+
src_size: 12345
|
9
|
+
duration: 12345
|
10
|
+
aspect: 3x4
|
11
|
+
width: 640
|
12
|
+
height: 480
|
13
|
+
|
14
|
+
with_convertor1_2:
|
15
|
+
id: 2
|
16
|
+
filename: test.avi
|
17
|
+
location: /tmp
|
18
|
+
enqueue_at: <%= Time.now %>
|
19
|
+
convertor: convertor1
|
20
|
+
broadcast_at: <%= Time.now %>
|
21
|
+
src_size: 12345
|
22
|
+
duration: 12345
|
23
|
+
aspect: 3x4
|
24
|
+
width: 640
|
25
|
+
height: 480
|
26
|
+
|
27
|
+
without_convertor_1:
|
28
|
+
id: 3
|
29
|
+
filename: test.avi
|
30
|
+
location: /tmp
|
31
|
+
enqueue_at: <%= Time.now %>
|
32
|
+
broadcast_at: <%= Time.now - 3600 * 24 %>
|
33
|
+
src_size: 12345
|
34
|
+
duration: 12345
|
35
|
+
aspect: 3x4
|
36
|
+
width: 640
|
37
|
+
height: 480
|
38
|
+
|
39
|
+
without_convertor_2:
|
40
|
+
id: 4
|
41
|
+
filename: test.avi
|
42
|
+
location: /tmp
|
43
|
+
enqueue_at: <%= Time.now %>
|
44
|
+
broadcast_at: <%= Time.now %>
|
45
|
+
src_size: 12345
|
46
|
+
duration: 12345
|
47
|
+
aspect: 3x4
|
48
|
+
width: 640
|
49
|
+
height: 480
|
data/test/helper.rb
CHANGED
@@ -1,10 +1,19 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'test/unit'
|
3
3
|
require 'shoulda'
|
4
|
+
require 'shoulda/active_record'
|
5
|
+
require 'factory_girl'
|
6
|
+
require 'mocha'
|
4
7
|
|
5
8
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
6
|
-
|
9
|
+
#$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
10
|
require 'convertr'
|
11
|
+
Convertr.configure do |c|
|
12
|
+
c.db_config_file = './test/database.yml'
|
13
|
+
c.settings_file = './test/settings.yml'
|
14
|
+
end
|
15
|
+
require 'factories/files'
|
16
|
+
require 'factories/tasks'
|
8
17
|
|
9
18
|
class Test::Unit::TestCase
|
10
19
|
end
|
data/test/settings.yml
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
development: &non_production_settings
|
2
|
+
convertor:
|
3
|
+
tmp_dir: /tmp
|
4
|
+
ftp_user: test
|
5
|
+
ftp_pass: test
|
6
|
+
enc_pass:
|
7
|
+
- vpre: "fastfirstpass-flash"
|
8
|
+
pass: 1
|
9
|
+
an: ""
|
10
|
+
f: mp4
|
11
|
+
- vpre: "hq-flash"
|
12
|
+
pass: 2
|
13
|
+
enc_profiles:
|
14
|
+
sd:
|
15
|
+
b: 236k
|
16
|
+
bt: 236k
|
17
|
+
ab: 64k
|
18
|
+
ac: 1
|
19
|
+
hd:
|
20
|
+
b: 472k
|
21
|
+
bt: 472k
|
22
|
+
ab: 128k
|
23
|
+
ac: 2
|
24
|
+
hdp:
|
25
|
+
b: 872k
|
26
|
+
bt: 872k
|
27
|
+
ab: 128k
|
28
|
+
ac: 2
|
29
|
+
enc_global:
|
30
|
+
acodec: libfaac
|
31
|
+
vcodec: libx264
|
32
|
+
threads: 3
|
33
|
+
ar: 44100
|
34
|
+
ffmpeg: /usr/bin/ffmpeg
|
35
|
+
tmaker: /usr/local/bin/tmaker
|
36
|
+
|
37
|
+
test:
|
38
|
+
<<: *non_production_settings
|
39
|
+
|
40
|
+
production:
|
41
|
+
<<: *non_production_settings
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
class TestConvertor < Test::Unit::TestCase
|
4
|
+
context "Convertor" do
|
5
|
+
setup do
|
6
|
+
@convertor = Convertr::Convertor.new
|
7
|
+
end
|
8
|
+
|
9
|
+
should "be able to calculate thumbnails count" do
|
10
|
+
assert_equal 3, @convertor.instance_eval("calc_thumbnails_count(600)")
|
11
|
+
assert_equal 6, @convertor.instance_eval("calc_thumbnails_count(1200)")
|
12
|
+
assert_equal 9, @convertor.instance_eval("calc_thumbnails_count(1800)")
|
13
|
+
end
|
14
|
+
|
15
|
+
should "find the right profile by bitrate" do
|
16
|
+
assert_equal 'sd', @convertor.instance_eval("profile_by_bitrate(300)")
|
17
|
+
assert_equal 'hd', @convertor.instance_eval("profile_by_bitrate(600)")
|
18
|
+
assert_equal 'hdp', @convertor.instance_eval("profile_by_bitrate(1000)")
|
19
|
+
assert_raise RuntimeError do
|
20
|
+
@convertor.instance_eval("profile_by_bitrate(1234)")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "for some task with crop and deinterlace" do
|
25
|
+
setup do
|
26
|
+
task = Factory.create(:task, :bitrate => 600, :crop => true, :deinterlace => true)
|
27
|
+
@convertor.stubs(:task).returns(task)
|
28
|
+
end
|
29
|
+
should "prepare valid command for ffmpeg" do
|
30
|
+
assert_equal '/usr/bin/ffmpeg -y -i infile.avi -s 1118x414 -ab 64k -ac 1 -acodec libfaac -ar 44100 -b 236k -bt 236k -cropbottom 8 -cropleft 16 -cropright 16 -croptop 10 -deinterlace -pass 2 -threads 3 -vcodec libx264 -vpre hq-flash outfile.mpg',
|
31
|
+
@convertor.instance_eval { mkcmd({'vpre' => 'hq-flash', 'pass' => 2}, 'sd', 'infile.avi', 'outfile.mpg', '16:9') }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "for some task with crop and without deinterlace" do
|
36
|
+
setup do
|
37
|
+
task = Factory.create(:task, :bitrate => 600, :crop => true, :deinterlace => false)
|
38
|
+
@convertor.stubs(:task).returns(task)
|
39
|
+
end
|
40
|
+
should "prepare valid command for ffmpeg" do
|
41
|
+
assert_equal '/usr/bin/ffmpeg -y -i infile.avi -s 1118x414 -ab 64k -ac 1 -acodec libfaac -ar 44100 -b 236k -bt 236k -cropbottom 8 -cropleft 16 -cropright 16 -croptop 10 -pass 2 -threads 3 -vcodec libx264 -vpre hq-flash outfile.mpg',
|
42
|
+
@convertor.instance_eval { mkcmd({'vpre' => 'hq-flash', 'pass' => 2}, 'sd', 'infile.avi', 'outfile.mpg', '16:9') }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "for some task without crop and without deinterlace" do
|
47
|
+
setup do
|
48
|
+
task = Factory.create(:task, :bitrate => 600, :crop => false, :deinterlace => false)
|
49
|
+
@convertor.stubs(:task).returns(task)
|
50
|
+
end
|
51
|
+
should "prepare valid command for ffmpeg" do
|
52
|
+
assert_equal '/usr/bin/ffmpeg -y -i infile.avi -s 704x396 -ab 64k -ac 1 -acodec libfaac -ar 44100 -b 236k -bt 236k -pass 2 -threads 3 -vcodec libx264 -vpre hq-flash outfile.mpg',
|
53
|
+
@convertor.instance_eval { mkcmd({'vpre' => 'hq-flash', 'pass' => 2}, 'sd', 'infile.avi', 'outfile.mpg', '16:9') }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/test/test_file.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
class TestFile < Test::Unit::TestCase
|
4
|
+
context "File class" do
|
5
|
+
should "have without_convertor scope" do
|
6
|
+
files = Convertr::File.without_convertor
|
7
|
+
assert_equal 2, files.size
|
8
|
+
assert_equal 3, files.first.id
|
9
|
+
end
|
10
|
+
|
11
|
+
should "have with_convertor scope" do
|
12
|
+
files = Convertr::File.with_convertor('convertor1')
|
13
|
+
assert_equal 2, files.size
|
14
|
+
assert_equal 1, files.first.id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
context "File" do
|
18
|
+
setup do
|
19
|
+
@file = Convertr::File.new
|
20
|
+
end
|
21
|
+
subject { @file }
|
22
|
+
should have_many(:tasks)
|
23
|
+
should "be valid" do
|
24
|
+
assert @file.valid?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
class TestSchedulerAllbtfirst < Test::Unit::TestCase
|
4
|
+
context "AllBtFirst scheduler" do
|
5
|
+
setup do
|
6
|
+
@scheduler = Convertr::SchedulerFactory.create('AllBtFirst')
|
7
|
+
end
|
8
|
+
should "find first task assigned for convertor" do
|
9
|
+
assert_equal 2, @scheduler.get_task_for_schedule('convertor1').id
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestSchedulerFactory < Test::Unit::TestCase
|
4
|
+
context "SchedulerFactory" do
|
5
|
+
should "create instance of AllBtFirst by default" do
|
6
|
+
assert_instance_of Convertr::Scheduler::AllBtFirst, Convertr::SchedulerFactory.create
|
7
|
+
end
|
8
|
+
|
9
|
+
should "create instance of appropriate scheduler" do
|
10
|
+
assert_instance_of Convertr::Scheduler::Bt600First, Convertr::SchedulerFactory.create('Bt600First')
|
11
|
+
end
|
12
|
+
|
13
|
+
should "return object inherited from base scheduler" do
|
14
|
+
assert_kind_of Convertr::Scheduler::Base, Convertr::SchedulerFactory.create
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/test/test_task.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
|
3
|
+
class TestTask < Test::Unit::TestCase
|
4
|
+
context "Tasks" do
|
5
|
+
should "have for_convertor scope" do
|
6
|
+
assert_equal 2, Convertr::Task.for_convertor('convertor1').size
|
7
|
+
end
|
8
|
+
should "have not_completed scope" do
|
9
|
+
assert_equal 3, Convertr::Task.not_completed.size
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context "Task" do
|
14
|
+
setup do
|
15
|
+
@task = Convertr::Task.new
|
16
|
+
end
|
17
|
+
subject { @task }
|
18
|
+
should belong_to(:file)
|
19
|
+
should "be valid" do
|
20
|
+
assert @task.valid?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: convertr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 29
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ilya Lityuga
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2010-10-
|
19
|
+
date: 2010-10-12 00:00:00 +04:00
|
20
20
|
default_executable: convertr
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
@@ -34,9 +34,37 @@ dependencies:
|
|
34
34
|
type: :development
|
35
35
|
version_requirements: *id001
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
|
-
name:
|
37
|
+
name: mocha
|
38
38
|
prerelease: false
|
39
39
|
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: factory_girl
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: activerecord
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
40
68
|
none: false
|
41
69
|
requirements:
|
42
70
|
- - ">="
|
@@ -47,8 +75,8 @@ dependencies:
|
|
47
75
|
- 0
|
48
76
|
- 0
|
49
77
|
version: 3.0.0
|
50
|
-
type: :
|
51
|
-
version_requirements: *
|
78
|
+
type: :runtime
|
79
|
+
version_requirements: *id004
|
52
80
|
description: Convertr works with database and handles converting tasks. It fetches files from remote sources and converts them to appropriate formats with ffmpeg
|
53
81
|
email: ilya.lityuga@gmail.com
|
54
82
|
executables:
|
@@ -68,8 +96,28 @@ files:
|
|
68
96
|
- bin/convertr
|
69
97
|
- convertr.gemspec
|
70
98
|
- lib/convertr.rb
|
99
|
+
- lib/convertr/convertor.rb
|
100
|
+
- lib/convertr/file.rb
|
101
|
+
- lib/convertr/migration.rb
|
102
|
+
- lib/convertr/runner.rb
|
103
|
+
- lib/convertr/scheduler.rb
|
104
|
+
- lib/convertr/scheduler/all_bt_first.rb
|
105
|
+
- lib/convertr/scheduler/bt_600_first.rb
|
106
|
+
- lib/convertr/scheduler_factory.rb
|
107
|
+
- lib/convertr/task.rb
|
108
|
+
- lib/tasks/convertr.rake
|
109
|
+
- test/database.yml
|
110
|
+
- test/factories/files.rb
|
111
|
+
- test/factories/tasks.rb
|
112
|
+
- test/fixtures/files.yml
|
113
|
+
- test/fixtures/tasks.yml
|
71
114
|
- test/helper.rb
|
72
|
-
- test/
|
115
|
+
- test/settings.yml
|
116
|
+
- test/test_convertor.rb
|
117
|
+
- test/test_file.rb
|
118
|
+
- test/test_scheduler_allbtfirst.rb
|
119
|
+
- test/test_scheduler_factory.rb
|
120
|
+
- test/test_task.rb
|
73
121
|
has_rdoc: true
|
74
122
|
homepage: http://github.com/tulaman/convertr
|
75
123
|
licenses: []
|
@@ -97,13 +145,19 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
145
|
segments:
|
98
146
|
- 0
|
99
147
|
version: "0"
|
100
|
-
requirements:
|
101
|
-
|
148
|
+
requirements:
|
149
|
+
- ffmpeg, any version
|
102
150
|
rubyforge_project:
|
103
151
|
rubygems_version: 1.3.7
|
104
152
|
signing_key:
|
105
153
|
specification_version: 3
|
106
154
|
summary: Useful utility for converting video files with ffmpeg
|
107
155
|
test_files:
|
156
|
+
- test/factories/files.rb
|
157
|
+
- test/factories/tasks.rb
|
158
|
+
- test/test_scheduler_allbtfirst.rb
|
159
|
+
- test/test_scheduler_factory.rb
|
160
|
+
- test/test_file.rb
|
108
161
|
- test/helper.rb
|
109
|
-
- test/
|
162
|
+
- test/test_task.rb
|
163
|
+
- test/test_convertor.rb
|