182 lines
5.3 KiB
Ruby
182 lines
5.3 KiB
Ruby
# typed: strict
|
|
class Domain::PostFile::Thumbnail < ReduxApplicationRecord
|
|
extend T::Sig
|
|
self.table_name = "domain_post_file_thumbnails"
|
|
|
|
belongs_to :post_file,
|
|
class_name: "::Domain::PostFile",
|
|
inverse_of: :thumbnails,
|
|
touch: true
|
|
|
|
enum :thumb_type, { content_container: 0, size_32_32: 1 }
|
|
validates :thumb_type, presence: true
|
|
validates :frame, presence: true
|
|
validate :validate_content_type!
|
|
|
|
THUMB_ROOT_DIR = T.let(File.join(BlobFile::ROOT_DIR, "thumbnails"), String)
|
|
|
|
THUMB_FILE_PATH_PATTERN = T.let([2, 2, 1].freeze, T::Array[Integer])
|
|
|
|
SINGLE_FRAME_CONTENT_TYPES =
|
|
T.let(
|
|
[%r{image/jpeg}, %r{image/jpg}, %r{image/png}, %r{image/bmp}],
|
|
T::Array[Regexp],
|
|
)
|
|
|
|
MULTI_FRAME_CONTENT_TYPES =
|
|
T.let(
|
|
[
|
|
%r{image/gif},
|
|
%r{image/apng},
|
|
%r{image/webp},
|
|
%r{video/webm},
|
|
%r{video/mp4},
|
|
],
|
|
T::Array[Regexp],
|
|
)
|
|
|
|
THUMBABLE_CONTENT_TYPES =
|
|
T.let(
|
|
SINGLE_FRAME_CONTENT_TYPES + MULTI_FRAME_CONTENT_TYPES,
|
|
T::Array[Regexp],
|
|
)
|
|
|
|
THUMB_TYPE_TO_OPTIONS =
|
|
T.let(
|
|
{
|
|
content_container:
|
|
LoadedMedia::ThumbnailOptions.new(
|
|
width: 768,
|
|
height: 2048,
|
|
quality: 95,
|
|
size: :down,
|
|
interlace: true,
|
|
for_frames: [0],
|
|
),
|
|
size_32_32:
|
|
LoadedMedia::ThumbnailOptions.new(
|
|
width: 32,
|
|
height: 32,
|
|
quality: 95,
|
|
size: :force,
|
|
interlace: false,
|
|
for_frames: [0.0, 0.1, 0.5, 0.9, 1.0],
|
|
),
|
|
},
|
|
T::Hash[Symbol, LoadedMedia::ThumbnailOptions],
|
|
)
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
def absolute_file_path
|
|
return nil unless (thumb_type = self.thumb_type)
|
|
return nil unless (post_file = self.post_file)
|
|
return nil unless (sha256 = post_file.sha256)
|
|
sha256_hex = HexUtil.bin2hex(sha256)
|
|
self.class.absolute_file_path(sha256_hex, thumb_type, T.must(self.frame))
|
|
end
|
|
|
|
sig do
|
|
params(sha256_hex: String, thumb_type: String, frame: Integer).returns(
|
|
T.nilable(String),
|
|
)
|
|
end
|
|
def self.absolute_file_path(sha256_hex, thumb_type, frame)
|
|
[
|
|
THUMB_ROOT_DIR,
|
|
thumb_type.to_s,
|
|
*BlobFile.path_segments(THUMB_FILE_PATH_PATTERN, sha256_hex),
|
|
].join("/") + "-#{frame.to_i.to_s.rjust(2, "0")}.jpg"
|
|
end
|
|
|
|
sig { params(post_file: Domain::PostFile).returns(T::Boolean) }
|
|
def self.can_thumbnail_post_file?(post_file)
|
|
return false unless content_type = post_file.content_type
|
|
can_thumbnail_content_type?(content_type)
|
|
end
|
|
|
|
sig { params(content_type: String).returns(T::Boolean) }
|
|
def self.can_thumbnail_content_type?(content_type)
|
|
THUMBABLE_CONTENT_TYPES.any? { |type| content_type.match?(type) }
|
|
end
|
|
|
|
sig do
|
|
params(post_file: Domain::PostFile).returns(
|
|
T::Array[Domain::PostFile::Thumbnail],
|
|
)
|
|
end
|
|
def self.create_for_post_file!(post_file)
|
|
logger.tagged("thumbnail", make_arg_tag(post_file)) do
|
|
return [] unless content_type = post_file.content_type
|
|
return [] unless can_thumbnail_content_type?(content_type)
|
|
blob_file_path = post_file.blob&.absolute_file_path
|
|
return [] unless blob_file_path
|
|
|
|
media = LoadedMedia.from_file(content_type, blob_file_path)
|
|
return [] unless media
|
|
|
|
num_frames = media.num_frames
|
|
logger.info(format_tags(make_tag("num_frames", num_frames)))
|
|
return [] if num_frames.zero?
|
|
|
|
existing_thumb_types = post_file.thumbnails.to_a.map(&:thumb_type).uniq
|
|
logger.info(
|
|
format_tags(make_tag("existing_thumb_types", existing_thumb_types)),
|
|
)
|
|
|
|
FileUtils.mkdir_p(BlobFile::TMP_DIR)
|
|
thumbnails = []
|
|
THUMB_TYPE_TO_OPTIONS.each do |thumb_type, options|
|
|
thumb_type = thumb_type.to_s
|
|
logger.tagged(make_tag("thumb_type", thumb_type)) do
|
|
next if existing_thumb_types.include?(thumb_type)
|
|
logger.info(format_tags("creating thumbnail"))
|
|
|
|
# get the number of frames in the post file
|
|
frames_to_thumbnail =
|
|
options
|
|
.for_frames
|
|
.map { |frame_fraction| (frame_fraction * (num_frames - 1)).to_i }
|
|
.uniq
|
|
.sort
|
|
|
|
frames_to_thumbnail.each do |frame|
|
|
logger.tagged(make_tag("frame", frame)) do
|
|
thumbnail = post_file.thumbnails.build(thumb_type:, frame:)
|
|
unless thumb_file_path = thumbnail.absolute_file_path
|
|
logger.info(format_tags("unable to compute thumbnail path"))
|
|
next
|
|
end
|
|
|
|
if File.exist?(thumb_file_path)
|
|
logger.info(format_tags("thumbnail file already exists"))
|
|
else
|
|
media.write_frame_thumbnail(frame, thumb_file_path, options)
|
|
end
|
|
|
|
thumbnail.save!
|
|
thumbnails << thumbnail
|
|
end
|
|
end
|
|
end
|
|
end
|
|
thumbnails
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
sig { void }
|
|
def validate_content_type!
|
|
return unless (post_file = self.post_file)
|
|
content_type = post_file.content_type
|
|
if content_type.nil?
|
|
errors.add(:post_file, "must have a content type")
|
|
return
|
|
end
|
|
|
|
unless self.class.can_thumbnail_content_type?(content_type)
|
|
errors.add(:post_file, "must be a thumbnailable content type")
|
|
end
|
|
end
|
|
end
|