tootsie 0.9.0
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/.gitignore +15 -0
- data/Gemfile +2 -0
- data/License +7 -0
- data/README.md +256 -0
- data/Rakefile +1 -0
- data/Tootsie.gemspec +36 -0
- data/bin/tootsie_task_manager +82 -0
- data/config.ru +22 -0
- data/config/development-sample.yml +4 -0
- data/lib/tootsie.rb +21 -0
- data/lib/tootsie/application.rb +48 -0
- data/lib/tootsie/client.rb +12 -0
- data/lib/tootsie/command_runner.rb +58 -0
- data/lib/tootsie/configuration.rb +29 -0
- data/lib/tootsie/daemon.rb +282 -0
- data/lib/tootsie/ffmpeg_adapter.rb +132 -0
- data/lib/tootsie/image_metadata_extractor.rb +64 -0
- data/lib/tootsie/input.rb +55 -0
- data/lib/tootsie/output.rb +67 -0
- data/lib/tootsie/processors/image_processor.rb +181 -0
- data/lib/tootsie/processors/video_processor.rb +85 -0
- data/lib/tootsie/queues/file_system_queue.rb +65 -0
- data/lib/tootsie/queues/sqs_queue.rb +93 -0
- data/lib/tootsie/s3_utilities.rb +24 -0
- data/lib/tootsie/spawner.rb +99 -0
- data/lib/tootsie/task_manager.rb +51 -0
- data/lib/tootsie/tasks/job_task.rb +111 -0
- data/lib/tootsie/tasks/notify_task.rb +27 -0
- data/lib/tootsie/version.rb +3 -0
- data/lib/tootsie/web_service.rb +37 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/test_files/BF 0622 1820.tif +0 -0
- data/spec/tootsie/command_runner_spec.rb +29 -0
- data/spec/tootsie/image_metadata_extracter_spec.rb +39 -0
- data/spec/tootsie/s3_utilities_spec.rb +40 -0
- metadata +337 -0
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            require 's3'
         | 
| 2 | 
            +
            require 'sqs'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Tootsie
         | 
| 5 | 
            +
                
         | 
| 6 | 
            +
              class Application
         | 
| 7 | 
            +
                
         | 
| 8 | 
            +
                def initialize(options = {})
         | 
| 9 | 
            +
                  @@instance = self
         | 
| 10 | 
            +
                  @environment = options[:environment] || :development
         | 
| 11 | 
            +
                  @logger = options[:logger] || Logger.new($stderr)
         | 
| 12 | 
            +
                  @configuration = Configuration.new
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
                
         | 
| 15 | 
            +
                def configure!
         | 
| 16 | 
            +
                  @configuration.load_from_file(File.join(Dir.pwd, "config/#{@environment}.yml"))
         | 
| 17 | 
            +
                  @queue = Tootsie::SqsQueue.new(@configuration.sqs_queue_name, sqs_service)
         | 
| 18 | 
            +
                  @task_manager = TaskManager.new(@queue)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                def s3_service
         | 
| 22 | 
            +
                  return @s3_service ||= ::S3::Service.new(
         | 
| 23 | 
            +
                    :access_key_id => @configuration.aws_access_key_id,
         | 
| 24 | 
            +
                    :secret_access_key => @configuration.aws_secret_access_key)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def sqs_service
         | 
| 28 | 
            +
                  return @sqs_service ||= ::Sqs::Service.new(
         | 
| 29 | 
            +
                    :access_key_id => @configuration.aws_access_key_id,
         | 
| 30 | 
            +
                    :secret_access_key => @configuration.aws_secret_access_key)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
                
         | 
| 33 | 
            +
                class << self
         | 
| 34 | 
            +
                  def get
         | 
| 35 | 
            +
                    @@instance
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
                
         | 
| 39 | 
            +
                attr_accessor :environment
         | 
| 40 | 
            +
                
         | 
| 41 | 
            +
                attr_reader :configuration
         | 
| 42 | 
            +
                attr_reader :task_manager
         | 
| 43 | 
            +
                attr_reader :queue
         | 
| 44 | 
            +
                attr_reader :logger
         | 
