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.
- 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
|