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)
20 # scheduled_status_id :bigint(8)
24 class MediaAttachment
< ApplicationRecord
25 self.inheritance_column
= nil
27 enum type
: [:image, :gifv, :video, :unknown, :audio]
29 IMAGE_FILE_EXTENSIONS
= %w(.jpg
.jpeg
.png
.gif
).freeze
30 VIDEO_FILE_EXTENSIONS
= %w(.webm
.mp4
.m4v
.mov
).freeze
31 AUDIO_FILE_EXTENSIONS
= %w(.ogg
.oga
.mp3
.wav
.flac
.opus
.aac
.m4a
.3gp
.wma
).freeze
33 IMAGE_MIME_TYPES
= %w(image
/jpeg image/png image
/gif
).freeze
34 VIDEO_MIME_TYPES
= %w(video
/webm video/mp4 video
/quicktime video/ogg
).freeze
35 VIDEO_CONVERTIBLE_MIME_TYPES
= %w(video
/webm video/quicktime
).freeze
36 AUDIO_MIME_TYPES
= %w(audio
/wave audio/wav audio
/x-wav audio/x-pn-wave audio
/ogg audio/mpeg audio
/mp3 audio/webm audio
/flac audio/aac audio
/m4a audio/x-m4a audio
/mp4 audio/3gpp video
/x-ms-asf
).freeze
45 pixels
: 1_638_400, # 1280x1280px
46 file_geometry_parser
: FastGeometryParser
,
50 pixels
: 160_000, # 400x400px
51 file_geometry_parser
: FastGeometryParser
,
52 blurhash
: BLURHASH_OPTIONS
,
60 vf
: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
65 file_geometry_parser
: FastGeometryParser
,
66 blurhash
: BLURHASH_OPTIONS
,
70 keep_same_format
: true,
73 'map_metadata' => '-1',
84 content_type
: 'audio/mpeg',
95 content_type
: 'video/mp4',
98 'loglevel' => 'fatal',
99 'movflags' => 'faststart',
100 'pix_fmt' => 'yuv420p',
101 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
104 'maxrate' => '1300K',
105 'bufsize' => '1300K',
106 'frames:v' => 60 * 60 * 3,
108 'map_metadata' => '-1',
113 VIDEO_CONVERTED_STYLES
= {
114 small
: VIDEO_STYLES
[:small],
115 original
: VIDEO_FORMAT
,
118 IMAGE_LIMIT
= 10.megabytes
119 VIDEO_LIMIT
= 40.megabytes
121 belongs_to
:account, inverse_of
: :media_attachments, optional
: true
122 belongs_to
:status, inverse_of
: :media_attachments, optional
: true
123 belongs_to
:scheduled_status, inverse_of
: :media_attachments, optional
: true
125 has_attached_file
:file,
126 styles
: ->(f
) { file_styles f
},
127 processors
: ->(f
) { file_processors f
},
128 convert_options
: { all
: '-quality 90 -strip +set modify-date +set create-date
' }
130 validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
131 validates_attachment_size
:file, less_than
: IMAGE_LIMIT
, unless: :larger_media_format?
132 validates_attachment_size
:file, less_than
: VIDEO_LIMIT
, if: :larger_media_format?
133 remotable_attachment
:file, VIDEO_LIMIT
, suppress_errors
: false
135 include Attachmentable
137 validates
:account, presence
: true
138 validates
:description, length
: { maximum
: 1_500 }, if: :local?
140 scope
:attached, -> { where
.not(status_id
: nil).or(where
.not(scheduled_status_id
: nil)) }
141 scope
:unattached, -> { where(status_id
: nil, scheduled_status_id
: nil) }
142 scope
:local, -> { where(remote_url
: '') }
143 scope
:remote, -> { where
.not(remote_url
: '') }
144 scope
:cached, -> { remote
.where
.not(file_file_name
: nil) }
146 default_scope
{ order(id
: :asc) }
152 def needs_redownload
?
153 file
.blank
? && remote_url
.present
?
156 def larger_media_format
?
157 video
? || gifv
? || audio
?
169 return if point
.blank
?
171 x
, y
= (point
.is_a
?(Enumerable
) ? point
: point
.split(',')).map(&:to_f)
173 meta
= file
.instance_read(:meta) || {}
174 meta
['focus'] = { 'x' => x
, 'y' => y
}
176 file
.instance_write(:meta, meta
)
180 x
= file
.meta
['focus']['x']
181 y
= file
.meta
['focus']['y']
186 after_commit
:reset_parent_cache, on
: :update
187 before_create
:prepare_description, unless: :local?
188 before_create
:set_shortcode
189 before_post_process
:set_type_and_extension
190 before_save
:set_meta
193 def supported_mime_types
194 IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
197 def supported_file_extensions
198 IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS
204 if f
.instance
.file_content_type
== 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES
.include?(f
.instance
.file_content_type
)
205 VIDEO_CONVERTED_STYLES
206 elsif IMAGE_MIME_TYPES
.include?(f
.instance
.file_content_type
)
208 elsif VIDEO_MIME_TYPES
.include?(f
.instance
.file_content_type
)
215 def file_processors(f
)
216 if f
.file_content_type
== 'image/gif'
217 [:gif_transcoder, :blurhash_transcoder]
218 elsif VIDEO_MIME_TYPES
.include?(f
.file_content_type
)
219 [:video_transcoder, :blurhash_transcoder, :type_corrector]
220 elsif AUDIO_MIME_TYPES
.include?(f
.file_content_type
)
221 [:transcoder, :type_corrector]
223 [:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
231 self.type
= :unknown if file
.blank
? && !type_changed
?
236 self.shortcode
= SecureRandom
.urlsafe_base64(14)
237 break if MediaAttachment
.find_by(shortcode
: shortcode
).nil?
241 def prepare_description
242 self.description
= description
.strip
[0...420] unless description
.nil?
245 def set_type_and_extension
247 if VIDEO_MIME_TYPES
.include?(file_content_type
)
249 elsif AUDIO_MIME_TYPES
.include?(file_content_type
)
262 file
.instance_write
:meta, meta
266 meta
= file
.instance_read(:meta) || {}
268 file
.queued_for_write
.each
do |style
, file
|
269 meta
[style
] = style
== :small || image
? ? image_geometry(file
) : video_metadata(file
)
275 def image_geometry(file
)
276 width
, height
= FastImage
.size(file
.path
)
278 return {} if width
.nil?
283 size
: "#{width}x#{height}",
284 aspect
: width
.to_f
/ height
.to_f
,
288 def video_metadata(file
)
289 movie
= FFMPEG
::Movie.new(file
.path
)
291 return {} unless movie
.valid
?
295 height
: movie
.height
,
296 frame_rate
: movie
.frame_rate
,
297 duration
: movie
.duration
,
298 bitrate
: movie
.bitrate
,
302 def reset_parent_cache
303 return if status_id
.nil?
305 Rails
.cache
.delete("statuses/#{status_id}")