vtools 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/INSTALL +0 -0
- data/LICENSE +20 -0
- data/README.md +131 -0
- data/Rakefile +29 -0
- data/bin/vtools +22 -0
- data/doc/CONFIG.md +36 -0
- data/doc/HOOKS.md +37 -0
- data/doc/LIB_EXAMPLE.md +109 -0
- data/extconf.rb +7 -0
- data/lib/vtools.rb +79 -0
- data/lib/vtools/config.rb +91 -0
- data/lib/vtools/convert_options.rb +155 -0
- data/lib/vtools/converter.rb +98 -0
- data/lib/vtools/errors.rb +21 -0
- data/lib/vtools/handler.rb +43 -0
- data/lib/vtools/harvester.rb +71 -0
- data/lib/vtools/job.rb +48 -0
- data/lib/vtools/options.rb +101 -0
- data/lib/vtools/shared_methods.rb +131 -0
- data/lib/vtools/storage.rb +67 -0
- data/lib/vtools/thumbnailer.rb +93 -0
- data/lib/vtools/thumbs_options.rb +80 -0
- data/lib/vtools/version.rb +6 -0
- data/lib/vtools/version.rb~ +4 -0
- data/lib/vtools/video.rb +158 -0
- data/setup.rb +1585 -0
- data/spec/config_spec.rb +142 -0
- data/spec/convert_options_spec.rb +284 -0
- data/spec/converter_spec.rb +167 -0
- data/spec/errors_spec.rb +39 -0
- data/spec/fixtures/outputs/file_with_iso-8859-1.txt +35 -0
- data/spec/fixtures/outputs/file_with_no_audio.txt +18 -0
- data/spec/fixtures/outputs/file_with_non_supported_audio.txt +29 -0
- data/spec/fixtures/outputs/file_with_start_value.txt +19 -0
- data/spec/fixtures/outputs/file_with_surround_sound.txt +19 -0
- data/spec/handler_spec.rb +81 -0
- data/spec/harvester_spec.rb +189 -0
- data/spec/job_spec.rb +130 -0
- data/spec/options_spec.rb +52 -0
- data/spec/shared_methods_spec.rb +351 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/storage_spec.rb +106 -0
- data/spec/thumbnailer_spec.rb +178 -0
- data/spec/thumbs_options_spec.rb +159 -0
- data/spec/video_spec.rb +274 -0
- data/vtools.gemspec +29 -0
- metadata +177 -0
data/lib/vtools/job.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# job instance
|
6
|
+
class Job
|
7
|
+
attr_reader :video, :id
|
8
|
+
|
9
|
+
# constructor
|
10
|
+
def initialize config
|
11
|
+
@id = self.object_id.to_i
|
12
|
+
@config = validate config
|
13
|
+
@video = Video.new @config.file
|
14
|
+
end
|
15
|
+
|
16
|
+
# execute job
|
17
|
+
def execute
|
18
|
+
# start callback
|
19
|
+
Handler.exec :job_started, @video, @config.action
|
20
|
+
|
21
|
+
result = @video.get_info # we always get info
|
22
|
+
|
23
|
+
case @config.action
|
24
|
+
when /^convert$/i
|
25
|
+
result = @video.convert @config.setup # will return video
|
26
|
+
when /^thumbs$/i
|
27
|
+
result = @video.create_thumbs @config.setup # will return thumbs array
|
28
|
+
end
|
29
|
+
|
30
|
+
# final callbacks
|
31
|
+
Handler.exec :job_finished, result, @video, @config.action
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
# parse video options
|
36
|
+
def validate options
|
37
|
+
unless options.action =~ /^convert|thumbs|info$/ && options.file && !options.file.empty?
|
38
|
+
raise ConfigError, "Invalid action (config: #{options.marshal_dump})"
|
39
|
+
else
|
40
|
+
return options if options.action =~ /^info$/
|
41
|
+
# empty set error
|
42
|
+
raise ConfigError, "Configuration is empty" if !options.setup || options.setup.empty?
|
43
|
+
end
|
44
|
+
options
|
45
|
+
end
|
46
|
+
|
47
|
+
end # Job
|
48
|
+
end # VTools
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# Takes care about passed via ARGV options
|
6
|
+
class Options
|
7
|
+
class << self
|
8
|
+
# parse config data
|
9
|
+
def parse! args
|
10
|
+
# slice options afer "--" sign if given
|
11
|
+
# catch help & version calls
|
12
|
+
case
|
13
|
+
when(args.include? '--')
|
14
|
+
argv = args[ ( args.index('--') + 1)..-1]
|
15
|
+
when(args.include? '-h')
|
16
|
+
argv = ['-h']
|
17
|
+
when(args.include? '-v')
|
18
|
+
argv = ['-v']
|
19
|
+
else
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
# parse passed options
|
24
|
+
OptionParser.new do |opts|
|
25
|
+
|
26
|
+
dot = ' ' * 4
|
27
|
+
opts.banner = "Usage: vtools <command> <daemon options> -- <options>\n" +
|
28
|
+
"\n" +
|
29
|
+
"Commands:\n" +
|
30
|
+
"#{dot}start start an instance of the application\n" +
|
31
|
+
"#{dot}stop stop all instances of the application\n" +
|
32
|
+
"#{dot}restart stop all instances and restart them afterwards\n" +
|
33
|
+
"#{dot}reload send a SIGHUP to all instances of the application\n" +
|
34
|
+
"#{dot}run start the application and stay on top\n" +
|
35
|
+
"#{dot}zap set the application to a stopped state\n" +
|
36
|
+
"#{dot}status show status (PID) of application instances\n" +
|
37
|
+
"\n" +
|
38
|
+
"Daemon options:\n" +
|
39
|
+
|
40
|
+
"#{dot}-t, --ontop Stay on top (does not daemonize)\n" +
|
41
|
+
"#{dot}-f, --force Force operation\n" +
|
42
|
+
"#{dot}-n, --no_wait Do not wait for processes to stop"
|
43
|
+
|
44
|
+
opts.separator ""
|
45
|
+
opts.separator "Options:"
|
46
|
+
|
47
|
+
# add config file
|
48
|
+
opts.on("-c", "--config-file FILE", "Use configuration file") do |f|
|
49
|
+
CONFIG[:config_file] = f
|
50
|
+
end
|
51
|
+
|
52
|
+
# add log file
|
53
|
+
opts.on("-l", "--log-file [FILE]", "Log process into file (default STDOUT)") do |l|
|
54
|
+
CONFIG[:logging] = true
|
55
|
+
CONFIG[:log_file] = "#{CONFIG[:PWD]}/#{l}" if l
|
56
|
+
end
|
57
|
+
|
58
|
+
# include path
|
59
|
+
opts.on("-I", "--include PATH",
|
60
|
+
"specify $LOAD_PATH (may be used more than once)") do |path|
|
61
|
+
$LOAD_PATH.unshift(*path.split(/:/))
|
62
|
+
end
|
63
|
+
|
64
|
+
# connect additional library
|
65
|
+
opts.on("-r", "--require LIBRARY",
|
66
|
+
"require the library, before daemon starts") do |library|
|
67
|
+
CONFIG[:library] << library
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.separator ""
|
71
|
+
|
72
|
+
# VTools version
|
73
|
+
opts.on_tail("-v", "--version", "Show current version") do |operator|
|
74
|
+
puts VERSION.join(',')
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
|
78
|
+
# options help
|
79
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
80
|
+
puts opts
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
|
84
|
+
# do parse!
|
85
|
+
begin
|
86
|
+
if argv.empty?
|
87
|
+
argv = ["-h"]
|
88
|
+
STDERR.puts "ERROR: No command given\n\n"
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.parse!(argv)
|
92
|
+
Handler.exec :config_parsed # callback
|
93
|
+
rescue OptionParser::ParseError => e
|
94
|
+
STDERR.puts "ERROR: #{e.message}\n\n", opts
|
95
|
+
exit(-1)
|
96
|
+
end
|
97
|
+
end # OptionParser.new
|
98
|
+
end # parse!
|
99
|
+
end # class <<
|
100
|
+
end # Options
|
101
|
+
end # VTools
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# shared methods
|
6
|
+
module SharedMethods
|
7
|
+
# both static & instance bindings
|
8
|
+
module Common
|
9
|
+
|
10
|
+
@@logger = nil
|
11
|
+
|
12
|
+
# custom logger
|
13
|
+
def logger= logger
|
14
|
+
@@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# logger mechanics
|
18
|
+
def log level, message = ""
|
19
|
+
|
20
|
+
if CONFIG[:logging]
|
21
|
+
unless @@logger
|
22
|
+
output = CONFIG[:log_file] || STDOUT
|
23
|
+
logger = Logger.new(output, 1000, 1024000)
|
24
|
+
logger.level = Logger::INFO
|
25
|
+
@@logger = logger
|
26
|
+
end
|
27
|
+
|
28
|
+
@@logger.send(level, message) if @@logger
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# converts json to the ruby object
|
33
|
+
# returns nil on invalid JSON
|
34
|
+
def json_to_obj json_str
|
35
|
+
hash_to_obj(parse_json(json_str))
|
36
|
+
end
|
37
|
+
|
38
|
+
# convert hash into object
|
39
|
+
def hash_to_obj hash
|
40
|
+
OpenStruct.new(hash) rescue raise ConfigError, "Can't convert setup to object"
|
41
|
+
end
|
42
|
+
|
43
|
+
# parse json string into hash
|
44
|
+
def parse_json str
|
45
|
+
JSON.parse str rescue raise ConfigError, "Invalid JSON"
|
46
|
+
end
|
47
|
+
|
48
|
+
# set symbols in place of string keys
|
49
|
+
def keys_to_sym hash
|
50
|
+
return hash unless hash.is_a? Hash
|
51
|
+
hash.inject({}){ |opts,(k,v)| opts[k.to_sym] = v; opts }
|
52
|
+
end
|
53
|
+
|
54
|
+
# config accessor
|
55
|
+
def config data
|
56
|
+
CONFIG[data]
|
57
|
+
end
|
58
|
+
|
59
|
+
# calls TCP/IP applications
|
60
|
+
def network_call url
|
61
|
+
require "socket"
|
62
|
+
|
63
|
+
url =~ %r#^([a-z]+://)?(?:www.)?([^/:]+)(:[\d]+)?(.*)$#
|
64
|
+
protocol, host, port, route =
|
65
|
+
($1 || '')[0...-3], $2, ($3 || ":80")[1..-1].to_i, "/#{$4.to_s.gsub(/^\//, '')}"
|
66
|
+
|
67
|
+
begin
|
68
|
+
sock = TCPSocket.open(host, port)
|
69
|
+
sock.print "GET #{route} HTTP/1.0\r\n\r\n"
|
70
|
+
response = sock.read.split("\r\n\r\n", 2).reverse[0]
|
71
|
+
sock.close
|
72
|
+
rescue => e
|
73
|
+
log :error, e
|
74
|
+
end
|
75
|
+
|
76
|
+
response
|
77
|
+
end
|
78
|
+
|
79
|
+
# function to create correct subdirectories to the file
|
80
|
+
def generate_path file_name, scope = "video"
|
81
|
+
storage = CONFIG[:"#{scope}_storage"]
|
82
|
+
return instance_exec(file_name, &storage) if storage.is_a? Proc
|
83
|
+
(!storage || storage.empty? ? CONFIG[:PWD] : storage).to_s.strip.gsub(%r#/$#, '')
|
84
|
+
end
|
85
|
+
|
86
|
+
# path generator setter
|
87
|
+
def path_generator scope = nil, &block
|
88
|
+
if scope
|
89
|
+
scope = "thumb" unless scope == "video"
|
90
|
+
CONFIG[:"#{scope}_storage"] = block
|
91
|
+
else
|
92
|
+
CONFIG[:thumb_storage] = CONFIG[:video_storage] = block
|
93
|
+
end if block_given?
|
94
|
+
end
|
95
|
+
|
96
|
+
# encoding fixer for iso-8859-1
|
97
|
+
def fix_encoding(output)
|
98
|
+
output[/test/] # Running a regexp on the string throws error if it's not UTF-8
|
99
|
+
rescue ArgumentError
|
100
|
+
output.force_encoding("ISO-8859-1")
|
101
|
+
end
|
102
|
+
end # Common
|
103
|
+
|
104
|
+
# class bindings
|
105
|
+
module Static
|
106
|
+
include Common
|
107
|
+
# load external libs
|
108
|
+
def load_libs
|
109
|
+
CONFIG[:library].each do |lib|
|
110
|
+
begin
|
111
|
+
require lib
|
112
|
+
rescue LoadError => e
|
113
|
+
print "Library file could not be found (#{lib})\n"
|
114
|
+
rescue SyntaxError => e
|
115
|
+
print "Library may contain non ascii characters (#{lib}).\n\n" +
|
116
|
+
"Try to set file encoding to 'binary'.\n\n"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end # Static
|
121
|
+
|
122
|
+
# instance bindings
|
123
|
+
module Instance
|
124
|
+
include Common
|
125
|
+
end # Instance
|
126
|
+
|
127
|
+
# extend it
|
128
|
+
def self.included (klass) klass.extend Static end
|
129
|
+
include Instance
|
130
|
+
end # SharedMethods
|
131
|
+
end # VTools
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# Interface API to the Message Queue or Database Server (AdapterInterface)
|
6
|
+
class Storage
|
7
|
+
include SharedMethods
|
8
|
+
|
9
|
+
@actions = {}
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# constructor (cretes connection)
|
14
|
+
def connect
|
15
|
+
fails __method__ unless @actions[:connect]
|
16
|
+
@actions[:connect].call
|
17
|
+
end
|
18
|
+
|
19
|
+
# recv basic method
|
20
|
+
def recv
|
21
|
+
fails __method__ unless @actions[:recv]
|
22
|
+
@actions[:recv].call
|
23
|
+
end
|
24
|
+
|
25
|
+
# send masic method
|
26
|
+
def send data
|
27
|
+
fails __method__ unless @actions[:send]
|
28
|
+
@actions[:send].call(data)
|
29
|
+
end
|
30
|
+
|
31
|
+
# callback setter to connect to the storage
|
32
|
+
def connect_action &block
|
33
|
+
@actions[:connect] = block
|
34
|
+
end
|
35
|
+
|
36
|
+
# callback setter to recieve data
|
37
|
+
def recv_action &block
|
38
|
+
@actions[:recv] = block
|
39
|
+
end
|
40
|
+
|
41
|
+
# callback setter to send data when done successfully
|
42
|
+
# Storage#send will pass Hash with content:
|
43
|
+
# :data => Job.execution_result,
|
44
|
+
# :action => Job.executed_action
|
45
|
+
def send_action &block
|
46
|
+
@actions[:send] = block
|
47
|
+
end
|
48
|
+
|
49
|
+
# callback setter for the collection
|
50
|
+
# usage:
|
51
|
+
# VTools::Storage.setup do
|
52
|
+
# connect_action { ... }
|
53
|
+
# send_action { |data| ... }
|
54
|
+
# recv_action { ... }
|
55
|
+
def setup &block
|
56
|
+
instance_eval &block if block_given?
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
# errors generator
|
61
|
+
def fails meth
|
62
|
+
p "fails orig: #{meth}"
|
63
|
+
raise NotImplementedError, "VTools::Storage##{meth}_action must be set"
|
64
|
+
end
|
65
|
+
end # class << self
|
66
|
+
end # Storage
|
67
|
+
end # VTools
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# Takes care about generating thumbnails
|
6
|
+
# requires ffmpegthumbnailer
|
7
|
+
class Thumbnailer
|
8
|
+
include SharedMethods
|
9
|
+
|
10
|
+
# constructor
|
11
|
+
def initialize video
|
12
|
+
@video = video
|
13
|
+
end
|
14
|
+
|
15
|
+
# thumbnailer job
|
16
|
+
def run
|
17
|
+
|
18
|
+
options = @video.thumbs_options
|
19
|
+
errors = []
|
20
|
+
thumbs = []
|
21
|
+
postfix = options[:postfix]
|
22
|
+
@total = options[:thumb_count].to_i
|
23
|
+
|
24
|
+
@output_file = "#{generate_path @video.name, "thumb"}/#{@video.name}_"
|
25
|
+
command = "#{CONFIG[:thumb_binary]} -i '#{@video.path}' #{options} "
|
26
|
+
|
27
|
+
# callback
|
28
|
+
Handler.exec :before_thumb, @video, options
|
29
|
+
|
30
|
+
# process cicle
|
31
|
+
@total.times do |count|
|
32
|
+
|
33
|
+
seconds = (options && options[:t]) || set_point(options || count)
|
34
|
+
|
35
|
+
file = "#{@output_file}#{ postfix || (options && options[:t]) || count }.jpg"
|
36
|
+
exec = "#{command} -t #{seconds} -o '#{file}' 2>&1"
|
37
|
+
options = nil
|
38
|
+
|
39
|
+
Open3.popen3(exec) do |stdin, stdout, stderr|
|
40
|
+
# save thumb if no error
|
41
|
+
if (error = VTools.fix_encoding(stdout.readlines).join(" ")).empty?
|
42
|
+
thumbs << thumb = {:path => file, :offset => time_offset(seconds)}
|
43
|
+
|
44
|
+
Handler.exec :in_thumb, @video, thumb # callbacks
|
45
|
+
else
|
46
|
+
errors << "#{error} (#{file})"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# callbacks
|
52
|
+
if errors.empty?
|
53
|
+
Handler.exec :thumb_success, @video, thumbs
|
54
|
+
else
|
55
|
+
errors = " Errors: #{errors.flatten.join(";").gsub(/\n/, ' ')}. "
|
56
|
+
Handler.exec :thumb_error, @video, errors
|
57
|
+
raise ProcessError, "Thumbnailer error: #{errors}" if thumbs.empty? && @total > 0
|
58
|
+
end
|
59
|
+
|
60
|
+
thumbs
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
# permits to set checkpoint
|
65
|
+
# for the current thumb
|
66
|
+
def set_point config
|
67
|
+
|
68
|
+
# start point as config hash given
|
69
|
+
checkpoint = if config.is_a? Hash
|
70
|
+
|
71
|
+
case config[:thumb_start_point].to_s
|
72
|
+
when /(\d+(\.\d+)?)%$/ # shift in percents
|
73
|
+
offset = $1.to_i
|
74
|
+
when /(\d+)$/ # shift in seconds
|
75
|
+
offset = time_offset $1
|
76
|
+
end
|
77
|
+
elsif config.is_a? Integer # thumb number
|
78
|
+
offset = (config * 100 / @total).to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
offset ||= 0
|
82
|
+
end
|
83
|
+
|
84
|
+
# calculates time offset string
|
85
|
+
# by given seconds length
|
86
|
+
def time_offset seconds
|
87
|
+
shift = (@video.duration > seconds.to_f ? seconds : @video.duration).to_i
|
88
|
+
hours, mins = (shift / 360), (shift / 60)
|
89
|
+
sec = shift - (hours * 360 + mins * 60)
|
90
|
+
"%02d:%02d:%02d" % [hours, mins, sec]
|
91
|
+
end
|
92
|
+
end # Thumbnailer
|
93
|
+
end # VTools
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# options for the thumbnailer
|
6
|
+
class ThumbsOptions < Hash
|
7
|
+
include SharedMethods
|
8
|
+
|
9
|
+
# constructor
|
10
|
+
def initialize options = {}
|
11
|
+
|
12
|
+
@ignore = [:thumb_count, :thumb_start_point, :quality, :width, :time, :postfix]
|
13
|
+
# default values
|
14
|
+
merge!(
|
15
|
+
:thumb_count => 0,
|
16
|
+
:thumb_start_point => 0,
|
17
|
+
)
|
18
|
+
|
19
|
+
parse! options
|
20
|
+
end
|
21
|
+
|
22
|
+
# redefine native method
|
23
|
+
# for more readable options
|
24
|
+
def []= index, value
|
25
|
+
case
|
26
|
+
when [:quality, :q].include?(index)
|
27
|
+
former = {:q => value, :quality => value}
|
28
|
+
when [:width, :s].include?(index)
|
29
|
+
former = {:s => value, :width => value}
|
30
|
+
when [:time, :t].include?(index)
|
31
|
+
former = {:t => value, :time => value}
|
32
|
+
else
|
33
|
+
return super
|
34
|
+
end
|
35
|
+
merge! former
|
36
|
+
value
|
37
|
+
end
|
38
|
+
|
39
|
+
# to string
|
40
|
+
def to_s
|
41
|
+
params = collect do |key, value|
|
42
|
+
"-#{key} #{value}" unless @ignore.include?(key)
|
43
|
+
end.compact
|
44
|
+
|
45
|
+
params.join " "
|
46
|
+
end
|
47
|
+
|
48
|
+
# options parser
|
49
|
+
def parse! options
|
50
|
+
|
51
|
+
case
|
52
|
+
# predefined
|
53
|
+
when options.is_a?(String) && CONFIG[:thumb_set].include?(options.to_sym)
|
54
|
+
# get config data
|
55
|
+
s, q, count, start_point = CONFIG[:thumb_set][options.to_sym]
|
56
|
+
options = {:thumb_count => count, :thumb_start_point => start_point, :s => s, :q => q}
|
57
|
+
|
58
|
+
# niether string nor hash..
|
59
|
+
when !options.is_a?(Hash)
|
60
|
+
raise ConfigError, "Options should be a Hash or String (predefined set)"
|
61
|
+
# convert keys to symbols
|
62
|
+
else
|
63
|
+
options = keys_to_sym options
|
64
|
+
# check inline predefined
|
65
|
+
parse! options.delete(:set) if options.has_key? :set
|
66
|
+
end
|
67
|
+
|
68
|
+
perform options
|
69
|
+
merge! options
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
# revalidate special options
|
74
|
+
def perform hash
|
75
|
+
{ :quality => :q, :width => :s, :time => :t }.each do |name, orig|
|
76
|
+
hash[orig] = hash[name] if hash.include?(name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end # ThumbsOptions
|
80
|
+
end # VTools
|