app/models/zencoder_job.rb

Summary

Maintainability
A
2 hrs
Test Coverage
class ZencoderJob < ActiveRecord::Base

  default_scope { order(:created_at) }

  self.primary_key= 'id'
  belongs_to :media_file
  serialize :notification, JsonSerializer
  serialize :request, JsonSerializer
  serialize :response, JsonSerializer

  before_create do |model|
    model.id ||= SecureRandom.uuid 
  end

  scope :only_latest_states, -> {
    where(%Q{
      zencoder_jobs.created_at = (
        SELECT MAX(created_at) from zencoder_jobs AS z_j
        WHERE z_j.media_file_id = zencoder_jobs.media_file_id
      )
    }).reorder("zencoder_jobs.created_at DESC").order(:id)
  }

  scope :failed, -> {
    where(state: 'failed')
  }

  ################################################################
  # config
  ################################################################

  def config
    Settings.zencoder
  end

  def self.config
    Settings.zencoder
  end


  ################################################################
  # submit and send to zencoder
  ################################################################

  def submit 
    begin 

      raise "Illegal State Error, state must be initialized" if state != 'initialized'

      update_attributes request: \
        case Rails.env
        when "production", "test"
          build_zencoder_request
        when "development" 
          build_zencoder_request_for_development
        end

      # send the request to zencoder
      case Rails.env
      when "test" 
        update_attributes state: 'submitted', comment: "Running in test mode, job is not really submitted" 
      else
          send_request_to_zencoder
      end
    rescue => e
      update_attributes state: 'failed', error: (e.message.to_s + "\n\n" + e.backtrace.join("\n"))
    end
  end


  def send_request_to_zencoder
    begin

      if config.api_key.present?
        Zencoder.api_key = config.api_key
      else
        raise "Zencoder API key is mandatory for submitting to Zencoder.com"
      end

      if(response = Zencoder::Job.create(request)).success?
        update_attributes state: 'submitted',
          error: nil,
          response: response.body,
          zencoder_id: response.body["id"],
          state: 'submitted'
      else
        update_attributes state: 'failed', error: response.body rescue nil
      end

    rescue => e
      logger.error (e.message.to_s + "\n\n" + e.backtrace.join("\n")) rescue nil
      update_attributes state: 'failed', error: Formatter.error_to_s(e)
    end
  end

  ################################################################
  # build zencoder request
  ################################################################
   
  def build_zencoder_request
    { input: "#{ENV['URL_HOST_PART']}/media_files/#{media_file.id}?access_hash=#{media_file.access_hash}",
      test: Settings.zencoder.test_mode?,
      notifications: [notification_url]
    }.merge(build_zencoder_outputs_request)
  end

  def width
    THUMBNAILS[:large].split("x").first.to_i rescue 620
  end

  def build_zencoder_outputs_request
    output_default={label: 'Default', base_url: config.ftp_base_url, quality: 4, speed: 2, width: width}
    thumbnails = {interval: 60, width: width, base_url: config.ftp_base_url ,prefix: self.id, format: "jpg"}

    if media_file.content_type =~ /video/
      output_webm = output_default.merge(format: 'webm', filename: "#{self.id}.webm", label: "webm", thumbnails: thumbnails)
      output_apple = output_default.merge(format: 'mp4', filename: "#{self.id}.mp4", video_codec:  "h264",label: "apple")
      { outputs: [output_webm,output_apple] }
    elsif media_file.content_type =~ /audio/
      { outputs: [output_default.merge(audio_codec: 'vorbis',skip_video: true, filename: "#{self.id}.ogg")]}
    else
      raise "don't know what to with this content_type"
    end
  end

  # will send a notification try: 
  # zencoder_fetcher -c 1 -u http://localhost:3000/zencoder_jobs/JOB_ID/notification API_KEY

  def build_zencoder_request_for_development
    build_zencoder_request.merge(
      input: "http://s3.amazonaws.com/zencodertesting/test.mov", 
      test: true)
  end


  ################################################################
  # progress
  ################################################################

  def progress_per_cent
    if zencoder_id and config.api_key
      Zencoder.api_key = config.api_key
      body= Zencoder::Job.progress(zencoder_id).body
      case body['state']
      when 'waiting'
        0.0
      when 'processing'
        body['progress'].to_f
      when 'finished'
        100.0
      else
        -1
      end
    else
      -1
    end
  end


  ################################################################
  # import previews 
  ################################################################

  def import_previews 
    begin 
      update_attributes state: 'importing'
      notification['outputs'].each do |output|
        if media_file.content_type =~ /video/
          import_preview_movie(output)
          (thumbnails = output['thumbnails']) and thumbnails.each do |thumbnail| 
            thumbnails_previews = thumbnail['images'].map do |image|
              import_preview_thumbnail(image)
            end
            if thumbnail_preview = thumbnails_previews.first
              media_file.create_jpeg_previews_for_file thumbnail_preview.full_path
            end
          end
        elsif  media_file.content_type =~ /audio/
          import_preview_audio(output)
        else
          raise "don't know how to import #{media_file.content_type}"
        end
      end
      update_attributes state: 'finished'
    rescue => e
      logger.error e
      update_attributes state: 'failed', error: Formatter.error_to_s(e) rescue nil
      raise e
    end
  end

  def preview_file_name media_file, uri
    media_file.guid + '_' +  uri.path.gsub(/\//,'_')
  end

  def import_preview_audio(output)
    uri = URI.parse(output['url'])
    preview = Preview.create \
      media_file: media_file, 
      content_type: 'audio/' + output['format'],
      filename: preview_file_name(media_file,uri)
    get_ftp_file(uri,preview)
    preview
  end

  def import_preview_movie(output)
    uri = URI.parse(output['url'])
    preview = Preview.create \
      media_file: media_file, 
      height: output['height'],
      width: output['width'],
      content_type: 'video/' + convert_format_to_content_type_postfix(output['format']),
      thumbnail: 'large',
      filename: preview_file_name(media_file,uri)
    get_ftp_file(uri,preview)
    preview
  end

  def convert_format_to_content_type_postfix format
    case format
    when 'mpeg4'
      'mp4'
    else
      format
    end
  end

  def format_to_content_type_second_part format
    case format
    when 'jpg'
      'jpeg'
    else
      format
    end
  end

  def import_preview_thumbnail(image)
    uri = URI.parse(image['url'])
    preview = Preview.create \
      media_file: media_file, 
      height: image['dimensions'].split('x').last,
      width: image['dimensions'].split('x').first,
      content_type: 'image/' + format_to_content_type_second_part(image['format'].downcase),
      thumbnail: 'large',
      filename: preview_file_name(media_file,uri)
    get_ftp_file uri,preview
    preview
  end


  def get_ftp_file uri,preview
    require 'net/ftp'
    begin 
      ftp = Net::FTP.new(uri.host)
      if uri.user and uri.password
        ftp.login(uri.user, uri.password)
      else
        ftp.login
      end
      ftp.getbinaryfile(uri.path, preview.full_path, 1024)
    ensure 
      ftp.close() unless ftp.closed?()
    end
  end



  ################################################################
  # build notification url
  ################################################################

  def notification_url
    url_helpers = Rails.application.routes.url_helpers

    (ENV['URL_HOST_PART'] || "http://zencoderfetcher") + 
      url_helpers.zencoder_job_notification_path(self)
  end



  ################################################################
  # class methods
  ################################################################

  class << self
    def create_zencoder_jobs_if_applicable media_resources
      if Settings.zencoder.enabled?
        media_resources.joins(:media_file) \
          .where("media_files.content_type SIMILAR TO '%(video|audio)%' ").map do |mr| 
          ZencoderJob.create media_file:  mr.media_file
        end
      else
        raise "Zencoder is not enabled! Check your zencoder configuration!"
      end
    end
  end

end