1 # frozen_string_literal: true
2 # == Schema Information
4 # Table name: media_attachments
6 # id :bigint(8) not null, primary key
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
16 # type :integer default("image"), not null
18 # account_id :bigint(8)
22 class MediaAttachment
< ApplicationRecord
23 self.inheritance_column
= nil
25 enum type
: [:image, :gifv, :video, :unknown]
27 IMAGE_FILE_EXTENSIONS
= ['.jpg', '.jpeg', '.png', '.gif'].freeze
28 VIDEO_FILE_EXTENSIONS
= ['.webm', '.mp4', '.m4v', '.mov'].freeze
30 IMAGE_MIME_TYPES
= ['image/jpeg', 'image/png', 'image/gif'].freeze
31 VIDEO_MIME_TYPES
= ['video/webm', 'video/mp4', 'video/quicktime'].freeze
32 VIDEO_CONVERTIBLE_MIME_TYPES
= ['video/webm', 'video/quicktime'].freeze
36 pixels
: 1_638_400, # 1280x1280px
37 file_geometry_parser
: FastGeometryParser
,
41 pixels
: 160_000, # 400x400px
42 file_geometry_parser
: FastGeometryParser
,
50 vf
: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
62 'movflags' => 'faststart',
63 'pix_fmt' => 'yuv420p',
64 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
75 IMAGE_LIMIT
= 8.megabytes
76 VIDEO_LIMIT
= 40.megabytes
78 belongs_to
:account, inverse_of
: :media_attachments, optional
: true
79 belongs_to
:status, inverse_of
: :media_attachments, optional
: true
81 has_attached_file
:file,
82 styles
: ->(f
) { file_styles f
},
83 processors
: ->(f
) { file_processors f
},
84 convert_options
: { all
: '-quality 90 -strip' }
86 validates_attachment_content_type
:file, content_type
: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
87 validates_attachment_size
:file, less_than
: IMAGE_LIMIT
, unless: :video?
88 validates_attachment_size
:file, less_than
: VIDEO_LIMIT
, if: :video?
89 remotable_attachment
:file, VIDEO_LIMIT
91 include Attachmentable
93 validates
:account, presence
: true
94 validates
:description, length
: { maximum
: 420 }, if: :local?
96 scope
:attached, -> { where
.not(status_id
: nil) }
97 scope
:unattached, -> { where(status_id
: nil) }
98 scope
:local, -> { where(remote_url
: '') }
99 scope
:remote, -> { where
.not(remote_url
: '') }
101 default_scope
{ order(id
: :asc) }
107 def needs_redownload
?
108 file
.blank
? && remote_url
.present
?
116 return if point
.blank
?
118 x
, y
= (point
.is_a
?(Enumerable
) ? point
: point
.split(',')).map(&:to_f)
120 meta
= file
.instance_read(:meta) || {}
121 meta
['focus'] = { 'x' => x
, 'y' => y
}
123 file
.instance_write(:meta, meta
)
127 x
= file
.meta
['focus']['x']
128 y
= file
.meta
['focus']['y']
133 after_commit
:reset_parent_cache, on
: :update
134 before_create
:prepare_description, unless: :local?
135 before_create
:set_shortcode
136 before_post_process
:set_type_and_extension
137 before_save
:set_meta
143 if f
.instance
.file_content_type
== 'image/gif'
145 small
: IMAGE_STYLES
[:small],
146 original
: VIDEO_FORMAT
,
148 elsif IMAGE_MIME_TYPES
.include? f
.instance
.file_content_type
150 elsif VIDEO_CONVERTIBLE_MIME_TYPES
.include?(f
.instance
.file_content_type
)
152 small
: VIDEO_STYLES
[:small],
153 original
: VIDEO_FORMAT
,
160 def file_processors(f
)
161 if f
.file_content_type
== 'image/gif'
163 elsif VIDEO_MIME_TYPES
.include? f
.file_content_type
174 self.type
= :unknown if file
.blank
? && !type_changed
?
179 self.shortcode
= SecureRandom
.urlsafe_base64(14)
180 break if MediaAttachment
.find_by(shortcode
: shortcode
).nil?
184 def prepare_description
185 self.description
= description
.strip
[0...420] unless description
.nil?
188 def set_type_and_extension
189 self.type
= VIDEO_MIME_TYPES
.include?(file_content_type
) ? :video : :image
195 file
.instance_write
:meta, meta
199 meta
= file
.instance_read(:meta) || {}
201 file
.queued_for_write
.each
do |style
, file
|
202 meta
[style
] = style
== :small || image
? ? image_geometry(file
) : video_metadata(file
)
208 def image_geometry(file
)
209 width
, height
= FastImage
.size(file
.path
)
211 return {} if width
.nil?
216 size
: "#{width}x#{height}",
217 aspect
: width
.to_f
/ height
.to_f
,
221 def video_metadata(file
)
222 movie
= FFMPEG
::Movie.new(file
.path
)
224 return {} unless movie
.valid
?
228 height
: movie
.height
,
229 frame_rate
: movie
.frame_rate
,
230 duration
: movie
.duration
,
231 bitrate
: movie
.bitrate
,
235 def reset_parent_cache
236 return if status_id
.nil?
237 Rails
.cache
.delete("statuses/#{status_id}")