| 45 | 
            +
                
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
              
         | 
| 48 | 
            +
            end
         | 
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            module Tootsie
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              class CommandExecutionFailed < StandardError; end
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              class CommandRunner
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(command_line, options = {})
         | 
| 8 | 
            +
                  @options = options.symbolize_keys
         | 
| 9 | 
            +
                  @options.assert_valid_keys(:ignore_exit_code, :output_encoding)
         | 
| 10 | 
            +
                  @command_line = command_line
         | 
| 11 | 
            +
                  @logger = Application.get.logger
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def run(params = {}, &block)
         | 
| 15 | 
            +
                  command_line = @command_line
         | 
| 16 | 
            +
                  if params.any?
         | 
| 17 | 
            +
                    params = params.with_indifferent_access
         | 
| 18 | 
            +
                    command_line = command_line.gsub(/(^|\s):(\w+)/) do
         | 
| 19 | 
            +
                      pre, key, all = $1, $2, $~[0]
         | 
| 20 | 
            +
                      if params.include?(key)
         | 
| 21 | 
            +
                        value = params[key]
         | 
| 22 | 
            +
                        value = "'#{value}'" if value =~ /\s/
         | 
| 23 | 
            +
                        "#{pre}#{value}"
         | 
| 24 | 
            +
                      else
         | 
| 25 | 
            +
                        all
         | 
| 26 | 
            +
                      end
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                  command_line = "#{command_line} 2>&1"
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  @logger.info("Running command: #{command_line}") if @logger.info?
         | 
| 32 | 
            +
                  IO.popen(command_line, "r:#{@options[:output_encoding] || 'utf-8'}") do |output|
         | 
| 33 | 
            +
                    output.each_line do |line|
         | 
| 34 | 
            +
                      @logger.info("[Command output] #{line.strip}") if @logger.info?
         | 
| 35 | 
            +
                      yield line if block_given?
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                  status = $?
         | 
| 39 | 
            +
                  if status.exited?
         | 
| 40 | 
            +
                    if status.exitstatus != 0
         | 
| 41 | 
            +
                      if @options[:ignore_exit_code]
         | 
| 42 | 
            +
                        return false
         | 
| 43 | 
            +
                      else
         | 
| 44 | 
            +
                        raise CommandExecutionFailed, "Command failed with exit code #{status.exitstatus}: #{command_line}"
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  elsif status.stopped?
         | 
| 48 | 
            +
                    raise CommandExecutionFailed, "Command stopped unexpectedly with signal #{status.stopsig}: #{command_line}"
         | 
| 49 | 
            +
                  elsif status.signaled?
         | 
| 50 | 
            +
                    raise CommandExecutionFailed, "Command died unexpectedly by signal #{status.termsig}: #{command_line}"
         | 
| 51 | 
            +
                  else
         | 
| 52 | 
            +
                    raise CommandExecutionFailed, "Command died unexpectedly: #{command_line}"
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                  true
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            module Tootsie
         | 
| 2 | 
            +
              
         | 
| 3 | 
            +
              class Configuration
         | 
| 4 | 
            +
                
         | 
| 5 | 
            +
                def initialize
         | 
| 6 | 
            +
                  @ffmpeg_thread_count = 1
         | 
| 7 | 
            +
                  @sqs_queue_name = 'tootsie'
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
                
         | 
| 10 | 
            +
                def load_from_file(file_name)
         | 
| 11 | 
            +
                  config = (YAML.load(File.read(file_name)) || {}).with_indifferent_access
         | 
| 12 | 
            +
                  [:aws_access_key_id, :aws_secret_access_key, :ffmpeg_thread_count,
         | 
| 13 | 
            +
                    :sqs_queue_name].each do |key|
         | 
| 14 | 
            +
                    if config.include?(key)
         | 
| 15 | 
            +
                      value = config[key]
         | 
| 16 | 
            +
                      value = $1.to_i if value =~ /\A\s*(\d+)\s*\z/
         | 
| 17 | 
            +
                      instance_variable_set("@#{key}", value)
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                attr_accessor :aws_access_key_id
         | 
| 23 | 
            +
                attr_accessor :aws_secret_access_key
         | 
