闭社主体 forked from https://github.com/tootsuite/mastodon
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

188 lines
5.1 KiB

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