127 行
3.3 KiB
Ruby
127 行
3.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'mini_magick'
|
|
require 'open3'
|
|
require 'tempfile'
|
|
|
|
|
|
class MaterialThumbnailGenerator
|
|
SIZE = '180x180'
|
|
|
|
class << self
|
|
def generate! material
|
|
new(material).generate!
|
|
end
|
|
end
|
|
|
|
def initialize material
|
|
@material = material
|
|
@ffmpeg_stderr = []
|
|
end
|
|
|
|
def generate!
|
|
return log_result(:no_file) unless @material.file.attached?
|
|
return log_result(:unsupported_content_type) unless image? || video?
|
|
|
|
@material.file.blob.open do |file|
|
|
thumbnail = image? ? image_thumbnail(file.path) : video_thumbnail(file.path)
|
|
return log_result(:generation_failed) unless thumbnail
|
|
|
|
return attach_thumbnail(thumbnail)
|
|
end
|
|
rescue ActiveStorage::FileNotFoundError => e
|
|
log_result(:file_not_found, error: e)
|
|
rescue MiniMagick::Error => e
|
|
log_result(:mini_magick_error, error: e)
|
|
rescue ArgumentError, StandardError => e
|
|
log_result(:generation_failed, error: e)
|
|
end
|
|
|
|
private
|
|
|
|
def image? = content_type.start_with?('image/')
|
|
|
|
def video? = content_type.start_with?('video/')
|
|
|
|
def content_type
|
|
return nil unless @material.file.attached?
|
|
|
|
@material.file.blob.content_type.to_s
|
|
end
|
|
|
|
def image_thumbnail path
|
|
image = MiniMagick::Image.open(path)
|
|
image.resize(SIZE)
|
|
image.format('jpg')
|
|
image
|
|
end
|
|
|
|
def video_thumbnail path
|
|
[1, 0].each do |seconds|
|
|
tempfile = Tempfile.new(['material-thumbnail', '.jpg'])
|
|
tempfile.close
|
|
ok = extract_video_frame(path, tempfile.path, seconds)
|
|
next unless ok && File.size?(tempfile.path)
|
|
|
|
return image_thumbnail(tempfile.path)
|
|
ensure
|
|
tempfile&.unlink
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def extract_video_frame input_path, output_path, seconds
|
|
_stdout, stderr, status =
|
|
Open3.capture3('ffmpeg',
|
|
'-y',
|
|
'-ss', seconds.to_s,
|
|
'-i', input_path,
|
|
'-frames:v', '1',
|
|
'-f', 'image2',
|
|
output_path)
|
|
@ffmpeg_stderr << stderr if stderr.present?
|
|
status.success?
|
|
rescue Errno::ENOENT => e
|
|
@ffmpeg_stderr << "ffmpeg unavailable: #{ e.message }"
|
|
false
|
|
end
|
|
|
|
def attach_thumbnail image
|
|
blob = nil
|
|
File.open(image.path) do |io|
|
|
blob = ActiveStorage::Blob.create_and_upload!(
|
|
io:,
|
|
filename: 'material-thumbnail.jpg',
|
|
content_type: 'image/jpeg')
|
|
end
|
|
@material.thumbnail.attach(blob)
|
|
log_result(:attached)
|
|
rescue StandardError => e
|
|
blob&.purge_later
|
|
log_result(:attach_failed, error: e)
|
|
end
|
|
|
|
def log_result result, error: nil
|
|
log_payload = { material_id: @material.id,
|
|
file_blob_id: file_blob_id,
|
|
content_type:,
|
|
result:,
|
|
error_class: error&.class&.name,
|
|
error_message: error&.message,
|
|
ffmpeg_stderr: @ffmpeg_stderr.join("\n").presence }
|
|
if [:attached, :no_file, :unsupported_content_type].include?(result)
|
|
Rails.logger.info("Material thumbnail generation: #{ log_payload.to_json }")
|
|
else
|
|
Rails.logger.warn("Material thumbnail generation: #{ log_payload.to_json }")
|
|
end
|
|
result
|
|
end
|
|
|
|
def file_blob_id
|
|
return nil unless @material.file.attached?
|
|
|
|
@material.file.blob.id
|
|
end
|
|
end
|