vtools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. data/INSTALL +0 -0
  2. data/LICENSE +20 -0
  3. data/README.md +131 -0
  4. data/Rakefile +29 -0
  5. data/bin/vtools +22 -0
  6. data/doc/CONFIG.md +36 -0
  7. data/doc/HOOKS.md +37 -0
  8. data/doc/LIB_EXAMPLE.md +109 -0
  9. data/extconf.rb +7 -0
  10. data/lib/vtools.rb +79 -0
  11. data/lib/vtools/config.rb +91 -0
  12. data/lib/vtools/convert_options.rb +155 -0
  13. data/lib/vtools/converter.rb +98 -0
  14. data/lib/vtools/errors.rb +21 -0
  15. data/lib/vtools/handler.rb +43 -0
  16. data/lib/vtools/harvester.rb +71 -0
  17. data/lib/vtools/job.rb +48 -0
  18. data/lib/vtools/options.rb +101 -0
  19. data/lib/vtools/shared_methods.rb +131 -0
  20. data/lib/vtools/storage.rb +67 -0
  21. data/lib/vtools/thumbnailer.rb +93 -0
  22. data/lib/vtools/thumbs_options.rb +80 -0
  23. data/lib/vtools/version.rb +6 -0
  24. data/lib/vtools/version.rb~ +4 -0
  25. data/lib/vtools/video.rb +158 -0
  26. data/setup.rb +1585 -0
  27. data/spec/config_spec.rb +142 -0
  28. data/spec/convert_options_spec.rb +284 -0
  29. data/spec/converter_spec.rb +167 -0
  30. data/spec/errors_spec.rb +39 -0
  31. data/spec/fixtures/outputs/file_with_iso-8859-1.txt +35 -0
  32. data/spec/fixtures/outputs/file_with_no_audio.txt +18 -0
  33. data/spec/fixtures/outputs/file_with_non_supported_audio.txt +29 -0
  34. data/spec/fixtures/outputs/file_with_start_value.txt +19 -0
  35. data/spec/fixtures/outputs/file_with_surround_sound.txt +19 -0
  36. data/spec/handler_spec.rb +81 -0
  37. data/spec/harvester_spec.rb +189 -0
  38. data/spec/job_spec.rb +130 -0
  39. data/spec/options_spec.rb +52 -0
  40. data/spec/shared_methods_spec.rb +351 -0
  41. data/spec/spec_helper.rb +20 -0
  42. data/spec/storage_spec.rb +106 -0
  43. data/spec/thumbnailer_spec.rb +178 -0
  44. data/spec/thumbs_options_spec.rb +159 -0
  45. data/spec/video_spec.rb +274 -0
  46. data/vtools.gemspec +29 -0
  47. metadata +177 -0
