「YouTube の動画をローカルに保存したい」という要件は、調べると答えがすぐ出てくる。yt-dlp を入れて URL を渡す。それで終わりのはずだった。
しかし実際に 9 本の動画(合計約 3 時間半、6.5GB)を保存し、
さらにチャプターごとに 22 パートへ分割するところまでやってみると、
「落ちてはくるが実用にならない」状態を何度も経由した。
- 落としたファイルが QuickTime で再生できない
- 単体のつもりで貼った URL が、実はプレイリスト全体を指している
- 取り直したいのに何も起きない
この記事では、最短手順から始めて、実運用に必要な設定を 1 つずつ足していく。
前提として 対象にできるのは、自分がアップロードした動画、CC ライセンスの動画、
権利者から許諾を得た動画に限られる。YouTube の利用規約は公式機能以外でのダウンロードを禁じており、
日本の著作権法上も、違法にアップロードされたと知りながらのダウンロードは私的利用でも違法になる。
今回扱ったのは権利関係を確認済みの動画のみ。
1. 最短手順
brew install yt-dlp ffmpeg
yt-dlp "https://www.youtube.com/watch?v=XXXXXXXX"
これで動画は落ちてくる。環境は以下。
- yt-dlp 2026.08.19
- ffmpeg 7.1.1
- macOS (Apple Silicon)
ffmpeg は必須である。YouTube は高画質だと映像と音声を別ストリームで配信する(DASH)ため、
結合に ffmpeg が要る。入れずに実行すると、勝手に低画質の結合済みファイルが選ばれる。
ここから、実用に必要なものを足していく。
2. 落としたファイルが再生できない — 最初の関門
最初のダウンロードは成功した。1080p、34 分、1.0GB の mp4。しかし QuickTime Player で開けない。
ffprobe で中身を見るとこうなっていた。
$ ffprobe -v error -show_entries stream=codec_type,codec_name \
-of default=noprint_wrappers=1 video.mp4
codec_name=vp9
codec_type=video
codec_name=opus
codec_type=audio
VP9 + Opus である。ここが最初の関門で、要点は次の一行に尽きる。
コンテナ(mp4 / webm / mkv)と、中身のコーデック(H.264 / VP9 / AV1、AAC / Opus)は独立している。
--merge-output-format mp4 は「入れ物を mp4 にする」だけの指定で、再エンコードはしない。
ffmpeg は VP9 や Opus を mp4 コンテナに詰めることができてしまう。
ファイルとしては正しいが、QuickTime(VideoToolbox 経由の再生系)はこの組み合わせをデコードできない。
VLC や IINA なら再生できるので「壊れている」わけではない。純粋に互換性の問題である。
対処:フォーマット選択でコーデックを指定する
yt-dlp のフォーマットセレクタは、コーデックでの絞り込みができる。
FMT="bv*[vcodec^=avc1][height<=1080]+ba[acodec^=mp4a]" # H.264 + AAC を最優先
FMT="$FMT/b[vcodec^=avc1][height<=1080][acodec^=mp4a]" # 結合済みで条件を満たすもの
FMT="$FMT/bv*[vcodec^=avc1][height<=1080]+ba" # 映像だけでも H.264 を確保
FMT="$FMT/bv*[height<=1080]+ba/b[height<=1080]/bv*+ba/b" # 最終フォールバック
yt-dlp -f "$FMT" --merge-output-format mp4 "URL"
bv* = 映像のみ、ba = 音声のみ、b = 結合済み。/ 区切りは上から順に試すフォールバックで、^= は前方一致。avc1 が H.264、mp4a が AAC を指す。
どのフォーマットが提供されているかは -F で確認できる。
$ yt-dlp -F "URL" | grep -E 'avc1|mp4a'
139 m4a audio only | 11.90MiB 49k | mp4a.40.5 49k
140 m4a audio only | 31.59MiB 129k | mp4a.40.2 129k
137 mp4 1920x1080 30 | 1.07GiB 4498k | avc1.640028 4498k video only
この指定で取り直すと Downloading 1 format(s): 137+140 となり、h264 + aac の mp4 が得られた。
どちらを選ぶか
YouTube の H.264 は基本的に 1080p までで、同じ画質なら VP9/AV1 より容量を食う(1.0GB → 1.1GB)。
| 優先するもの | 選ぶコーデック | 備考 |
|---|---|---|
| 汎用性(QuickTime、編集ソフト、古い機器) | H.264 + AAC | 1080p まで |
| 画質・容量効率、1440p 以上 | VP9 / AV1 | 再生は IINA や VLC |
用途によって両方要るので、既定を H.264 にしつつ、フラグ 1 つでコーデック不問に切り替えられるようにした。
3. URL の &list= が指しているのは 1 本ではない
作業の順序としては、まず動画 ID 単体で 1 本落として設定を詰め、
そのうえで残りをまとめて取得する、という流れで進めた。
このとき効いてくるのが URL の形である。
ブラウザからコピーした URL は、たいていこうなっている。
https://www.youtube.com/watch?v=XXXXXXXX&list=PLXXXXXXXX
yt-dlp はこれを渡されるとプレイリスト全体をダウンロードする。
つまり「まず 1 本だけ試したい」つもりで貼り付けた URL が、
そのまま数 GB の一括取得になりうる。URL を見た時点で気づけないと事故になる。
対処は、既定を --no-playlist にして、一括取得のときだけ明示的にフラグを立てることである。
yt-dlp --no-playlist "URL" # 既定:URL に list= が付いていても 1 本だけ
ytdl -p "URL" # 明示したときだけプレイリスト全体
--no-playlist は「URL が動画とプレイリストの両方を指している場合に、動画だけを取る」という指定なので、playlist?list=... のような純粋なプレイリスト URL には影響しない。
つまり既定を --no-playlist にしても、一括取得の機能は失われない。実行ログにも意図が出る。
[youtube:tab] Downloading just the video XXXXXXXX because of --no-playlist
今回も、検証は単体 ID で行い、9 本の一括取得は -p を付けて 1 回の実行で済ませた。
どちらのつもりで実行したのかがコマンドに残るのが、この設計の利点である。
4. 大量に取るなら --download-archive
プレイリストから 9 本を一括取得するとき、必須になるのがこのオプションである。
取得済みの動画 ID がテキストファイルに追記される。
youtube VmpVoIB7Kwc
youtube rHu9zIlgrVc
...
再実行すると、記録済みのものはスキップされる。
[download] XXXXXXXX: ... has already been recorded in the archive
[download] Downloading item 2 of 9
これにより、
- 途中で中断しても同じコマンドで続きから再開できる(大量取得では事実上必須)
- 後から追加された動画だけを取りに行ける
ただし「取り直し」が効かなくなる
アーカイブは動画 ID 単位である。動画として取得した後に「音声だけ mp3 で欲しい」と実行しても、
同じ ID なのでスキップされ、何も起きない。
コーデックを変えて取り直したいときも同様である。該当行を消す必要がある。
grep -v "動画ID" .downloaded.txt > tmp && mv tmp .downloaded.txt
今回、VP9 で落としたものを H.264 で取り直す際にこれを使った。
「実行しても何も起きない」ときは、まずアーカイブを疑うとよい。
5. ファイル名とメタデータを整える
大量に落とすと、ファイル名の設計が効いてくる。最終的にこうなった。
yt-dlp \
--paths "$OUTDIR" \
--output "%(upload_date>%Y%m%d,release_date>%Y%m%d|NA)s_%(title).80B_%(id)s.%(ext)s" \
--restrict-filenames \
--no-mtime \
--embed-metadata --embed-thumbnail \
--download-archive "$ARCHIVE" \
--concurrent-fragments 4 \
--retries 10 \
"URL"
意図は以下のとおり。
| 指定 | 理由 |
|---|---|
| 先頭に投稿日 | Finder で時系列に並ぶ。同じ日の複数試合もまとまる |
| 末尾に動画 ID | タイトルが同じでも衝突しない。アーカイブとの突き合わせにも使える |
%(title).80B | タイトルを 80 バイトで切り詰め。ファイル名長の上限対策 |
--restrict-filenames | ASCII 化。シェルスクリプトで扱う前提なら安全側に倒す |
--no-mtime | 更新日時を投稿日でなく取得日にする |
--concurrent-fragments 4 | DASH の断片を並列取得。実測で目に見えて速い |
upload_date は動画によっては取れないため、release_date へのフォールバックと NA を指定している。
なお、ここで付けた --embed-thumbnail が後の分割工程で効いてくる(後述)。
6. スクリプトにまとめる際のつまずき
ここまでの指定をラッパースクリプトにまとめた。その過程で踏んだのが getopts の仕様である。
while getopts "q:o:c:aslh" opt; do
case "$opt" in
c) COOKIES="$OPTARG" ;;
ヘルプにはこう書いてあった。
./ytdl.sh -c "URL" Chrome の Cookie を使う
これは動かない。c: は引数必須なので、-c "URL" と書くと URL が Cookie 名として食われ、
肝心の URL が消える。正しくは -c chrome "URL" である。
さらにその下にデッドコードが残っていた。
# -c を値なしで使ったとき用のデフォルト
if [ "$COOKIES" = "-" ]; then COOKIES="chrome"; fi
getopts は引数が欠けたオプションを - として返さない(optstring が : で始まらない限り ? を返す)。
この行は一度も実行されない。「引数なしでも動くようにしたい」という意図の残骸で、
ヘルプの記述もその名残だったのだろう。
なお Cookie が必要になるのは、限定公開や年齢制限の動画、
そして Sign in to confirm you're not a bot が出たときである。
yt-dlp --cookies-from-browser chrome "URL"
ブラウザを完全に終了してから実行しないと、DB がロックされていて読めない。
7. チャプターごとに分割する
各動画にはクォーターごとのチャプター情報がある。開始秒に変換して ffmpeg で切り出す。
ffmpeg -nostdin -v error -y \
-ss "$start" -i "$src" -t "$duration" \
-map 0:v:0 -map 0:a:0 -c copy \
-movflags +faststart -avoid_negative_ts make_zero \
"$out"
ポイントは 3 つ。
-c copy で再エンコードしない。 パケットを右から左に流すだけなので、画質劣化がなく速い。
22 ファイルの切り出しが数分で終わった。再エンコードなら数時間かかる。
-ss を -i の前に置く。 入力側のシークになり、頭から読み飛ばさず直接その位置に飛べる。
30 分の動画の後半を切り出すときに効く。
-map 0:v:0 -map 0:a:0 で必要なストリームだけ拾う。
前述の --embed-thumbnail を使っていると、mp4 の中にサムネイル(png)が
もう 1 本の映像ストリームとして入っている。-map を省くとこれも一緒にコピーされ、
プレイヤーによっては先頭の静止画を本編と誤認する。
指定した秒数ちょうどには切れない
-c copy は再エンコードしないため、任意の位置では切れない。
映像は前のフレームとの差分で構成されており、単独でデコードできるのは
キーフレーム(I フレーム)だけだからである。
対象動画のキーフレーム位置を調べてみる。
$ ffprobe -v error -select_streams v:0 -skip_frame nokey \
-show_entries frame=pts_time -of csv=p=0 -read_intervals '%+60' video.mp4
0.000000
6.000000
12.000000
18.000000
6 秒間隔。指定時刻の直前のキーフレームまで戻って切り出しが始まる。
実際、分割後のファイル長の合計は元動画より 0.4〜11.2 秒長くなった(1 カットあたり平均約 2 秒)。
重要なのはずれの方向である。開始位置は必ず手前に寄るため、
前のパートの末尾が数秒混ざるだけで、映像が欠落することはない。用途によっては実害がない。
秒単位でぴったり切りたいなら再エンコードするしかない。
ffmpeg -ss "$start" -i in.mp4 -t "$dur" -c:v libx264 -crf 20 -c:a aac out.mp4
精度と引き換えに、画質の劣化と数時間の処理時間を受け入れることになる。
検証:長さの合計で照合する
分割結果は目視で確認しづらい。長さで機械的に照合した。
for f in *.mp4; do
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f")
printf '%-60s %7.1fs\n' "$f" "$d"
done
各動画について「分割後の長さの合計 − 元動画の長さ」を見れば、
差分がキーフレーム由来の数秒に収まっているか、指定ミスで区間が抜けているかを判別できる。
今回はすべて +0.4〜11.2 秒の範囲に収まり、取りこぼしがないことを確認できた。
まとめ
「ダウンロードする」だけなら 1 行で済む。実用にするために必要だったのは以下である。
| 症状 | 原因 | 対処 |
|---|---|---|
| mp4 が QuickTime で再生できない | コンテナと中身のコーデックは別物。VP9 + Opus だった | -f で avc1 + mp4a を優先指定 |
| 単体のつもりの URL でプレイリスト全体が落ちる | watch?v=...&list=... はプレイリスト扱い | --no-playlist を既定にし、一括取得は明示フラグ |
| 実行しても何も起きない | --download-archive に ID が記録済み | 該当行を消してから再実行 |
-c "URL" が動かない | getopts の引数必須オプションとヘルプの不一致 | -c chrome "URL" と書く |
| 分割位置が数秒ずれる | -c copy はキーフレームでしか切れない | ずれは手前方向で欠落なし。精度が要るなら再エンコード |
いずれも「ツールが悪い」のではなく、コンテナとコーデックの関係、URL が指す対象、
コーデックの構造といった前提を知らないと踏む類のものだった。
なお、この手のツールは YouTube 側の仕様変更で頻繁に壊れる。動かなくなったらまず更新する。
brew upgrade yt-dlp
Analyzegear

