# frozen_string_literal: true require 'mini_magick' require 'open3' require 'tempfile' class MaterialThumbnailGenerator SIZE = '180x180' class << self def generate! material new(material).generate! rescue ActiveStorage::FileNotFoundError, ArgumentError, MiniMagick::Error => e Rails.logger.warn("Material thumbnail generation skipped: #{ e.class }: #{ e.message }") nil end end def initialize material @material = material end def generate! @material.thumbnail.purge if @material.thumbnail.attached? return unless @material.file.attached? return unless image? || video? @material.file.blob.open do |file| thumbnail = image? ? image_thumbnail(file.path) : video_thumbnail(file.path) attach_thumbnail(thumbnail) if thumbnail end end private def image? = content_type.start_with?('image/') def video? = content_type.start_with?('video/') def content_type = @material.file.blob.content_type.to_s 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) Rails.logger.warn("ffmpeg thumbnail failed: #{ stderr }") unless status.success? status.success? rescue Errno::ENOENT => e Rails.logger.warn("ffmpeg unavailable for material thumbnail: #{ e.message }") false end def attach_thumbnail image @material.thumbnail.attach(io: File.open(image.path), filename: 'material-thumbnail.jpg', content_type: 'image/jpeg') end end