| 24 | 
            +
                attr_accessor :ffmpeg_thread_count
         | 
| 25 | 
            +
                attr_accessor :sqs_queue_name
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
              
         | 
| 29 | 
            +
            end
         | 
| @@ -0,0 +1,282 @@ | |
| 1 | 
            +
            require "logger"
         | 
| 2 | 
            +
            require "fileutils"
         | 
| 3 | 
            +
            require 'optparse'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Tootsie
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              class DaemonError < Exception; end
         | 
| 8 | 
            +
              class DaemonAlreadyRunning < DaemonError; end
         | 
| 9 | 
            +
              class DaemonNotRunning < DaemonError; end
         | 
| 10 | 
            +
              class DaemonTerminationFailed < DaemonError; end
         | 
| 11 | 
            +
              class DaemonNotConfigured < DaemonError; end
         | 
| 12 | 
            +
              class DaemonStartFailed < DaemonError; end 
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              # Daemon controller class that encapsulates a running daemon and a remote interface to it.
         | 
| 15 | 
            +
              class Daemon
         | 
| 16 | 
            +
              
         | 
| 17 | 
            +
                # Initializes the daemon controller.
         | 
| 18 | 
            +
                def initialize(options = {})
         | 
| 19 | 
            +
                  @root = options[:root] || Dir.pwd
         | 
| 20 | 
            +
                  @pid_file = options[:pid_file]
         | 
| 21 | 
            +
                  @logger = options[:logger] || Logger.new('/dev/null')
         | 
| 22 | 
            +
                  @on_spawn = nil
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                # Specifies a block to execute to run the actual daemon. Each call overrides the previous one.  
         | 
| 26 | 
            +
                def on_spawn(&block)
         | 
| 27 | 
            +
                  @on_spawn = block
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                # Specifies a block to execute to termiantion. Each call overrides the previous one.  
         | 
| 31 | 
            +
                def on_terminate(&block)
         | 
| 32 | 
            +
                  @on_terminate = block
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              
         | 
| 35 | 
            +
                # Control the daemon through command-line arguments.
         | 
| 36 | 
            +
                def control(args, title = nil)
         | 
| 37 | 
            +
                  $stderr.sync = true
         | 
| 38 | 
            +
                  title ||= File.basename($0)
         | 
| 39 | 
            +
                  command = args.shift
         | 
| 40 | 
            +
                  control_with_command(command, args, title)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              
         | 
| 43 | 
            +
                # Control the daemon through a specific command.
         | 
| 44 | 
            +
                def control_with_command(command, args, title = nil)
         | 
| 45 | 
            +
                  case command
         | 
| 46 | 
            +
                    when "start"
         | 
| 47 | 
            +
                      $stderr << "Starting #{title}: "
         | 
| 48 | 
            +
                      handle_errors do
         | 
| 49 | 
            +
                        start
         | 
| 50 | 
            +
                        $stderr << "started\n"
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
                    when "stop"
         | 
| 53 | 
            +
                      $stderr << "Stopping #{title}: "
         | 
| 54 | 
            +
                      options = {}
         | 
| 55 | 
            +
                      handle_errors do
         | 
| 56 | 
            +
                        stop({:signal => "TERM"}.merge(options))
         | 
| 57 | 
            +
                        $stderr << "stopped\n"
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
                    when "restart"
         | 
| 60 | 
            +
                      $stderr << "Restarting #{title}: "
         | 
| 61 | 
            +
                      handle_errors do
         | 
| 62 | 
            +
                        restart
         | 
| 63 | 
            +
                        $stderr << "restarted\n"
         | 
| 64 | 
            +
                      end
         | 
| 65 | 
            +
                    when "status"
         | 
| 66 | 
            +
                      if running?
         | 
| 67 | 
            +
                        $stderr << "#{title} is running\n"
         | 
| 68 | 
            +
                      else
         | 
| 69 | 
            +
                        $stderr << "#{title} is not running\n"
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
                  else
         | 
| 72 | 
            +
                    if command
         | 
| 73 | 
            +
                      $stderr << "#{File.basename($0)}: Invalid command #{command}\n"
         | 
