Skip to content

Instantly share code, notes, and snippets.

@shimarin
Created June 26, 2010 18:38
Show Gist options
  • Save shimarin/454252 to your computer and use it in GitHub Desktop.
Save shimarin/454252 to your computer and use it in GitHub Desktop.
共有フォルダに動画ファイルを放り込んでおくと勝手にiPhone/iPad用にトランスコードしといてくれるシステムを作った。動画ファイルのハッシュ値を記録しておいて、過去に処理済みの動画は後回しにして暇なときだけ処理する機能つき。きわめて自分専用。
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require "fileutils"
require "digest/md5"
require "dbi"
# HandBrakeを使って infileを outfileにトランスコードする。
# optionsはコマンドラインオプション
def encode(infile, outfile, options)
cmdline = "HandBrakeCLI -i \"#{infile}\" -o \"#{outfile}\" #{options}"
system(cmdline)
$?
end
# ファイルのmd5sumを得る
def get_md5(filename)
md5 = Digest::MD5.new
File.open(filename) {|f|
while s = f.read(1024)
md5.update(s)
end
}
md5
end
# データベースアクセス
def with_dbi
# DBサーバlocalhost(デフォルト), DB名 handbrake、ユーザ名 handbrake、パスワードなし
DBI.connect("DBI:Mysql:handbrake", "handbrake", "") {|dbh|
dbh['AutoCommit'] = false
dbh.transaction {|dbh|
yield dbh
}
}
end
# 過去に同じmd5sumを持つファイルを処理したことがあるか
def md5sum_exists?(md5, device)
with_dbi {|dbh|
num = dbh.select_one("select count(*) from completed_processes where md5sum=? and device_name=?", md5.hexdigest, device)[0]
return true if num > 0
}
return false
end
# ソースディレクトリ(の中にある対応するターゲットデバイス名のディレクトリ)を走査し、
# それぞれトランスコードにかける
def process_dir(source_dir, target_dir, device, config, skip_cached = true, max_files = nil)
# ディレクトリがない場合はただ戻る
return 0 if !FileTest.exist?("#{source_dir}/#{device}")
count = 0
Dir.open("#{source_dir}/#{device}") {|dir|
dir.each {|file|
source = dir.path + "/" + file
next if File.ftype(source) != "file"
next if file[0,1] == "."
stat = File.stat(source)
# ファイルサイズゼロはスキップ
next if stat.size == 0
# 最終更新チェック。最後の書き込みから60秒が経過していない場合はまだ使用中とみなして
# 処理をスキップする
next if Time.new - 60 < stat.mtime
# ハッシュ値を計算し、過去に同じ物を同じデバイス向けに処理したことがあるかチェック
# skip_cachedフラグがtrueでかつ過去に同じ物を処理したことがあるならスキップする
md5 = get_md5(source)
next if md5sum_exists?(md5,device) && skip_cached
# ターゲットディレクトリに対象デバイス名のサブディレクトリが無ければ作る
if !FileTest.exist?("#{target_dir}/#{device}") then
FileUtils.mkdir_p("#{target_dir}/#{device}")
end
# ターゲットファイル名の決定。処理中のファイルはサフィックスを ".bin" にすることで
# 処理中のファイルをプレイヤーで開いてしまうことを防ぐ
destination_base = target_dir + "/" + device + "/" + File.basename(file, ".*")
temp_destination = destination_base + ".bin"
destination = destination_base + "." + config[:suffix]
# トランスコード処理にかける
if encode(source, temp_destination, config[:options]) == 0 && FileTest.exist?(temp_destination) then
# トランスコードが成功したようであれば、ファイル名(サフィックス)をあるべき
# ものに変更する
FileUtils.mv(temp_destination, destination)
# トランスコード元ファイルのmd5sumをデータベースに記録する。新規ならINSERT
# 既存なら UPDATEで最終更新日時の更新を行う
with_dbi {|dbh|
row = dbh.select_one("select * from completed_processes where md5sum=? and device_name=? for update", md5.hexdigest, device)
if row != nil then
dbh.do("update completed_processes set updated_at=now() where md5sum=? and device_name=?", md5.hexdigest, device)
else
dbh.do("insert into completed_processes(md5sum,device_name,created_at) values(?,?,now())", md5.hexdigest, device)
end
}
# 処理済みのトランスコード元ファイルを done サブディレクトリに移動する
FileUtils.mkdir_p("#{source_dir}/#{device}/done")
FileUtils.mv(source, "#{source_dir}/#{device}/done")
count += 1
return count if max_files != nil && count >= max_files
else
# トランスコードが失敗した場合は、元ファイルを failed サブディレクトリに移動する
FileUtils.mkdir_p("#{source_dir}/#{device}/failed")
FileUtils.mv(source, "#{source_dir}/#{device}/failed")
end
}
}
count
end
def main()
# ユーザーがトランスコード元の動画ファイルを設置するディレクトリ
# sambaでここを共有する
source_dir = "/var/handbrake"
# トランスコード済みの動画ファイルを出力するディレクトリ
# ApacheのDocumentRootをここにする
target_dir = "/var/www/localhost/htdocs"
# 多重起動防止のためのロックファイルを開く
File.open(File.expand_path("~") + "/var/run/lock", "w") {|lock|
# ロックが既に他のプロセスに獲得されている場合はおとなしく処理を中断する
return if lock.flock(File::LOCK_EX | File::LOCK_NB) == false
# 対象デバイスのリストをデータベースから取得する
devices = {}
with_dbi {|dbh|
dbh.select_all("select device_name,program,switches,suffix from devices").each {|row|
device_name = row["device_name"]
devices[device_name] = {:program=>row["program"],:options=>row["switches"],:suffix=>row["suffix"]}
}
}
# 各デバイス用のサブディレクトリについて一括トランスコード処理を呼び出す
devices.each {|device,config|
if process_dir(source_dir, target_dir, device, config, true) == 0 then
# 処理するファイルが無くて暇なときだけ、過去にトランスコード済みのファイルも1個だけ処理する
process_dir(source_dir, target_dir, device, config, false, 1)
end
}
}
end
main()
create table sources (
md5sum varchar(32) primary key,
filesize int not null
);
create table source_filenames (
md5sum varchar(32) not null,
filename varchar not null,
primary key(md5sum, filename)
);
create table completed_processes (
md5sum varchar(32) not null,
device_name varchar(32) not null,
created_at datetime not null,
updated_at datetime null,
primary key(md5sum,device_name)
);
create table devices (
device_name varchar(32) primary key,
program varchar(32) null,
switches varchar(255) not null,
suffix varchar(16) not null
);
insert into devices(device_name,switches,suffix) values('iPad','-e x264 -q 0.6 -a 1,1 -E faac,ac3 -B 160,auto -R 48,Auto -6 dpl2,auto -f mp4 -4 -X 960 -m --loose-anamorphic -x cabac=0:ref=2:me=umh:b-adapt=2:weightb=0:trellis=0:weightp=0','m4v');
insert into devices(device_name,switches,suffix) values('iPhone','-e x264 -q 0.6 -a 1 -E faac -B 128 -6 dpl2 -R 48 -D 0.0 -f mp4 -X 480 -m -x cabac=0:ref=2:me=umh:bframes=0:subme=6:8x8dct=0:trellis=0','m4v');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment