|
@@ -21,6 +21,11 @@
|
|
|
# blurhash :string
|
|
|
# processing :integer
|
|
|
# file_storage_schema_version :integer
|
|
|
+# thumbnail_file_name :string
|
|
|
+# thumbnail_content_type :string
|
|
|
+# thumbnail_file_size :integer
|
|
|
+# thumbnail_updated_at :datetime
|
|
|
+# thumbnail_remote_url :string
|
|
|
#
|
|
|
|
|
|
class MediaAttachment < ApplicationRecord
|
|
@@ -49,13 +54,13 @@ class MediaAttachment < ApplicationRecord
|
|
|
original: {
|
|
|
pixels: 1_638_400, # 1280x1280px
|
|
|
file_geometry_parser: FastGeometryParser,
|
|
|
- },
|
|
|
+ }.freeze,
|
|
|
|
|
|
small: {
|
|
|
pixels: 160_000, # 400x400px
|
|
|
file_geometry_parser: FastGeometryParser,
|
|
|
blurhash: BLURHASH_OPTIONS,
|
|
|
- },
|
|
|
+ }.freeze,
|
|
|
}.freeze
|
|
|
|
|
|
VIDEO_FORMAT = {
|
|
@@ -74,14 +79,14 @@ class MediaAttachment < ApplicationRecord
|
|
|
'frames:v' => 60 * 60 * 3,
|
|
|
'crf' => 18,
|
|
|
'map_metadata' => '-1',
|
|
|
- },
|
|
|
- },
|
|
|
+ }.freeze,
|
|
|
+ }.freeze,
|
|
|
}.freeze
|
|
|
|
|
|
VIDEO_PASSTHROUGH_OPTIONS = {
|
|
|
- video_codecs: ['h264'],
|
|
|
- audio_codecs: ['aac', nil],
|
|
|
- colorspaces: ['yuv420p'],
|
|
|
+ video_codecs: ['h264'].freeze,
|
|
|
+ audio_codecs: ['aac', nil].freeze,
|
|
|
+ colorspaces: ['yuv420p'].freeze,
|
|
|
options: {
|
|
|
format: 'mp4',
|
|
|
convert_options: {
|
|
@@ -90,9 +95,9 @@ class MediaAttachment < ApplicationRecord
|
|
|
'map_metadata' => '-1',
|
|
|
'c:v' => 'copy',
|
|
|
'c:a' => 'copy',
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
+ }.freeze,
|
|
|
+ }.freeze,
|
|
|
+ }.freeze,
|
|
|
}.freeze
|
|
|
|
|
|
VIDEO_STYLES = {
|
|
@@ -101,15 +106,15 @@ class MediaAttachment < ApplicationRecord
|
|
|
output: {
|
|
|
'loglevel' => 'fatal',
|
|
|
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
|
|
|
- },
|
|
|
- },
|
|
|
+ }.freeze,
|
|
|
+ }.freeze,
|
|
|
format: 'png',
|
|
|
time: 0,
|
|
|
file_geometry_parser: FastGeometryParser,
|
|
|
blurhash: BLURHASH_OPTIONS,
|
|
|
- },
|
|
|
+ }.freeze,
|
|
|
|
|
|
- original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS),
|
|
|
+ original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS).freeze,
|
|
|
}.freeze
|
|
|
|
|
|
AUDIO_STYLES = {
|
|
@@ -119,16 +124,23 @@ class MediaAttachment < ApplicationRecord
|
|
|
convert_options: {
|
|
|
output: {
|
|
|
'loglevel' => 'fatal',
|
|
|
- 'map_metadata' => '-1',
|
|
|
'q:a' => 2,
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
+ }.freeze,
|
|
|
+ }.freeze,
|
|
|
+ }.freeze,
|
|
|
}.freeze
|
|
|
|
|
|
VIDEO_CONVERTED_STYLES = {
|
|
|
- small: VIDEO_STYLES[:small],
|
|
|
- original: VIDEO_FORMAT,
|
|
|
+ small: VIDEO_STYLES[:small].freeze,
|
|
|
+ original: VIDEO_FORMAT.freeze,
|
|
|
+ }.freeze
|
|
|
+
|
|
|
+ THUMBNAIL_STYLES = {
|
|
|
+ original: IMAGE_STYLES[:small].freeze,
|
|
|
+ }.freeze
|
|
|
+
|
|
|
+ GLOBAL_CONVERT_OPTIONS = {
|
|
|
+ all: '-quality 90 -strip +set modify-date +set create-date',
|
|
|
}.freeze
|
|
|
|
|
|
IMAGE_LIMIT = 10.megabytes
|
|
@@ -144,18 +156,28 @@ class MediaAttachment < ApplicationRecord
|
|
|
has_attached_file :file,
|
|
|
styles: ->(f) { file_styles f },
|
|
|
processors: ->(f) { file_processors f },
|
|
|
- convert_options: { all: '-quality 90 -strip +set modify-date +set create-date' }
|
|
|
+ convert_options: GLOBAL_CONVERT_OPTIONS
|
|
|
|
|
|
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
|
|
|
validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :larger_media_format?
|
|
|
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :larger_media_format?
|
|
|
- remotable_attachment :file, VIDEO_LIMIT, suppress_errors: false
|
|
|
+ remotable_attachment :file, VIDEO_LIMIT, suppress_errors: false, download_on_assign: false, attribute_name: :remote_url
|
|
|
+
|
|
|
+ has_attached_file :thumbnail,
|
|
|
+ styles: THUMBNAIL_STYLES,
|
|
|
+ processors: [:lazy_thumbnail, :blurhash_transcoder],
|
|
|
+ convert_options: GLOBAL_CONVERT_OPTIONS
|
|
|
+
|
|
|
+ validates_attachment_content_type :thumbnail, content_type: IMAGE_MIME_TYPES
|
|
|
+ validates_attachment_size :thumbnail, less_than: IMAGE_LIMIT
|
|
|
+ remotable_attachment :thumbnail, IMAGE_LIMIT, suppress_errors: true, download_on_assign: false
|
|
|
|
|
|
include Attachmentable
|
|
|
|
|
|
validates :account, presence: true
|
|
|
validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, if: :local?
|
|
|
validates :file, presence: true, if: :local?
|
|
|
+ validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? }
|
|
|
|
|
|
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
|
|
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
|
@@ -215,16 +237,21 @@ class MediaAttachment < ApplicationRecord
|
|
|
@delay_processing
|
|
|
end
|
|
|
|
|
|
+ def delay_processing_for_attachment?(attachment_name)
|
|
|
+ @delay_processing && attachment_name == :file
|
|
|
+ end
|
|
|
+
|
|
|
after_commit :enqueue_processing, on: :create
|
|
|
after_commit :reset_parent_cache, on: :update
|
|
|
|
|
|
before_create :prepare_description, unless: :local?
|
|
|
before_create :set_shortcode
|
|
|
before_create :set_processing
|
|
|
- before_create :set_meta
|
|
|
|
|
|
- before_post_process :set_type_and_extension
|
|
|
- before_post_process :check_video_dimensions
|
|
|
+ after_post_process :set_meta
|
|
|
+
|
|
|
+ before_file_post_process :set_type_and_extension
|
|
|
+ before_file_post_process :check_video_dimensions
|
|
|
|
|
|
class << self
|
|
|
def supported_mime_types
|
|
@@ -237,25 +264,25 @@ class MediaAttachment < ApplicationRecord
|
|
|
|
|
|
private
|
|
|
|
|
|
- def file_styles(f)
|
|
|
- if f.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
|
|
|
+ def file_styles(attachment)
|
|
|
+ if attachment.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(attachment.instance.file_content_type)
|
|
|
VIDEO_CONVERTED_STYLES
|
|
|
- elsif IMAGE_MIME_TYPES.include?(f.instance.file_content_type)
|
|
|
+ elsif IMAGE_MIME_TYPES.include?(attachment.instance.file_content_type)
|
|
|
IMAGE_STYLES
|
|
|
- elsif VIDEO_MIME_TYPES.include?(f.instance.file_content_type)
|
|
|
+ elsif VIDEO_MIME_TYPES.include?(attachment.instance.file_content_type)
|
|
|
VIDEO_STYLES
|
|
|
else
|
|
|
AUDIO_STYLES
|
|
|
end
|
|
|
end
|
|
|
|
|
|
- def file_processors(f)
|
|
|
- if f.file_content_type == 'image/gif'
|
|
|
+ def file_processors(instance)
|
|
|
+ if instance.file_content_type == 'image/gif'
|
|
|
[:gif_transcoder, :blurhash_transcoder]
|
|
|
- elsif VIDEO_MIME_TYPES.include?(f.file_content_type)
|
|
|
+ elsif VIDEO_MIME_TYPES.include?(instance.file_content_type)
|
|
|
[:video_transcoder, :blurhash_transcoder, :type_corrector]
|
|
|
- elsif AUDIO_MIME_TYPES.include?(f.file_content_type)
|
|
|
- [:transcoder, :type_corrector]
|
|
|
+ elsif AUDIO_MIME_TYPES.include?(instance.file_content_type)
|
|
|
+ [:image_extractor, :transcoder, :type_corrector]
|
|
|
else
|
|
|
[:lazy_thumbnail, :blurhash_transcoder, :type_corrector]
|
|
|
end
|
|
@@ -298,7 +325,7 @@ class MediaAttachment < ApplicationRecord
|
|
|
def check_video_dimensions
|
|
|
return unless (video? || gifv?) && file.queued_for_write[:original].present?
|
|
|
|
|
|
- movie = FFMPEG::Movie.new(file.queued_for_write[:original].path)
|
|
|
+ movie = ffmpeg_data(file.queued_for_write[:original].path)
|
|
|
|
|
|
return unless movie.valid?
|
|
|
|
|
@@ -317,6 +344,8 @@ class MediaAttachment < ApplicationRecord
|
|
|
meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file)
|
|
|
end
|
|
|
|
|
|
+ meta[:small] = image_geometry(thumbnail.queued_for_write[:original]) if thumbnail.queued_for_write.key?(:original)
|
|
|
+
|
|
|
meta
|
|
|
end
|
|
|
|
|
@@ -334,7 +363,7 @@ class MediaAttachment < ApplicationRecord
|
|
|
end
|
|
|
|
|
|
def video_metadata(file)
|
|
|
- movie = FFMPEG::Movie.new(file.path)
|
|
|
+ movie = ffmpeg_data(file.path)
|
|
|
|
|
|
return {} unless movie.valid?
|
|
|
|
|
@@ -347,6 +376,13 @@ class MediaAttachment < ApplicationRecord
|
|
|
}.compact
|
|
|
end
|
|
|
|
|
|
+ # We call this method about 3 different times on potentially different
|
|
|
+ # paths but ultimately the same file, so it makes sense to memoize the
|
|
|
+ # result while disregarding the path
|
|
|
+ def ffmpeg_data(path = nil)
|
|
|
+ @ffmpeg_data ||= FFMPEG::Movie.new(path)
|
|
|
+ end
|
|
|
+
|
|
|
def enqueue_processing
|
|
|
PostProcessMediaWorker.perform_async(id) if delay_processing?
|
|
|
end
|