@@ -0,0 +1,91 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ # default configuration options
4
+ module VTools
5
+
6
+ CONFIG = {
7
+
8
+ # system environment
9
+ :PWD => Dir.getwd,
10
+ :library => [],
11
+ :logging => nil,
12
+ :log_file => nil,
13
+ :config_file => nil,
14
+ :ffmpeg_binary => '/usr/bin/ffmpeg',
15
+ :thumb_binary => '/usr/bin/ffmpegthumbnailer',
16
+
17
+ # harvester
18
+ :max_jobs => 5,
19
+ :store_jobs => 10,
20
+ :harvester_timer => 3,
21
+ :temp_dir => '',
22
+
23
+ # converter
24
+ :video_storage => nil,
25
+ :validate_duration => nil,
26
+ # thumbnailer
27
+ :thumb_storage => nil,
28
+
29
+ # predefined video qualities
30
+ :video_set => {
31
+ # SET_NAME -vcodec VC -acodec AC -s WDTxHGT -vb BR -ab BR -ar SMPL -ac CH EXT POSTFIX -vpre CONF
32
+ :x264_180p => ['libx264', 'libfaac', '240x180', '96k', '64k', 22050, 2, 'mp4', '_180', 'normal' ],
33
+ :x264_240p => ['libx264', 'libfaac', '426x240', '128k', '64k', 22050, 2, 'mp4', '_240', 'normal' ],
34
+ :x264_360p => ['libx264', 'libfaac', '640x360', '480k', '128k', 44100, 2, 'mp4', '_360', 'normal' ],
35
+ :x264_480p => ['libx264', 'libfaac', '845x480', '720k', '128k', 44100, 2, 'mp4', '_480', 'normal' ],
36
+ :x264_720p => ['libx264', 'libfaac', '1280x720', '1024k', '128k', 44100, 2, 'mp4', '_720', 'normal' ],
37
+ :x264_1080p => ['libx264', 'libfaac', '1920x1080', '2048k', '128k', 44100, 2, 'mp4', '_1080', 'normal' ],
38
+
39
+ :mp4_180p => ['mpeg4', 'libfaac', '240x180', '96k', '64k', 22050, 2, 'mp4', '_180', ],
40
+ :mp4_240p => ['mpeg4', 'libfaac', '426x240', '128k', '64k', 22050, 2, 'mp4', '_240', ],
41
+ :mp4_360p => ['mpeg4', 'libfaac', '640x360', '480k', '128k', 44100, 2, 'mp4', '_360', ],
42
+ :mp4_480p => ['mpeg4', 'libfaac', '845x480', '720k', '128k', 44100, 2, 'mp4', '_480', ],
43
+ :mp4_720p => ['mpeg4', 'libfaac', '1280x720', '1024k', '128k', 44100, 2, 'mp4', '_720', ],
44
+ :mp4_1080p => ['mpeg4', 'libfaac', '1920x1080', '2048k', '128k', 44100, 2, 'mp4', '_1080', ],
45
+
46
+ :flv_180p => ['flv', 'libfaac', '240x180', '96k', '64k', 22050, 2, 'flv', '_180', ],
47
+ :flv_240p => ['flv', 'libfaac', '426x240', '128k', '64k', 22050, 2, 'flv', '_240', ],
48
+ :flv_360p => ['flv', 'libfaac', '640x360', '480k', '128k', 44100, 2, 'flv', '_360', ],
49
+ :flv_480p => ['flv', 'libfaac', '845x480', '720k', '128k', 44100, 2, 'flv', '_480', ],
50
+ :flv_720p => ['flv', 'libfaac', '1280x720', '1024k', '128k', 44100, 2, 'flv', '_720', ],
51
+ :flv_1080p => ['flv', 'libfaac', '1920x1080', '2048k', '128k', 44100, 2, 'flv', '_1080', ],
52
+ },
53
+
54
+ # predefined thumbnailer setup
55
+ :thumb_set => {
56
+ # -s -q count start%
57
+ :w120 => [120, 10, 5, 0],
58
+ :w240 => [240, 10, 5, 0],
59
+ :w360 => [360, 10, 5, 0],
60
+ :w360 => [480, 10, 5, 0],
61
+ :w600 => [600, 10, 5, 0],
62
+ }
63
+ }
64
+
65
+ # parse external config file
66
+ def CONFIG.load!
67
+ begin
68
+ data = YAML.load_file self[:config_file]
69
+ data = VTools.keys_to_sym data
70
+ append! data
71
+ rescue => e
72
+ raise ConfigError, "Invalid config data #{e}"
73
+ end if self[:config_file]
74
+ end
75
+
76
+ # merge config data
77
+ def CONFIG.append! data
78
+ direct = [:ffmpeg_binary, :thumb_binary, :max_jobs, :store_jobs,
79
+ :harvester_timer, :temp_dir, :video_storage, :thumb_storage]
80
+ # common data
81
+ merge! data.select { |key, value| direct.include?(key) }
82
+
83
+ self[:library] += data[:library] if data[:library].is_a? Array # libs
84
+
85
+ [:video_set, :thumb_set].each do |index| # predefined video & thumbs
86
+ self[index].merge! data[index] if
87
+ data[index].is_a?(Hash) &&
88
+ data[index].values.reject{ |val| val.is_a? Array}.empty?
89
+ end
90
+ end
91
+ end # VTools::CONFIG
@@ -0,0 +1,155 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module VTools
4
+
5
+ # options for the video converter
6
+ class ConvertOptions < Hash
7
+ include SharedMethods
8
+
9
+ def initialize options = {}
10
+ @ignore = [:width, :height, :resolution, :extension, :preserve_aspect, :duration, :postfix]
11
+ parse! options
12
+ end
13
+
14
+ # set value method
15
+ # backward compatibility for width height & resolution
16
+ def []= (key, value)
17
+ ignore = @ignore[0..2] << :s
18
+
19
+ case
20
+ # width & height
21
+ when ignore.first(2).include?(key)
22
+ ignore.last(2).each { |index| delete(index) }
23
+ data = { key => value }
24
+ # resolution
25
+ when ignore.last(2).include?(key)
26
+ ignore.each { |index| delete(index) }
27
+ width, height = value.split("x")
28
+ data = { :width => width.to_i, :height => height.to_i, :resolution => value, :s => value}
29
+ # duration
30
+ when [:duration, :t].include?(key)
31
+ data = { :duration => value, :t => value }
32
+ else
33
+ return super
34
+ end
35
+
36
+ merge! data
37
+ return value
38
+ end
39
+
40
+ # to string method
41
+ def to_s
42
+
43
+ params = collect do |key, value|
44
+ "-#{key} #{value}" unless @ignore.include?(key)
45
+ end.compact
46
+
47
+ # put the preset parameters last
48
+ params = params.reject { |p| p =~ /[avfs]pre/ } + params.select { |p| p =~ /[avfs]pre/ }
49
+
50
+ params.join " "
51
+ end
52
+
53
+ private
54
+ # string parser for the options
55
+ def parse! options
56
+
57
+ case
58
+ # try to convert string into valid ffmpeg values
59
+ when options.is_a?(String) && CONFIG[:video_set].include?(options.to_sym)
60
+ # get config data
61
+ vcodec, acodec, s, vb, ab, ar, ac,
62
+ extension, postfix, vpre = CONFIG[:video_set][options.to_sym]
63
+
64
+ # set storage
65
+ options = {
66
+ :vcodec => vcodec,
67
+ :acodec => acodec,
68
+ :s => s,
69
+ :vb => vb,
70
+ :ab => ab,
71
+ :ar => ar,
72
+ :ac => ac,
73
+ :postfix => postfix,
74
+ :extension => extension,
75
+ :preserve_aspect => true,
76
+ }
77
+ options[:vpre] = vpre if vpre
78
+
79
+ when !options.is_a?(Hash)
80
+ raise ConfigError, "Options should be a Hash or String (predefined set)"
81
+ else
82
+ options = keys_to_sym options
83
+ # check inline predefined
84
+ parse! options.delete(:set) if options.has_key? :set
85
+ end
86
+
87
+ perform options
88
+ merge! options
89
+ end
90
+
91
+ # correct width x height | resolution | s values
92
+ def perform hash
93
+
94
+ # fix duration
95
+ hash[:t] = hash[:duration] if hash[:duration]
96
+
97
+ dimmensions = hash[:resolution] ||
98
+ ("#{hash[:width]}x#{hash[:height]}" if hash[:width] && hash[:height]) ||
99
+ hash[:s]
100
+
101
+ if dimmensions
102
+ # recreate dimmensions dimmensions
103
+ if hash[:preserve_aspect]
104
+ width, height = recalculate dimmensions
105
+ dimmensions = "#{width}x#{height}"
106
+ end
107
+
108
+ hash[:s] = dimmensions
109
+ else
110
+ hash.delete(:s)
111
+ end
112
+ end
113
+
114
+ # keep resolution
115
+ def recalculate dimm
116
+ width, height = dimm.split("x").map(&:to_f)
117
+
118
+ return [width, height] unless self[:aspect]
119
+
120
+ # width main:
121
+ if self[:aspect] > 1
122
+ # heigh = -1
123
+ resize = (width / self[:aspect]).round
124
+ resize += 1 if resize.odd? # needed if new_height ended up with no decimals in the first place
125
+
126
+ if height < resize
127
+ width = (height * self[:aspect]).round
128
+ width += 1 if width.odd?
129
+ else
130
+ height = resize
131
+ end
132
+
133
+ # height main:
134
+ elsif self[:aspect] < 1
135
+ # width = -1
136
+ resize = (height * self[:aspect]).round
137
+ resize += 1 if resize.odd?
138
+
139
+ if width < resize
140
+ height = (width / self[:aspect]).round
141
+ height += 1 if height.odd?
142
+ else
143
+ width = resize
144
+ end
145
+
146
+ # square:
147
+ else
148
+ width = height
149
+ end
150
+
151
+ [width, height].map(&:to_i)
152
+ end
153
+
154
+ end # ConverterOptions
155
+ end # VTools
@@ -0,0 +1,98 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module VTools
4
+
5
+ # Video converter itself
6
+ class Converter
7
+ include SharedMethods
8
+
9
+ # constructor
10
+ def initialize video
11
+ @video = video
12
+ end
13
+
14
+
15
+ # ffmpeg converter cicle
16
+ #
17
+ # ffmpeg < 0.8: frame= 413 fps= 48 q=31.0 size= 2139kB time=16.52 bitrate=1060.6kbits/s
18
+ # ffmpeg >= 0.8: frame= 485 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/
19
+ def run
20
+
21
+ @options = @video.convert_options
22
+ @output_file = "#{generate_path @video.name}/#{@video.name}#{@options[:postfix]}.#{@options[:extension]}"
23
+
24
+ command = "#{CONFIG[:ffmpeg_binary]} -y -i '#{@video.path}' #{@options} '#{@output_file}'"
25
+ output = ""
26
+ convert_error = true
27
+
28
+ # before convert callbacks
29
+ Handler.exec :before_convert, @video, command
30
+
31
+ # process video
32
+ Open3.popen3(command) do |stdin, stdout, stderr|
33
+ stderr.each "r" do |line|
34
+ VTools.fix_encoding line
35
+ output << line
36
+
37
+ # we know, that all is not so bad, if "time=" at least once met
38
+ if line.include? "time="
39
+
40
+ convert_error = false # that is why, we say "generally it's OK"
41
+
42
+ if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
43
+ time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
44
+ elsif line =~ /time=(\d+.\d+)/ # ffmpeg 0.7 and below style
45
+ time = $1.to_f
46
+ else # in case of unexpected output
47
+ time = 0.0
48
+ end
49
+ progress = time / @video.duration
50
+
51
+ # callbacks
52
+ Handler.exec :in_convert, @video, progress
53
+ end
54
+ end
55
+ end
56
+
57
+ raise ProcessError, output.split("\n").last if convert_error # exit on error
58
+
59
+ # callbacks
60
+ unless error = encoding_invalid?
61
+ Handler.exec :convert_success, @video, @output_file
62
+ else
63
+ Handler.exec :convert_error, @video, error, output
64
+ raise ProcessError, error # raise exception in error
65
+ end
66
+
67
+ encoded
68
+ end
69
+
70
+ # define if encoded succeed
71
+ def encoding_invalid?
72
+ unless File.exists?(@output_file)
73
+ return "No output file created"
74
+ end
75
+
76
+ unless encoded.valid?
77
+ return "Encoded file is invalid"
78
+ end
79
+
80
+ if CONFIG[:validate_duration]
81
+ # reavalidate duration
82
+ precision = @options[:duration] ? 1.5 : 1.1
83
+ desired_duration = @options[:duration] && @options[:duration] < @video.duration ? @options[:duration] : @video.duration
84
+
85
+ if (encoded.duration >= (desired_duration * precision) or encoded.duration <= (desired_duration / precision))
86
+ return "Encoded file duration is invalid (original/specified: #{desired_duration}sec, got: #{encoded.duration}sec)"
87
+ end
88
+ end
89
+
90
+ false
91
+ end
92
+
93
+ # encoded media
94
+ def encoded
95
+ @encoded ||= Video.new(@output_file).get_info
96
+ end
97
+ end # Converter
98
+ end # VTools
@@ -0,0 +1,21 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ # VTools exceptions
4
+ module VTools
5
+
6
+ # confuguration error
7
+ class ConfigError < ArgumentError
8
+ end
9
+
10
+ # specified file does not exist
11
+ class FileError < Errno::ENOENT
12
+ end
13
+
14
+ # invalid video format
15
+ class FormatError < IOError
16
+ end
17
+
18
+ # invalid video format
19
+ class ProcessError < IOError
20
+ end
21
+ end # VTools
@@ -0,0 +1,43 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module VTools
4
+
5
+ # callback handler
6
+ # allows to execute external script callbacks
7
+ # multiple callbacs in one placeholder are allowed
8
+ #
9
+ # usage:
10
+ # Handler.set :placeholder_name, &block
11
+ # or
12
+ # Handler.collection do
13
+ # set :placeholder_one, &block
14
+ # set :placeholder_other, &block
15
+ # end
16
+ class Handler
17
+ include SharedMethods
18
+
19
+ @callbacks = {}
20
+
21
+ class << self
22
+ # callbacks setter
23
+ def set action, &block
24
+ action = action.to_sym
25
+ @callbacks[action] = [] unless @callbacks[action].is_a? Array
26
+ @callbacks[action] << block if block_given?
27
+ end
28
+
29
+ # pending callbacks exectuion
30
+ def exec action, *args
31
+ action = action.to_sym
32
+ @callbacks[action].each do |block|
33
+ block.call(*args)
34
+ end if @callbacks[action].is_a? Array
35
+ end
36
+
37
+ # collection setup
38
+ def collection &block
39
+ instance_eval &block if block_given?
40
+ end
41
+ end # << self
42
+ end # Handler
43
+ end # VTools
@@ -0,0 +1,71 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module VTools
4
+
5
+ # Takes care about jobs
6
+ class Harvester
7
+ include SharedMethods
8
+ @jobs = {}
9
+ @run_jobs = 0
10
+
11
+ # get job info
12
+ class << self
13
+
14
+ # collector
15
+ def daemonize!
16
+
17
+ Storage.connect # connect jobs pool
18
+ loop do
19
+
20
+ with_error_handle do # catch job exceptions
21
+ config = json_to_obj Storage.recv
22
+ add_job config
23
+ end if CONFIG[:max_jobs] > @run_jobs
24
+
25
+ sleep CONFIG[:harvester_timer]
26
+ end
27
+ end
28
+
29
+ # set new job
30
+ def add_job config
31
+
32
+ job = Job.new config
33
+
34
+ @jobs[job.id] = job
35
+ @run_jobs += 1
36
+
37
+ # execute job
38
+ Thread.new(job, config) do
39
+ # catch job exceptions here
40
+ with_error_handle do # catch job exceptions
41
+ Storage.send({ :data => job.execute, :action => config.action })
42
+ end
43
+ finish job # in any case close job instance
44
+ end
45
+ end
46
+
47
+ private
48
+ # job terminator
49
+ def finish job
50
+ job = job.id if job.is_a? Job
51
+ return unless @jobs.has_key? job
52
+ @run_jobs -= 1
53
+ @jobs.delete job
54
+ end
55
+
56
+ # error handler
57
+ def with_error_handle &block
58
+ # catch job exceptions here
59
+ begin
60
+ yield if block_given?
61
+ # configuration, create video after convert, valid video file & process
62
+ rescue ConfigError, FileError, FormatError, ProcessError => e
63
+ log :error, "JOB rejected, #{e}"
64
+ rescue => e # uncknown error
65
+ log :fatal, "#{e}"
66
+ raise e
67
+ end
68
+ end
69
+ end # << class
70
+ end # Harvester
71
+ end # VTools