yt-dlpでチャンネル動画をプレイリストに再編する
Live With Linux::script
- TOP
- Articles
- Live With Linux
- yt-dlpでチャンネル動画をプレイリストに再編する
序
本記事はグレーな要素を取り扱う。
yt-dlpを用いてチャンネルの動画をすべてアーカイブすることを前提とし、しかしそのチャンネルの動画の特定プレイリストのみからなるデータも欲しいとする。
場合によっては別途プレイリストをダウンロードするのも悪くないが、動画のサイズは大きいし、ダウンロード面でもストレージ面でも厳しい。そこで、既にあるアーカイブからプレイリストを構成したい。
yt-dlpにはそのような機能はないため、yt-dlpが持っている連携機能を活かして、力技で実現していく。
アーカイブ
まずはチャンネル全動画アーカイブはこんな感じ。
#!/bin/zsh
config_file="$1"
(( $# > 0 )) && shift
########### BASE CONFIG ###########
basic_options=(--write-info-json -i --download-archive downloaded.txt)
downloader_options=(--external-downloader aria2c --downloader aria2c --extractor-args --external-downloader-args aria2c:"-j 12 -s 12 -x 12 -k 5M")
output_options=(-o '%(upload_date)s-%(title).64s')
extra_options=()
if [[ -z "$config_file" ]]
then
config_file="./.dlconfig.zsh"
fi
if [[ -d "$config_file" ]]
then
config_file="$config_file/.dlconfig.zsh"
fi
source $config_file
yt-dlp ${basic_options} ${downloader_options} ${output_options} ${extra_options} "$TARGET"
--write-info-json
をつけているけど、これはこのプログラムの製作中に追加したもので、プログラムはこれがないことを想定して作っている。
明らかに--write-info-json
したほうが良い。
プレイリストを編成する
プレイリストをダウンロードし、しかしプレイリストの動画のダウンロードはせず、プレイリストの動画のメタデータ一覧を獲得する。
それに使えるのが--dump-json
オプションだ。
--dump-json
オプションはデフォルトで動画をダウンロードせず、1行ごとに動画のJSONメタデータを出力する。
--download-archive
を使うことで未ダウンロードのものを獲得する。
#!/bin/zsh
for i in */target
do
(
print "============================>>>> "${i:h}
cd ${i:h}
yt-dlp --dump-json -i --download-archive downloaded.txt --external-downloader aria2c --downloader aria2c --extractor-args --external-downloader-args aria2c:"-j 12 -s 12 -x 12 -k 5M" -i -o '%(upload_date)s-%(title).64s' $(<target) | ruby ../pudlmeta.rb
)
done
rudlmeta.rb
は出力されたメタデータを再編する。
#!/bin/ruby
require 'json'
= []
transfar
ARGF.each do |line|
= JSON.load(line)
meta .push({
transfar"id" => meta["id"],
"filename" => meta["filename"],
"ext" => meta["ext"]
})
end
unless transfar.empty?
File.open("transfar.json", "w") do |f|
JSON.dump(transfar, f)
end
end
これでプレイリストの中で動画がないものを列挙したtransfar.json
が準備できる。
アーカイブから動画を取得する
メタデータから百発百中で動画を獲得できれば良いのだが、実際はそうはいかない。 同じオプションを与えたとしても、動画ファイルが異なるものになったりするからだ。
理由はよくわからないが、upload_date
が違う日になったりするし、タイトルを使っていると概要が編集されてしまったりもする。
IDを使えば確実だが、そのためには前提としてIDで構成するようにしていないといけない。
そのため、ファイル名のsimilarityに基づいて判定する。 RubyのGem
string-similarity
を使う。
想定されるファイル名と同一のファイル名が存在するのであればそれで良い。
そうでない場合、ファイル名が近ければ(例えば日付だけ変わったようなことで)同一だとみなしていいだろう。
ただし、#1
のような動画ナンバリングだけが異なる動画をアップロードしているチャンネルではないことが前提として必要になる。
閾値は実際に取り扱っているチャンネルに基づいて決めている。
また、前提としてアーカイブが$日付-$タイトル.$拡張子
であることを忘れないように。
近似ファイルの検索は、まず日付が同じものに絞って行っている。 全リストだと日付が遠く離れたものに引きずられやすいのと、ファイル数が多いと時間がかかりすぎるためだ。 それで見つからなかった場合は全ファイルに対象を広げる。
存在するのであればIDベースのメタデータファイルを利用する。
そして、同一だとみなせるものはコピーしていく。
rsync
でコピーしているが、これはプレイリストをコンパイルしたものは別ファイルシステムにあるためで、通常はハードリンクにしたほうが良いだろう。
同一だとみなせるものがないのなら、missing.json
に記録する。
downloaded.txt
への記録は、youtube
にしているが、これは対象プラグイン(=対象サイト)による。
#!/bin/ruby
require 'string/similarity'
require 'json'
= ARGV.shift
superdir = {}
filelist_map = []
filelist Dir.children(superdir).each {|i|
= File.basename(i, ".*")
b [b] = i
filelist_map.push b
filelist}
= {}
missing = []
copylist
= JSON.load File.read("transfar.json")
t = File.read("#{superdir}/downloaded.txt").split("\n")
downloaded = (Dir.children("#{superdir}/.meta") rescue [])
stored_meta
.each do |i|
tif stored_meta.include("#{i["id"]}.json")
= JSON.load(File.read "#{i["id"]}.json")
meta .push({
copylistrequested: i,
found: {
file_full: meta["filename"],
}
})
else
sub = filelist.select {|f| f[0,8] == i["filename"][0,8] }
= i["filename"].gsub("/", "_").gsub("⧸", "_")
rf = sub.map {|x| {file: x, score: String::Similarity.cosine(x, rf), req: rf} }
subsim = subsim.max_by {|x| x[:score] }
best if best[:score] < 0.97
= filelist.max_by {|x| String::Similarity.cosine(x, rf) }
most if (ms = String::Similarity.cosine(most, rf)) < 0.99
[i["id"]] = {
missingrequested: i,
found: best,
found_real: filelist_map[best[:file]],
archived: downloaded.include?("youtube #{i["id"]}"),
most: most,
most_score: ms
}
else
.push({
copylistrequested: i,
found: best
})
end
else
.push({
copylistrequested: i,
found: best
})
end
end
end
File.open("downloaded.txt", "a") do |f|
.each do |i|
copylistsystem("rsync", "--ignore-existing", "-v", "#{superdir}/#{i[:found][:file_full] || filelist_map[i[:found][:file]]}", "./")
.puts("youtube #{i[:requested]["id"]}")
fend
end
unless missing.empty?
= (JSON.load(File.read("missing.json")) rescue {})
missing_data .merge! missing
missing_dataFile.open("missing.json", "w") {|f| JSON.dump(missing, f)}
end
一致するものが見つけられなかった場合
missing.json
にデータが残っているので、これを手動で拾い上げていく。
十分な近似性が見つけられなかったものの、もっとも有力だと思われるものは記録してあるので、それが同じかどうかを対話的に判定する。
同一だった場合、コピーしてdownloaded.txt
に追加すれば良い。
同一でなかった場合、当該動画がアーカイブにないのだろう。アーカイブ側のdownloaded.txt
に(動画がダウンロードできていないにも関わらず)存在しているのなら消去する。
#!/bin/ruby
require 'json'
= ARGV.shift
superdir
abort unless superdir
= JSON.load File.read "missing.json"
missing = File.read("#{superdir}/downloaded.txt").split("\n")
archived = archived.dup
archived_orig
File.open("downloaded.txt", "a") do |f|
.each do |k, v|
missingputs "R: #{v["requested"]["filename"]}"
puts "M: #{v["found"]["file"]}"
loop do
print "Is it same? "
= gets.chomp
input if input[0].downcase == "y"
system("rsync", "--ignore-existing", "-v", "#{superdir}/#{v["found_real"]}", "./")
.puts("youtube #{k}")
fbreak
elsif input[0].downcase == "n"
.delete("youtube #{k}")
archivedbreak
else
nil
end
end
end
end
if archived != archived_orig
File.open("#{superdir}/downloaded.txt", "w") {|f| f.puts archived.join("\n") }
end
これで一致しなかったものについては、一旦忘れるで良い。
アーカイブ側をまたダウンロードすれば、downloaded.txt
からは消してあるので、ダウンロードされるだろう。
IDベースのメタデータを編成する
--write-info-json
を使って作ったメタデータを、IDで引けるようにする。
単にリンクでも良いのだが、そうすると対応するファイルを引けなくなるため、それだけ追加する。
#!/bin/ruby
require 'json'
= Dir.children(".").select {|i| i[-10, 10] == ".info.json" }
json_files
.each do |jf|
json_files= JSON.load File.read jf
meta = File.basename(jf, ".info.json") + "." + (meta["ext"] || "")
fn next unless File.exist?(fn)
= { "meta" => meta, "filename" => fn }
new_meta File.open(".meta/#{meta["id"]}.json", "w") {|f| JSON.dump(new_meta, f) }
end