| 74 | 
            +
                    else
         | 
| 75 | 
            +
                      puts "Usage: #{File.basename($0)} [start | stop | restart | status]"
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              
         | 
| 80 | 
            +
                # Starts daemon.
         | 
| 81 | 
            +
                def start
         | 
| 82 | 
            +
                  raise DaemonNotConfigured, "Daemon not configured" unless @on_spawn
         | 
| 83 | 
            +
                  FileUtils.mkdir_p(File.dirname(@pid_file)) rescue nil
         | 
| 84 | 
            +
                  pid = self.pid
         | 
| 85 | 
            +
                  if pid
         | 
| 86 | 
            +
                    if pid_running?(pid)
         | 
| 87 | 
            +
                      raise DaemonAlreadyRunning, "Process is already running with pid #{pid}"
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                  File.delete(@pid_file) rescue nil
         | 
| 91 | 
            +
                  child_pid = Process.fork
         | 
| 92 | 
            +
                  if child_pid
         | 
| 93 | 
            +
                    sleep(1)      
         | 
| 94 | 
            +
                    unless running?
         | 
| 95 | 
            +
                      pid = self.pid
         | 
| 96 | 
            +
                      if pid == child_pid
         | 
| 97 | 
            +
                        raise DaemonStartFailed, "Daemon started, but failed prematurely"
         | 
| 98 | 
            +
                      else
         | 
| 99 | 
            +
                        raise DaemonStartFailed, "Daemon failed to start for some unknown reason"
         | 
| 100 | 
            +
                      end
         | 
| 101 | 
            +
                    end      
         | 
| 102 | 
            +
                    return
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
                  class << logger
         | 
| 105 | 
            +
                    def format_message(severity, timestamp, progname, msg)
         | 
| 106 | 
            +
                      "[#{timestamp}] #{msg}\n"
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
                  logger.info("Starting")
         | 
| 110 | 
            +
                  begin
         | 
| 111 | 
            +
                    Process.setsid
         | 
| 112 | 
            +
                    0.upto(255) do |n|
         | 
| 113 | 
            +
                      File.for_fd(n, "r").close rescue nil
         | 
| 114 | 
            +
                    end
         | 
| 115 | 
            +
                    File.umask(27)
         | 
| 116 | 
            +
                    Dir.chdir(@root)
         | 
| 117 | 
            +
                    $stdin = File.open("/dev/null", File::RDWR)
         | 
| 118 | 
            +
                    $stdout = File.open("/dev/null", File::RDWR)
         | 
| 119 | 
            +
                    $stderr = File.open("/dev/null", File::RDWR)
         | 
| 120 | 
            +
                    @pid = Process.pid
         | 
| 121 | 
            +
                    File.open(@pid_file, "w") do |file|
         | 
| 122 | 
            +
                      file << Process.pid
         | 
| 123 | 
            +
                    end
         | 
| 124 | 
            +
                    Signal.trap("HUP") do
         | 
| 125 | 
            +
                      logger.debug("Ignoring SIGHUP")
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
                    Signal.trap("TERM") do
         | 
| 128 | 
            +
                      if $$ == @pid
         | 
| 129 | 
            +
                        logger.info("Terminating (#{$$})")
         | 
| 130 | 
            +
                        @on_terminate.call if @on_terminate
         | 
| 131 | 
            +
                        File.delete(@pid_file) rescue nil
         | 
| 132 | 
            +
                      else
         | 
| 133 | 
            +
                        # Was sent to a child
         | 
| 134 | 
            +
                      end
         | 
| 135 | 
            +
                      exit(0)
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
                    @on_spawn.call
         | 
| 138 | 
            +
                    exit(0)
         | 
| 139 | 
            +
                  rescue SystemExit
         | 
| 140 | 
            +
                    # Do nothing
         | 
| 141 | 
            +
                  rescue Exception => e
         | 
| 142 | 
            +
                    message = "#{e.message}\n"
         | 
| 143 | 
            +
                    message << e.backtrace.map { |line| "\tfrom #{line}\n" }.join
         | 
| 144 | 
            +
                    logger.error(message)
         | 
