media_attachment.rb 4.54 KB
Newer Older
1
# frozen_string_literal: true
yhirano's avatar
yhirano committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# == Schema Information
#
# Table name: media_attachments
#
#  id                :integer          not null, primary key
#  status_id         :integer
#  file_file_name    :string
#  file_content_type :string
#  file_file_size    :integer
#  file_updated_at   :datetime
#  remote_url        :string           default(""), not null
#  account_id        :integer
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#  shortcode         :string
#  type              :integer          default("image"), not null
#  file_meta         :json
#
20

21 22
require 'mime/types'

23
class MediaAttachment < ApplicationRecord
24 25
  self.inheritance_column = nil

26
  enum type: [:image, :gifv, :video, :unknown]
27

Eugen Rochko's avatar
Eugen Rochko committed
28
  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
29
  VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
30

31 32 33 34 35 36 37 38 39 40 41 42 43
  IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
  VIDEO_STYLES = {
    small: {
      convert_options: {
        output: {
          vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
        },
      },
      format: 'png',
      time: 0,
    },
  }.freeze

44 45 46
  belongs_to :account, inverse_of: :media_attachments
  belongs_to :status,  inverse_of: :media_attachments

47
  has_attached_file :file,
48 49
                    styles: ->(f) { file_styles f },
                    processors: ->(f) { file_processors f },
50
                    convert_options: { all: '-quality 90 -strip' }
51 52 53

  include Remotable

54
  validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
55
  validates_attachment_size :file, less_than: 8.megabytes
56 57 58

  validates :account, presence: true

59
  scope :attached, -> { where.not(status_id: nil) }
60
  scope :unattached, -> { where(status_id: nil) }
61
  scope :local, -> { where(remote_url: '') }
alpaca-tc's avatar
alpaca-tc committed
62
  default_scope { order(id: :asc) }
63

64
  def local?
Eugen Rochko's avatar
Eugen Rochko committed
65
    remote_url.blank?
66
  end
67

68 69 70 71 72
  def to_param
    shortcode
  end

  before_create :set_shortcode
73
  before_post_process :set_type_and_extension
Francis Chong's avatar
Francis Chong committed
74
  before_save :set_meta
75

Eugen Rochko's avatar
Eugen Rochko committed
76 77 78 79
  class << self
    private

    def file_styles(f)
80
      if f.instance.file_content_type == 'image/gif'
Eugen Rochko's avatar
Eugen Rochko committed
81
        {
82 83
          small: IMAGE_STYLES[:small],
          original: {
84
            format: 'mp4',
Eugen Rochko's avatar
Eugen Rochko committed
85 86
            convert_options: {
              output: {
87 88 89 90 91
                'movflags' => 'faststart',
                'pix_fmt'  => 'yuv420p',
                'vf'       => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
                'vsync'    => 'cfr',
                'b:v'      => '1300K',
92
                'maxrate'  => '500K',
93 94
                'bufsize'  => '1300K',
                'crf'      => 18,
95
              },
Eugen Rochko's avatar
Eugen Rochko committed
96
            },
97
          },
Eugen Rochko's avatar
Eugen Rochko committed
98
        }
99 100 101 102 103 104 105 106 107 108 109
      elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
        IMAGE_STYLES
      else
        VIDEO_STYLES
      end
    end

    def file_processors(f)
      if f.file_content_type == 'image/gif'
        [:gif_transcoder]
      elsif VIDEO_MIME_TYPES.include? f.file_content_type
110
        [:video_transcoder]
111 112
      else
        [:thumbnail]
Eugen Rochko's avatar
Eugen Rochko committed
113
      end
114 115
    end
  end
116 117 118 119

  private

  def set_shortcode
120
    self.type = :unknown if file.blank? && !type_changed?
121

122 123 124 125 126 127 128
    return unless local?

    loop do
      self.shortcode = SecureRandom.urlsafe_base64(14)
      break if MediaAttachment.find_by(shortcode: shortcode).nil?
    end
  end
129

130
  def set_type_and_extension
Eugen's avatar
Eugen committed
131 132 133
    self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
    extension = appropriate_extension
    basename  = Paperclip::Interpolations.basename(file, :original)
alpaca-tc's avatar
alpaca-tc committed
134
    file.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
Eugen's avatar
Eugen committed
135 136
  end

Francis Chong's avatar
Francis Chong committed
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
  def set_meta
    meta = populate_meta
    return if meta == {}
    file.instance_write :meta, meta
  end

  def populate_meta
    meta = {}
    file.queued_for_write.each do |style, file|
      begin
        geo = Paperclip::Geometry.from_file file
        meta[style] = {
          width: geo.width.to_i,
          height: geo.height.to_i,
          size: "#{geo.width.to_i}x#{geo.height.to_i}",
          aspect: geo.width.to_f / geo.height.to_f,
        }
      rescue Paperclip::Errors::NotIdentifiedByImageMagickError
        meta[style] = {}
      end
    end
    meta
  end

Eugen's avatar
Eugen committed
161 162 163 164 165 166 167
  def appropriate_extension
    mime_type = MIME::Types[file.content_type]

    extensions_for_mime_type = mime_type.empty? ? [] : mime_type.first.extensions
    original_extension       = Paperclip::Interpolations.extension(file, :original)

    extensions_for_mime_type.include?(original_extension) ? original_extension : extensions_for_mime_type.first
168
  end
169
end