アプリの紹介動画を3本作りました。手で上げれば10分の作業です。それを自動化しようとして、半日溶かしました。
結論だけ先に書きます。ブラウザ自動操作でのYouTube投稿は成立しません。API一択です。 理由と、APIに移ってからも踏んだ罠を全部書きます。
Googleは自動化ブラウザからのログインを必ず弾く
最初の方針は、Playwrightで新しいプロファイルを作り、YouTube Studioにログインして投稿する、でした。
ログイン画面でこれが出ます。
このブラウザまたはアプリは安全でない可能性があります
回避策を探す種類の壁ではありません。自動化されたブラウザからの新規ログインは通らないという前提で設計を組み直すのが正解でした。X(Twitter)の自動投稿を作ったときも同じ結論に到達しており、そちらは既にログイン済みの実プロファイルを再利用することで回避しています。新規ログインをスクリプトにやらせようとした時点で負けです。
そこで「既存のログイン済みプロファイルを使えばいいのでは」と考えました。ここで2つ目の罠を踏みます。
「権限がありません」をURL判定で成功と誤検知した
手元にはログイン済みのChromeプロファイルがありました。ただしそれは別チャンネルの運用に使っていたもので、YK Studioチャンネルの権限を持っていません。
スクリプトはこう判定していました。
await page.goto(`https://studio.youtube.com/channel/${CHANNEL_ID}`);
const ok = page.url().includes(CHANNEL_ID); // ← これが常にtrue
権限がないアカウントでこのURLを開くと、URLはそのままで、ページの中身だけが「このページを表示する権限がありません」に差し替わります。 リダイレクトされないので、URL判定は素通りします。
これで一度「アクセスできた」と誤って報告しました。判定は必ずページの内容で行う。URLは、失敗時に変わる保証がないので使えません。
(余談ですが、この「URLで成否を判定して誤検知」は、Xの自動投稿でもまったく同じ形で踏んでいます。そちらの記事に詳しく書きました。2回やっているので、もう学習しました。)
API方式に切り替える
YouTube Data API v3を使う実装に書き直しました。全体はこうです。
npx tsx src/yt_api_upload.ts --auth # 初回だけ。同意してトークンを保存
npx tsx src/yt_api_upload.ts # 以後は無人で実行できる
アップロード対象はマニフェストで管理します。
[
{ "file": "promo_narabu.mp4", "title": "...", "description": "...", "visibility": "public" }
]
完了したぶんは別ファイルに記録して、二重投稿を防ぎます。
誤チャンネル投稿を止めたガード
これは入れておいて本当に良かった処理です。アップロードの前に、いま認証されているアカウントのチャンネルIDを取りにいきます。
const me = await youtube.channels.list({ part: ['id'], mine: true });
if (me.data.items?.[0]?.id !== EXPECTED_CHANNEL_ID) {
throw new Error(`想定外のチャンネル: ${me.data.items?.[0]?.id}`);
}
実際にこれが発火して、誤ったチャンネルへの投稿を止めました。
原因は認証したアカウントです。チャンネルの持ち主は運用用のアカウントなのに、普段使いのアカウントで同意していました。その状態で mine: true を叩くと、個人名の別チャンネルが返ってきます。 そのまま投稿していたら、身内の個人チャンネルにアプリの宣伝動画が公開されていました。
「どのアカウントで認証したか」は、実行前に必ず機械で確かめる価値があります。 人間の記憶は当てになりません。
OAuthが「テスト中」だと403、しかも7日で失効
同意画面が「テスト中」のままだと、テストユーザーに登録していないアカウントは弾かれます。
Error 403: access_denied
新しいコンソールUIでは、公開設定は「Google Auth Platform → 対象」にあります。ここで「アプリを公開」を押せば解決するのですが、アプリ名・サポートメール・ホームページURL・プライバシーポリシーURLが未入力だとボタンがグレーアウトしています。
急ぐならテストユーザーに自分を追加するのが速い。ただし、テスト中のリフレッシュトークンは7日で失効します。 1週間後に無人実行が黙って止まります。恒久運用するつもりなら、最初からブランディングの4項目を埋めて本番公開にしておくべきでした。
クォータ
1本のアップロードで1600ユニット、1日の上限が10,000。1日6本までです。
まとめて上げる運用をするなら先に計算しておくべき数字で、これを知らずに10本並べると4本目以降が静かに失敗します。
縦動画で踏んだ、しょうもないバグ
横長のプロモ3本とは別に、Shorts用の1080x1920を3本作りました。横版の単純な切り抜きではなく、縦向けにレイアウトし直したものです。
そのとき、角丸マスクと影を生成するシェル関数を横版から流用したのですが、出力ファイル名が固定でした。
roundrect 1920 1080 out.png # 1回目(横)
roundrect 1080 1920 out.png # 2回目(縦)が1回目を上書き
2回目が1回目を潰し、後段の alphamerge がサイズ不一致で落ちます。エラーメッセージは合成処理のサイズ違反を指すので、原因が関数の出力先にあることに気づくまで少し遠回りしました。
呼び出しごとにサフィックスを付けて終わりです。中間ファイル名を固定にする関数は、2回呼ばれた瞬間に壊れます。
まとめ
- ブラウザ自動操作でGoogleにログインするのは不可能。既存プロファイル再利用かAPIのどちらか
- 成否の判定にURLを使わない。ページ内容かAPIの戻り値で見る
- 投稿・送信の前に**「どのアカウントか」を機械で確認する**。これだけで最悪の事故が止まる
- OAuthがテスト中ならトークンは7日で切れる。無人運用には向かない
自動化は、失敗したときに誰も見ていないのが本質的なリスクです。だから投稿の直前に、取り返しのつかない条件だけを機械で確かめる——今回いちばん役に立ったのは、凝ったリトライでもエラーハンドリングでもなく、この4行のガードでした。