| 145 | 
            +
                    exit(1)
         | 
| 146 | 
            +
                  ensure
         | 
| 147 | 
            +
                    logger.close
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
              
         | 
| 151 | 
            +
                # Stops daemon.
         | 
| 152 | 
            +
                def stop(options = {})
         | 
| 153 | 
            +
                  stopped = false
         | 
| 154 | 
            +
                  found = false
         | 
| 155 | 
            +
                  pid = self.pid
         | 
| 156 | 
            +
                  if pid
         | 
| 157 | 
            +
                    # Send TERM to process
         | 
| 158 | 
            +
                    begin
         | 
| 159 | 
            +
                      Process.kill(options[:signal] || "TERM", pid)
         | 
| 160 | 
            +
                    rescue Errno::ESRCH
         | 
| 161 | 
            +
                      stopped = true
         | 
| 162 | 
            +
                    rescue Exception => e
         | 
| 163 | 
            +
                      raise DaemonTerminationFailed, "Could not stop process #{pid}: #{e.message}"
         | 
| 164 | 
            +
                    end
         | 
| 165 | 
            +
                    unless stopped
         | 
| 166 | 
            +
                      # Process was signaled, now wait for it to die
         | 
| 167 | 
            +
                      found = true
         | 
| 168 | 
            +
                      30.times do
         | 
| 169 | 
            +
                        begin
         | 
| 170 | 
            +
                          if not pid_running?(pid)
         | 
| 171 | 
            +
                            stopped = true
         | 
| 172 | 
            +
                            break
         | 
| 173 | 
            +
                          end
         | 
| 174 | 
            +
                          sleep(1)
         | 
| 175 | 
            +
                        rescue Exception => e
         | 
| 176 | 
            +
                          raise DaemonTerminationFailed, "Could not stop process #{pid}: #{e.message}"
         | 
| 177 | 
            +
                        end
         | 
| 178 | 
            +
                      end
         | 
| 179 | 
            +
                      if found and not stopped
         | 
| 180 | 
            +
                        # Process still running after wait, force kill and wait
         | 
| 181 | 
            +
                        begin
         | 
| 182 | 
            +
                          Process.kill("KILL", pid)
         | 
| 183 | 
            +
                        rescue Errno::ESRCH
         | 
| 184 | 
            +
                          stopped = true
         | 
| 185 | 
            +
                        end
         | 
| 186 | 
            +
                        30.times do
         | 
| 187 | 
            +
                          begin
         | 
| 188 | 
            +
                            if not pid_running?(pid)
         | 
| 189 | 
            +
                              stopped = true
         | 
| 190 | 
            +
                              break
         | 
| 191 | 
            +
                            end
         | 
| 192 | 
            +
                            sleep(1)
         | 
| 193 | 
            +
                          rescue Exception => e
         | 
| 194 | 
            +
                            raise DaemonTerminationFailed, "Could not stop process #{pid}: #{e.message}"
         | 
| 195 | 
            +
                          end
         | 
| 196 | 
            +
                        end
         | 
| 197 | 
            +
                        if not stopped
         | 
| 198 | 
            +
                          raise DaemonTerminationFailed, "Timeout attempting to stop process #{pid}"
         | 
| 199 | 
            +
                        end
         | 
| 200 | 
            +
                      end
         | 
| 201 | 
            +
                    end
         | 
| 202 | 
            +
                  end
         | 
| 203 | 
            +
                  unless found
         | 
| 204 | 
            +
                    raise DaemonNotRunning, "Process is not running"
         | 
| 205 | 
            +
                  end
         | 
| 206 | 
            +
                end
         | 
| 207 | 
            +
              
         | 
| 208 | 
            +
                # Restarts daemon.
         | 
| 209 | 
            +
                def restart
         | 
| 210 | 
            +
                  if running?
         | 
| 211 | 
            +
                    begin
         | 
| 212 | 
            +
                      stop
         | 
| 213 | 
            +
                    rescue DaemonNotRunning
         | 
| 214 | 
            +
                      # Ignore
         | 
| 215 | 
            +
                    end
         | 
| 216 | 
            +
                  end
         | 
