Files
redux-scraper/app/models/domain/post_file/thumbnail.rb

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