Fix temp files generated during the upload process not being cleaned up quickly enough. This included downloaded files, generated preview images, and Ugoira video conversions. Before we relied on `Tempfile` cleaning up files automatically. But this only happened when the Tempfile object was garbage collected, which could take a long time. In the meantime we could have hundreds of megabytes of temp files hanging around. The fix is to explicitly close temp files when we're done with them. But the standard `Tempfile` class doesn't immediately delete the file when it's closed. So we also have to introduce a Danbooru::Tempfile wrapper that deletes the tempfile as soon as it's closed.
99 lines
3.4 KiB
Ruby
99 lines
3.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# A MediaFile for a Pixiv ugoira file.
|
|
#
|
|
# A Pixiv ugoira is an animation format that consists of a zip file containing
|
|
# JPEG or PNG images, one per frame, plus a JSON object containing the
|
|
# inter-frame delay timings. Each frame can have a different delay, therefore
|
|
# ugoiras can have a variable framerate. The frame data isn't stored inside the
|
|
# zip file, so it must be passed around separately.
|
|
class MediaFile::Ugoira < MediaFile
|
|
class Error < StandardError; end
|
|
attr_accessor :frame_delays
|
|
|
|
def initialize(file, frame_delays: [], **options)
|
|
super(file, **options)
|
|
@frame_delays = frame_delays
|
|
end
|
|
|
|
def close
|
|
super
|
|
@preview_frame&.close
|
|
# XXX should clean up `convert` too
|
|
end
|
|
|
|
def metadata
|
|
super.merge("Ugoira:FrameDelays" => frame_delays)
|
|
end
|
|
|
|
def dimensions
|
|
preview_frame.dimensions
|
|
end
|
|
|
|
def preview!(width, height, **options)
|
|
preview_frame.preview!(width, height, **options)
|
|
end
|
|
|
|
def duration
|
|
(frame_delays.sum / 1000.0)
|
|
end
|
|
|
|
def frame_count
|
|
frame_delays.count
|
|
end
|
|
|
|
def frame_rate
|
|
frame_count / duration
|
|
end
|
|
|
|
# Convert a ugoira to a webm.
|
|
# XXX should take width and height and resize image
|
|
def convert
|
|
raise NotImplementedError, "can't convert ugoira to webm: ffmpeg or mkvmerge not installed" unless self.class.videos_enabled?
|
|
raise RuntimeError, "can't convert ugoira to webm: no ugoira frame data was provided" unless frame_delays.present?
|
|
|
|
Danbooru::Archive.extract!(file) do |tmpdir, filenames|
|
|
output_file = Danbooru::Tempfile.new(["danbooru-ugoira-conversion-#{md5}-", ".webm"], binmode: true)
|
|
|
|
# Duplicate last frame to avoid it being displayed only for a very short amount of time.
|
|
last_file_name = File.basename(filenames.last)
|
|
last_index, file_ext = last_file_name.split(".")
|
|
new_last_filename = "#{"%06d" % (last_index.to_i + 1)}.#{file_ext}"
|
|
path_from = File.join(tmpdir, last_file_name)
|
|
path_to = File.join(tmpdir, new_last_filename)
|
|
FileUtils.cp(path_from, path_to)
|
|
|
|
delay_sum = 0
|
|
timecodes_path = File.join(tmpdir, "timecodes.tc")
|
|
File.open(timecodes_path, "w+") do |f|
|
|
f.write("# timecode format v2\n")
|
|
frame_delays.each do |delay|
|
|
f.write("#{delay_sum}\n")
|
|
delay_sum += delay
|
|
end
|
|
f.write("#{delay_sum}\n")
|
|
f.write("#{delay_sum}\n")
|
|
end
|
|
|
|
ffmpeg_out, status = Open3.capture2e("ffmpeg -i #{tmpdir}/%06d.#{file_ext} -codec:v libvpx-vp9 -crf 12 -b:v 0 -an -threads 8 -tile-columns 2 -tile-rows 1 -row-mt 1 -pass 1 -passlogfile #{tmpdir}/ffmpeg2pass -f null /dev/null")
|
|
raise Error, "ffmpeg failed: #{ffmpeg_out}" unless status.success?
|
|
|
|
ffmpeg_out, status = Open3.capture2e("ffmpeg -i #{tmpdir}/%06d.#{file_ext} -codec:v libvpx-vp9 -crf 12 -b:v 0 -an -threads 8 -tile-columns 2 -tile-rows 1 -row-mt 1 -pass 2 -passlogfile #{tmpdir}/ffmpeg2pass #{tmpdir}/tmp.webm")
|
|
raise Error, "ffmpeg failed: #{ffmpeg_out}" unless status.success?
|
|
|
|
mkvmerge_out, status = Open3.capture2e("mkvmerge -o #{output_file.path} --webm --timecodes 0:#{tmpdir}/timecodes.tc #{tmpdir}/tmp.webm")
|
|
raise Error, "mkvmerge failed: #{mkvmerge_out}" unless status.success?
|
|
|
|
MediaFile.open(output_file)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def preview_frame
|
|
@preview_frame ||= FFmpeg.new(convert).smart_video_preview
|
|
end
|
|
|
|
memoize :dimensions, :convert, :metadata
|
|
end
|