| 217 | 
            +
                  start
         | 
| 218 | 
            +
                end
         | 
| 219 | 
            +
              
         | 
| 220 | 
            +
                # Is the daemon running?
         | 
| 221 | 
            +
                def running?
         | 
| 222 | 
            +
                  !pid.nil?
         | 
| 223 | 
            +
                end
         | 
| 224 | 
            +
              
         | 
| 225 | 
            +
                # Returns the daemon pid.
         | 
| 226 | 
            +
                def pid
         | 
| 227 | 
            +
                  pid = nil
         | 
| 228 | 
            +
                  maybe_pid = File.read(@pid_file) rescue nil
         | 
| 229 | 
            +
                  if maybe_pid =~ /([0-9]+)/
         | 
| 230 | 
            +
                    maybe_pid = $1.to_i
         | 
| 231 | 
            +
                    begin
         | 
| 232 | 
            +
                      Process.kill(0, maybe_pid)
         | 
| 233 | 
            +
                    rescue Errno::ESRCH
         | 
| 234 | 
            +
                    else
         | 
| 235 | 
            +
                      pid = maybe_pid
         | 
| 236 | 
            +
                    end
         | 
| 237 | 
            +
                  end
         | 
| 238 | 
            +
                  pid
         | 
| 239 | 
            +
                end
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                # Signals the daemon.  
         | 
| 242 | 
            +
                def signal(signal)
         | 
| 243 | 
            +
                  pid = self.pid
         | 
| 244 | 
            +
                  if pid
         | 
| 245 | 
            +
                    Process.kill(signal, pid)
         | 
| 246 | 
            +
                  else
         | 
| 247 | 
            +
                    raise DaemonNotRunning, "Process is not running"
         | 
| 248 | 
            +
                  end
         | 
| 249 | 
            +
                end
         | 
| 250 | 
            +
              
         | 
| 251 | 
            +
                attr_reader :root
         | 
| 252 | 
            +
                attr_reader :pid_file
         | 
| 253 | 
            +
                attr_reader :logger
         | 
| 254 | 
            +
              
         | 
| 255 | 
            +
                private
         | 
| 256 | 
            +
             | 
| 257 | 
            +
                  def pid_running?(pid)
         | 
| 258 | 
            +
                    begin
         | 
| 259 | 
            +
                      Process.kill(0, pid)
         | 
| 260 | 
            +
                    rescue Errno::ESRCH
         | 
| 261 | 
            +
                      return false
         | 
| 262 | 
            +
                    end
         | 
| 263 | 
            +
                    return true
         | 
| 264 | 
            +
                  end
         | 
| 265 | 
            +
                    
         | 
| 266 | 
            +
                  def handle_errors(&block)
         | 
| 267 | 
            +
                    begin
         | 
| 268 | 
            +
                      yield
         | 
| 269 | 
            +
                    rescue DaemonError => e
         | 
| 270 | 
            +
                      $stderr << "#{e.message}\n"
         | 
| 271 | 
            +
                      if e.is_a?(DaemonAlreadyRunning) or e.is_a?(DaemonNotRunning)
         | 
| 272 | 
            +
                        exit_code = 0
         | 
| 273 | 
            +
                      else
         | 
| 274 | 
            +
                        exit_code = 1
         | 
| 275 | 
            +
                      end
         | 
| 276 | 
            +
                      exit(exit_code)
         | 
| 277 | 
            +
                    end
         | 
| 278 | 
            +
                  end
         | 
| 279 | 
            +
             | 
| 280 | 
            +
              end
         | 
| 281 | 
            +
             | 
| 282 | 
            +
            end
         | 
| @@ -0,0 +1,132 @@ | |
| 1 | 
            +
            module Tootsie
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              class FfmpegAdapter
         | 
| 4 | 
            +
                
         | 
| 5 | 
            +
                def initialize(options = {})
         | 
| 6 | 
            +
                  @logger = Application.get.logger
         | 
| 7 | 
            +
                  @ffmpeg_binary = 'ffmpeg'
         | 
| 8 | 
            +
                  @ffmpeg_arguments = {}
         | 
