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 'loglevel' => 'fatal',
61 vf
: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
66 file_geometry_parser
: FastGeometryParser
,
67 blurhash
: BLURHASH_OPTIONS
,
71 keep_same_format
: true,
74 'loglevel' => 'fatal',
75 'map_metadata' => '-1',
86 content_type
: 'audio/mpeg',
89 'loglevel' => 'fatal',
98 content_type
: 'video/mp4',
101 'loglevel' => 'fatal',
102 'movflags' => 'faststart',
103 'pix_fmt' => 'yuv420p',
104 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
107 'maxrate' => '1300K',
108 'bufsize' => '1300K',
109 'frames:v' => 60 * 60 * 3,
111 'map_metadata' => '-1',
116 VIDEO_CONVERTED_STYLES
= {
117 small
: VIDEO_STYLES
[:small],
118 original
: VIDEO_FORMAT
,
121 IMAGE_LIMIT
= 10.megabytes
122 VIDEO_LIMIT
= 40.megabytes
124 belongs_to
:account, inverse_of
: :media_attachments, optional
: true
125 belongs_to
:status, inverse_of
: :media_attachments, optional
: true
126 belongs_to
:scheduled_status, inverse_of
: :media_attachments, optional
: true
128 has_attached_file
:file,
129 styles
: ->(f
) { file_styles f
},
130 processors
: ->(f
) { file_processors f
},
131 convert_options
: { all
: '-quality 90 -strip +set modify-date +set create-date
' }
133 validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
134 validates_attachment_size
:file, less_than
: IMAGE_LIMIT
, unless: :larger_media_format?
135 validates_attachment_size
:file, less_than
: VIDEO_LIMIT
, if: :larger_media_format?
136 remotable_attachment
:file, VIDEO_LIMIT
, suppress_errors
: false
138 include Attachmentable
140 validates
:account, presence
: true
141 validates
:description, length
: { maximum
: 1_500 }, if: :local?
143 scope
:attached, -> { where
.not(status_id
: nil).or(where
.not(scheduled_status_id
: nil)) }
144 scope
:unattached, -> { where(status_id
: nil, scheduled_status_id
: nil) }
145 scope
:local, -> { where(remote_url
: '') }
146 scope
:remote, -> { where
.not(remote_url
: '') }
147 scope
:cached, -> { remote
.where
.not(file_file_name
: nil) }
149 default_scope
{ order(id
: :asc) }
155 def needs_redownload
?
156 file
.blank
? && remote_url
.present
?
159 def larger_media_format
?
160 video
? || gifv
? || audio
?
172 return if point
.blank
?
174 x
, y
= (point
.is_a
?(Enumerable
) ? point
: point
.split(',')).map(&:to_f)
176 meta
= file
.instance_read(:meta) || {}
177 meta
['focus'] = { 'x' => x
, 'y' => y
}
179 file
.instance_write(:meta, meta
)
183 x
= file
.meta
['focus']['x']
184 y
= file
.meta
['focus']['y']
189 after_commit
:reset_parent_cache, on
: :update
190 before_create
:prepare_description, unless: :local?
191 before_create
:set_shortcode
192 before_post_process
:set_type_and_extension
193 before_save
:set_meta
196 def supported_mime_types
197 IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
200 def supported_file_extensions
201 IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS
207 if f
.instance
.file_content_type
== 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES
.include?(f
.instance
.file_content_type
)
208 VIDEO_CONVERTED_STYLES
209 elsif IMAGE_MIME_TYPES
.include?(f
.instance
.file_content_type
)
211 elsif VIDEO_MIME_TYPES
.include?(f
.instance
.file_content_type
)
218 def file_processors(f
)
219 if f
.file_content_type
== 'image/gif'
220 [:gif_transcoder, :blurhash_transcoder]
221 elsif VIDEO_MIME_TYPES
.include?(f
.file_content_type
)
222 [:video_transcoder, :blurhash_transcoder, :type_corrector]
223 elsif AUDIO_MIME_TYPES
.include?(f
.file_content_type
)
224 [:transcoder, :type_corrector]
226 [:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
234 self.type
= :unknown if file
.blank
? && !type_changed
?
239 self.shortcode
= SecureRandom
.urlsafe_base64(14)
240 break if MediaAttachment
.find_by(shortcode
: shortcode
).nil?
244 def prepare_description
245 self.description
= description
.strip
[0...420] unless description
.nil?
248 def set_type_and_extension
250 if VIDEO_MIME_TYPES
.include?(file_content_type
)
252 elsif AUDIO_MIME_TYPES
.include?(file_content_type
)
265 file
.instance_write
:meta, meta
269 meta
= file
.instance_read(:meta) || {}
271 file
.queued_for_write
.each
do |style
, file
|
272 meta
[style
] = style
== :small || image
? ? image_geometry(file
) : video_metadata(file
)
278 def image_geometry(file
)
279 width
, height
= FastImage
.size(file
.path
)
281 return {} if width
.nil?
286 size
: "#{width}x#{height}",
287 aspect
: width
.to_f
/ height
.to_f
,
291 def video_metadata(file
)
292 movie
= FFMPEG
::Movie.new(file
.path
)
294 return {} unless movie
.valid
?
298 height
: movie
.height
,
299 frame_rate
: movie
.frame_rate
,
300 duration
: movie
.duration
,
301 bitrate
: movie
.bitrate
,
305 def reset_parent_cache
306 return if status_id
.nil?
308 Rails
.cache
.delete("statuses/#{status_id}")