【Daily hack】未処理の動画ファイルを検出してひとつのライブラリに収める
Live With Linux::dailyhack
- TOP
- Articles
- Live With Linux
- 【Daily hack】未処理の動画ファイルを検出してひとつのライブラリに収める
序
目の前の問題を解決する小さなプログラムを書いたり、Linuxテクニックを駆使したりといったことは非常に日常的に行っていることなのだが、すぐ忘れてしまうため、どうしても例示しようとしても出てこないということが多い。
そこで、できるだけそのようなトライを形に残すことで参考になるようにしたいと思う。
今回は、カメラによるメディア(写真・動画)の全変換の最終段階におけるものである。
動画として撮られたもの(例えばビデオカメラで撮影したもの)はvideo
ディレクトリの下にあるが、普通にカメラ(例えばスマートフォン)で撮影されたものはimage
ディレクトリの下にある。
今回の作業は
- JPEGの写真をAVIFに変換/サムネイルを生成
- 混在している動画ファイルを分離してVP9/OpusのWebMに変換
という形で圧縮/再編するというものである。 前者に関しては入念に行ったのだが、後者については方針が定まらない状態で着手した上に途中でミスがあったため、変換できていないファイルがないかの確認をすると共に、抜けがあれば変換を行うという対応を行った。
方針を考える
もともとのソースディレクトリ、一度退避したソースディレクトリ、ソースディレクトリの元になったもともとのアーカイブ、最近アップデートしたアーカイブの4つに分かれている。これらをソースとして完全な写真フォルダと動画フォルダを完成させようとしている。
ソースディレクトリがアーカイブと異なるものであるのは、新しい写真フォルダを構成するために、バラバラのフォルダ名や階層の構造を統一するために、一旦コピーして調整したためだ。
これが問題を複雑にもしていて、ファイルパスを基準にすると、同じファイルが異なるフォルダ構成で存在するために重複したファイルが変換されてしまう。 基本的にカメラはデータを取り出しても同じファイル名でファイルを生成することはない(アクションカメラなどは別だが)ため、ファイル名だけを考えることにする。
例えばオリジナルアーカイブが
moto/mt09/video0001.MOV
ソースディレクトリが
20150525-mt09/video0001.MOV
変換後ファイルが
20150525-mt09/videeo0001.webm
となるが、ふたつのソースファイルが同じもので、変換済みであると判定できなければならない。 これは、ファイル名のみを拡張子も除いて抽出することで
video0001
とすれば判定可能だ。
フォルダ名を抽出する必要がある関係で、アーカイブよりもソースディレクトリが優先されなければならない。
変換については並列で行いたいため、工夫が必要だ。 その目的で作られたmmffrとmmfft9があるが、今回はうまくフィットしない。 変換漏れなどが発生しないようにかなり高度なキューシステムが必要となるが、並列度がそこまで高くないことを考えてファイルに情報を書き、flockを使う方針とした。
最初はうまくコマンドやワンライナーでがんばろうとしていたが、Rubyで書いてしまったほうが楽だと判断してワンタイムなプログラムを書いた。
キューファイルを作る
(ソース集合) - (変換済みファイル群)
によって変換されたファイルに見当たらないファイルを列挙し、そのまま処理に渡すことができるキューJSONを生成して出力する。
#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-
require 'find'
require 'oj'
= {}
converted = {}
sources
Find.find("Play") do |i|
next unless i =~ /\.webm$/
[File.basename i, ".*"] = true
convertedend
ARGV.each do |base|
Find.find(base) do |i|
= File.basename i, ".*"
filename next if sources[filename]
next if filename[0] == "."
next unless i =~ /\.(?:webm|mov|mkv|mp4|avi|3gp|3gg|3g2|3gp2)$/i
[filename] = i
sourcesend
end
= converted.keys
converted_list = sources.keys
sources_list
= sources_list - converted_list
diff_list
= {
obj "queue" => diff_list.map {|i| sources[i] },
"processing" => {},
"failed" => [],
"finished" => [],
"skipped" => {}
}
STDOUT.puts Oj.dump obj
File.find
によってそれぞれのディレクトリをさらい、拡張子で対象のファイルを抽出して全リストをつくる。
変換対象になった場合はソースファイルのフルパスが必要であることに留意する。
既に拾ったことがあるファイル名をスキップすることで、ARGV
に優先するものを先に書けるようにした。
このスクリプトは最終的にJSONを出力するだけである。 変換プログラムのほうがこのJSONを利用する。そのため、JSONは単なるファイルのリストではなく、キューデータの形式として出力する。
Ruby標準のjson
ライブラリもあるが、私の手元にはより高速なoj
gemがインストールされているため、Oj
を使っている。
変換を行う
#!/bun/ruby
require 'oj'
def update
File.open("videofiles.json", "r+") do |f|
.flock(File::LOCK_EX)
fbegin
= Oj.load(f)
object yield(object)
.seek(0)
f.truncate(0)
f.write Oj.dump(object)
fensure
.flock(File::LOCK_EX)
fend
end
end
loop do
= nil
source_file
do |db|
update = db["queue"].shift
source_file break unless source_file
["processing"][source_file] = $$
dbend
break unless source_file
= File.basename(File.dirname(source_file))
dir = File.basename(source_file, ".*")
file
unless File.exist? ["Play", dir].join("/")
Dir.mkdir ["Play", dir].join("/")
end
if source_file[-5, 5] == ".webm"
do |db|
update ["processing"].delete source_file
db[source_file] = ["Play", dir, (file + ".webm")].join("/")
dbend
end
begin
system("ffmpeg", "-i", source_file, "-c:v", "libvpx-vp9", "-crf", "35", "-b:v", "20000k", "-c:a", "libopus", "-b:a", "192k", ["Play", dir, (file + ".webm")].join("/"))
unless $?.success?
raise
end
rescue
do |db|
update ["processing"].delete source_file
db["failed"].push source_file
dbend
else
do |db|
update ["processing"].delete source_file
db["finished"].push source_file
dbend
end
end
JSONファイルを共有キューとして使うため、
- JSONファイルをオープン
- flock
- ファイルをリード
- JSONをparse
- オブジェクトを更新
- ファイルをクリア
- JSONを書き込み
- flock解除
という処理が高い頻度で入る。
Ruby標準のMarshal
を使う場合はPStore
が、YAMLを使う場合はYAML::Store
があるが、JSONにはないため自分で書く必要がある。
素直にYAMLを使ったほうが楽だったという気もするが、大した手間ではない。
自前でupdate
メソッドを用意する。
「オブジェクトを更新」の手順が動的だが、Rubyの場合は「ブロックを渡す」ということが可能なので、yield
で簡単に実現可能。
ソース側がWebMであった場合は「変換済みファイルがアップされている」状態なので、ハードリンクするのが正しい。 この手順では処理できないため、ハードリンクすべき対応関係を残すようにする。
エラー発生時はエラーが発生したファイルを残すようにして、手動リカバリーを可能にする。
mmfft9ほど強力な並列処理が可能なわけではないが、結構実用的だ。
スナップショット
容量は圧縮したいが、ドライブ容量が現時点で逼迫しているわけではない。 かなり強く破壊的な変更であるため、Btrfsでスナップショットを取る。 普段はタイムスタンプでスナップショットを取っているだけだが、今回はわかるようにする。
btrfs subvolume snapshot -r main 20230321-230000-USER_MEDIA_ORIGINAL_FINAL
実際にディスク容量が空くのは、ここまでのスナップショットをすべて削除したときである。