| 9 | 
            +
                  @ffmpeg_arguments['threads'] = (options[:thread_count] || 1)
         | 
| 10 | 
            +
                  @ffmpeg_arguments['v'] = 1
         | 
| 11 | 
            +
                  if false
         | 
| 12 | 
            +
                    # TODO: Only in newer FFmpeg versions
         | 
| 13 | 
            +
                    @ffmpeg_arguments['xerror'] = true
         | 
| 14 | 
            +
                    @ffmpeg_arguments['loglevel'] = 'verbose'
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                  @ffmpeg_arguments['y'] = true
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
                
         | 
| 19 | 
            +
                # Transcode a file by taking an input file and writing an output file.
         | 
| 20 | 
            +
                def transcode(input_filename, output_filename, options = {})
         | 
| 21 | 
            +
                  arguments = @ffmpeg_arguments.dup
         | 
| 22 | 
            +
                  if options[:audio_codec].to_s == 'none'
         | 
| 23 | 
            +
                    arguments['an'] = true
         | 
| 24 | 
            +
                  else
         | 
| 25 | 
            +
                    case options[:audio_codec].try(:to_s)
         | 
| 26 | 
            +
                      when 'aac'
         | 
| 27 | 
            +
                        arguments['acodec'] = 'libfaac'
         | 
| 28 | 
            +
                      when String
         | 
| 29 | 
            +
                        arguments['acodec'] = options[:audio_codec]
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                    arguments['ar'] = options[:audio_sample_rate] if options[:audio_sample_rate]
         | 
| 32 | 
            +
                    arguments['ab'] = options[:audio_bitrate] if options[:audio_bitrate]
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                  if options[:video_codec].to_s == 'none'
         | 
| 35 | 
            +
                    arguments['vn'] = true
         | 
| 36 | 
            +
                  else
         | 
| 37 | 
            +
                    case options[:video_codec].try(:to_s)
         | 
| 38 | 
            +
                      when 'h264'
         | 
| 39 | 
            +
                        arguments['vcodec'] = 'libx264'
         | 
| 40 | 
            +
                        arguments['vpre'] = ['medium', 'main']  # TODO: Allow override
         | 
| 41 | 
            +
                        arguments['crf'] = 15                   # TODO: Allow override
         | 
| 42 | 
            +
                        arguments['threads'] = 0
         | 
| 43 | 
            +
                      when String
         | 
| 44 | 
            +
                        arguments['vcodec'] = options[:video_codec]
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                    arguments['b'] = options[:video_bitrate] if options[:video_bitrate]
         | 
| 47 | 
            +
                    arguments['r'] = options[:video_frame_rate] if options[:video_frame_rate]
         | 
| 48 | 
            +
                    arguments['s'] = "#{options[:width]}x#{options[:height]}" if options[:width] or options[:height]
         | 
| 49 | 
            +
                    arguments['sameq'] = true
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  arguments['f'] = options[:format] if options[:format]
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  progress, expected_duration = @progress, nil
         | 
| 54 | 
            +
                  result_width, result_height = nil
         | 
| 55 | 
            +
                  run_ffmpeg(input_filename, output_filename, arguments) do |line|
         | 
| 56 | 
            +
                    if progress
         | 
| 57 | 
            +
                      case line
         | 
| 58 | 
            +
                        when /^\s*Duration: (\d+):(\d+):(\d+)\./
         | 
| 59 | 
            +
                          unless expected_duration
         | 
| 60 | 
            +
                            hours, minutes, seconds = $1.to_i, $2.to_i, $3.to_i
         | 
| 61 | 
            +
                            expected_duration = seconds + minutes * 60 + hours * 60 * 60
         | 
| 62 | 
            +
                          end
         | 
| 63 | 
            +
                        when /^frame=.* time=(\d+)\./
         | 
| 64 | 
            +
                          if expected_duration
         | 
| 65 | 
            +
                            elapsed_time = $1.to_i
         | 
| 66 | 
            +
                          end
         | 
| 67 | 
            +
                        when /Stream.*Video: .*, (\d+)x(\d+)\s/
         | 
| 68 | 
            +
                          unless result_width and result_height
         | 
