vtools 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,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