media_attachment.rb 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: media_attachments
  5. #
  6. # id :integer not null, primary key
  7. # status_id :integer
  8. # file_file_name :string
  9. # file_content_type :string
  10. # file_file_size :integer
  11. # file_updated_at :datetime
  12. # remote_url :string default(""), not null
  13. # account_id :integer
  14. # created_at :datetime not null
  15. # updated_at :datetime not null
  16. # shortcode :string
  17. # type :integer default("image"), not null
  18. # file_meta :json
  19. #
  20. require 'mime/types'
  21. class MediaAttachment < ApplicationRecord
  22. self.inheritance_column = nil
  23. enum type: [:image, :gifv, :video, :unknown]
  24. IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
  25. VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
  26. IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
  27. VIDEO_STYLES = {
  28. small: {
  29. convert_options: {
  30. output: {
  31. vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
  32. },
  33. },
  34. format: 'png',
  35. time: 0,
  36. },
  37. }.freeze
  38. belongs_to :account, inverse_of: :media_attachments
  39. belongs_to :status, inverse_of: :media_attachments
  40. has_attached_file :file,
  41. styles: ->(f) { file_styles f },
  42. processors: ->(f) { file_processors f },
  43. convert_options: { all: '-quality 90 -strip' }
  44. include Remotable
  45. validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
  46. validates_attachment_size :file, less_than: 8.megabytes
  47. validates :account, presence: true
  48. scope :attached, -> { where.not(status_id: nil) }
  49. scope :unattached, -> { where(status_id: nil) }
  50. scope :local, -> { where(remote_url: '') }
  51. scope :remote, -> { where.not(remote_url: '') }
  52. default_scope { order(id: :asc) }
  53. def local?
  54. remote_url.blank?
  55. end
  56. def needs_redownload?
  57. file.blank? && remote_url.present?
  58. end
  59. def to_param
  60. shortcode
  61. end
  62. before_create :set_shortcode
  63. before_post_process :set_type_and_extension
  64. before_save :set_meta
  65. class << self
  66. private
  67. def file_styles(f)
  68. if f.instance.file_content_type == 'image/gif'
  69. {
  70. small: IMAGE_STYLES[:small],
  71. original: {
  72. format: 'mp4',
  73. convert_options: {
  74. output: {
  75. 'movflags' => 'faststart',
  76. 'pix_fmt' => 'yuv420p',
  77. 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
  78. 'vsync' => 'cfr',
  79. 'b:v' => '1300K',
  80. 'maxrate' => '500K',
  81. 'bufsize' => '1300K',
  82. 'crf' => 18,
  83. },
  84. },
  85. },
  86. }
  87. elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
  88. IMAGE_STYLES
  89. else
  90. VIDEO_STYLES
  91. end
  92. end
  93. def file_processors(f)
  94. if f.file_content_type == 'image/gif'
  95. [:gif_transcoder]
  96. elsif VIDEO_MIME_TYPES.include? f.file_content_type
  97. [:video_transcoder]
  98. else
  99. [:thumbnail]
  100. end
  101. end
  102. end
  103. private
  104. def set_shortcode
  105. self.type = :unknown if file.blank? && !type_changed?
  106. return unless local?
  107. loop do
  108. self.shortcode = SecureRandom.urlsafe_base64(14)
  109. break if MediaAttachment.find_by(shortcode: shortcode).nil?
  110. end
  111. end
  112. def set_type_and_extension
  113. self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
  114. extension = appropriate_extension
  115. basename = Paperclip::Interpolations.basename(file, :original)
  116. file.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
  117. end
  118. def set_meta
  119. meta = populate_meta
  120. return if meta == {}
  121. file.instance_write :meta, meta
  122. end
  123. def populate_meta
  124. meta = {}
  125. file.queued_for_write.each do |style, file|
  126. begin
  127. geo = Paperclip::Geometry.from_file file
  128. meta[style] = {
  129. width: geo.width.to_i,
  130. height: geo.height.to_i,
  131. size: "#{geo.width.to_i}x#{geo.height.to_i}",
  132. aspect: geo.width.to_f / geo.height.to_f,
  133. }
  134. rescue Paperclip::Errors::NotIdentifiedByImageMagickError
  135. meta[style] = {}
  136. end
  137. end
  138. meta
  139. end
  140. def appropriate_extension
  141. mime_type = MIME::Types[file.content_type]
  142. extensions_for_mime_type = mime_type.empty? ? [] : mime_type.first.extensions
  143. original_extension = Paperclip::Interpolations.extension(file, :original)
  144. extensions_for_mime_type.include?(original_extension) ? original_extension : extensions_for_mime_type.first
  145. end
  146. end