| 69 | 
            +
                            result_width, result_height = $1.to_i, $2.to_i
         | 
| 70 | 
            +
                          end
         | 
| 71 | 
            +
                      end
         | 
| 72 | 
            +
                      progress.call(elapsed_time, expected_duration) if elapsed_time
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  thumbnail_options = options[:thumbnail]
         | 
| 77 | 
            +
                  if thumbnail_options
         | 
| 78 | 
            +
                    thumb_width = thumbnail_options[:width].try(:to_i) || options[:width].try(:to_i)
         | 
| 79 | 
            +
                    thumb_height = thumbnail_options[:height].try(:to_i) || options[:height].try(:to_i)
         | 
| 80 | 
            +
                    if not thumbnail_options[:force_aspect_ratio] and result_width and result_height
         | 
| 81 | 
            +
                      thumb_height = (thumb_width / (result_width / result_height.to_f)).to_i
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                    at_seconds = thumbnail_options[:at_seconds].try(:to_f)
         | 
| 84 | 
            +
                    at_seconds ||= (expected_duration || 0) * (thumbnail_options[:at_fraction].try(:to_f) || 0.5)
         | 
| 85 | 
            +
                    @logger.info("Getting thumbnail frame (#{thumb_width}x#{thumb_height}) with FFmpeg at #{at_seconds} seconds")
         | 
| 86 | 
            +
                    begin
         | 
| 87 | 
            +
                      run_ffmpeg(input_filename, thumbnail_options[:filename], @ffmpeg_arguments.merge(
         | 
| 88 | 
            +
                        :ss => at_seconds,
         | 
| 89 | 
            +
                        :vcodec => :mjpeg,
         | 
| 90 | 
            +
                        :vframes => 1,
         | 
| 91 | 
            +
                        :an => true,
         | 
| 92 | 
            +
                        :f => :rawvideo,
         | 
| 93 | 
            +
                        :s => "#{thumb_width}x#{thumb_height}"))
         | 
| 94 | 
            +
                    rescue FfmpegAdapterExecutionFailed => e
         | 
| 95 | 
            +
                      @logger.error("Thumbnail rendering failed, ignoring: #{e}")
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
                
         | 
| 100 | 
            +
                attr_accessor :ffmpeg_binary
         | 
| 101 | 
            +
                attr_accessor :ffmpeg_arguments
         | 
| 102 | 
            +
                
         | 
| 103 | 
            +
                # Output captured from FFmpeg command line tool so far.
         | 
| 104 | 
            +
                attr_reader :output
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                # Progress reporter that implements +call(seconds, total_seconds)+ to record
         | 
| 107 | 
            +
                # transcoding progress.
         | 
| 108 | 
            +
                attr_accessor :progress
         | 
| 109 | 
            +
                
         | 
| 110 | 
            +
                private
         | 
| 111 | 
            +
                
         | 
| 112 | 
            +
                  def run_ffmpeg(input_filename, output_filename, arguments, &block)
         | 
| 113 | 
            +
                    command_line = @ffmpeg_binary.dup
         | 
| 114 | 
            +
                    command_line << " -i '#{input_filename}' "
         | 
| 115 | 
            +
                    command_line << arguments.map { |k, v|
         | 
| 116 | 
            +
                      case v
         | 
| 117 | 
            +
                        when TrueClass, FalseClass
         | 
| 118 | 
            +
                          "-#{k}"
         | 
| 119 | 
            +
                        when Array
         | 
| 120 | 
            +
                          v.map { |w| "-#{k} '#{w}'" }.join(' ')
         | 
| 121 | 
            +
                        else
         | 
| 122 | 
            +
                          "-#{k} '#{v}'"
         | 
| 123 | 
            +
                      end
         | 
| 124 | 
            +
                    }.join(' ')
         | 
| 125 | 
            +
                    command_line << ' '
         | 
| 126 | 
            +
                    command_line << "'#{output_filename}'"
         | 
| 127 | 
            +
                    CommandRunner.new(command_line).run(&block)
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
              end
         | 
| 131 | 
            +
                
         | 
| 132 | 
            +
            end
         |