AIツクリラボ
(更新: )・AI日本株自動売買実験・9分で読めます・🔵 実装記録

YouTube Data APIで動画を公開予約付きで無人アップロードする

この記事で分かること
YouTube Data API v3のOAuth認可・resumableアップロード・公開予約(publishAt)を組み合わせて、生成した動画を毎日無人でアップロードする仕組み
実際に使った環境
Python / google-api-python-client / google-auth-oauthlib。AI日本株自動売買実験(`trading/`サブプロジェクト)の本番実装
筆者の結論
アップロード自体より、「公開予約した動画は限定公開のまま予約できず、必ず一般公開になる」というYouTube仕様の落とし穴と、OAuthスコープを絞りすぎて動画の後からの修正・削除ができなくなった点が実装の教訓でした

AI日本株自動売買実験では、引け後に毎日1本、その日の売買結果をまとめたYouTube Shorts動画を生成しています。せっかく無人で動画まで作れるようになったので、YouTubeへのアップロードもYouTube Data API v3経由で自動化しました。

🔵 実装してみて思うこと

API連携自体はGoogleの公式クライアントライブラリを使えば数十行で書けましたが、「公開予約した動画は限定公開のまま予約できない」というYouTube側の仕様は、実装中に公式ドキュメントを読んで初めて知りました。無人で毎日投稿する以上、こうした細かい仕様を事前に理解しておかないと、想定と違う公開状態のまま気づかず動画が世に出てしまうリスクがあると実感しました。

事前準備:初回のOAuth認可だけは人間が行う

YouTube Data APIを使うには、Google Cloud ConsoleでOAuthクライアント(デスクトップアプリ種別)を作成し、初回だけブラウザでログイン・許可する認可フローを通す必要があります。この操作は当然AIが代わりに行えないため、scripts/authorize_youtube.pyという、ユーザー自身が一度だけ手元のターミナルで実行するCLIを用意しました。

flow = InstalledAppFlow.from_client_secrets_file(str(client_secrets_path), [YOUTUBE_UPLOAD_SCOPE])
print("ブラウザが開きます。Googleアカウントでログインし、アクセスを許可してください。")
credentials = flow.run_local_server(port=0)
 
token_path = Path(settings.youtube_token_path)
token_path.write_text(credentials.to_json(), encoding="utf-8")

これを一度実行すると、リフレッシュトークンがyoutube_token.jsonに保存されます。以後は無人スケジューラがこのファイルを読み込むだけでアップロードできるようになり、ブラウザ操作は不要になります。

人間がブラウザでGoogleにログイン・許可する初回OAuth認可から、リフレッシュトークンの保存、無人スケジューラによる日々の動画アップロードまでの流れを示す図
初回OAuth認可(人間・1回だけ)から無人アップロードまでの流れ

実際にtrading/ディレクトリを確認すると、初回認可が完了していることが分かります(トークンファイルの中身自体は認証情報のため掲載していません)。

$ ls -la trading/youtube_token.json trading/youtube_client_secret.json
-rw-r--r-- 1 tai32 197609 405 Sep 11 10:37 trading/youtube_client_secret.json
-rw-r--r-- 1 tai32 197609 743 Sep 11 16:43 trading/youtube_token.json

アップロード本体:resumableアップロード + 公開予約

動画本体のアップロードは、Googleの公式クライアントライブラリのMediaFileUpload(resumable=True)を使い、チャンク単位で進捗を確認しながら送信します。

def upload_video(
    video_path: Path,
    *,
    title: str,
    description: str,
    tags: list[str] | None = None,
    privacy_status: str | None = None,
    publish_at: str | None = None,
) -> str:
    creds = _load_credentials()
    youtube = build("youtube", "v3", credentials=creds)
 
    status: dict = {"selfDeclaredMadeForKids": False}
    if publish_at:
        status["privacyStatus"] = "private"
        status["publishAt"] = publish_at
    else:
        status["privacyStatus"] = privacy_status or settings.youtube_upload_privacy_status
 
    body = {
        "snippet": {"title": title, "description": description, "tags": tags or [], "categoryId": "25"},
        "status": status,
    }
    media = MediaFileUpload(str(video_path), mimetype="video/mp4", resumable=True)
    request = youtube.videos().insert(part="snippet,status", body=body, media_body=media)
 
    response = None
    while response is None:
        status, response = request.next_chunk()
 
    return response["id"]

呼び出し側では、記事の公開時刻(17:00 JST)に合わせてRFC3339形式のpublishAtを組み立てて渡しています。

publish_at = dt.datetime.combine(today, dt.time(VIDEO_PUBLISH_HOUR_JST, 0), tzinfo=JST).isoformat()
video_id = upload_video(
    video_path,
    title=f"AI日本株トレード日誌 {episode_label}{today.isoformat()}",
    description=_video_description(summary),
    tags=["AI自動売買", "日本株", "投資"],
    publish_at=publish_at,
)

つまずいたポイント1:公開予約は「限定公開のまま予約」できない

実装前に想像していたのは、「限定公開で予約しておき、指定時刻になったら限定公開のまま何も変わらない」ような使い方でした。しかし実際のYouTube仕様では、publishAtを指定した動画はprivacyStatusを強制的にprivateにする必要があり、指定時刻になると自動的に「一般公開」に切り替わります。「限定公開のまま予約」という状態はYouTube側に存在しません。

これは無視できない仕様で、無人で毎日投稿を続ける以上、予約した動画は指定時刻に必ず不特定多数に公開されることを理解した上で運用する必要があります。逆に言えば、公開予約を使わない通常アップロードの場合はyoutube_upload_privacy_statusのデフォルトをunlisted(限定公開)にしており、無人実行で動画の出来栄えを事前に検証できないリスクを考慮した設計にしています。

つまずいたポイント2:OAuthスコープを絞ると後から動画を触れなくなる

もう1つの教訓は、OAuthスコープをyoutube.upload(アップロード専用)だけに絞ったことです。これはアップロードにしか使わない前提で最小権限にしたつもりでしたが、実際に「アップロード済みの動画のタイトルを修正したい」「間違えてアップロードした動画を削除したい」という場面で、APIからvideos.update・videos.deleteを呼んだところ**403エラー(認証スコープ不足)**になりました。

スコープを広げ直せば解決しますが、そのためには一度認可し直す(リフレッシュトークンを再発行する)手間が発生します。現状は、動画の削除・修正が必要になった場合はYouTube Studioの管理画面から手動で行う運用にしています。「最小権限の原則」は正しい判断ですが、運用上どこまでの操作を後から自動化したいかを事前に洗い出してからスコープを決めるべきだった、という反省点です。

よくある質問

Q. 無人でOAuth認可までできますか? A. 初回の認可(ブラウザでGoogleアカウントにログインして許可する操作)だけは人間が一度だけ手動で行う必要があります。この操作が完了すると、以後はリフレッシュトークンが保存されたファイルを読むだけで無人アップロードができるようになります。

Q. アップロード後に動画を削除・修正したい場合はどうしていますか? A. youtube.uploadスコープだけではアップロードした動画の更新・削除ができず、実際にAPI経由で試したところ403エラー(認証スコープ不足)になりました。動画の削除・差し替えが必要な場合は、YouTube Studioの管理画面から手動で行っています。

まとめ

YouTube Data APIとの連携自体は公式クライアントライブラリのおかげで実装量は少なく済みましたが、「公開予約の仕様」と「スコープの設計」という、コードを書く前に仕様を読んでいれば避けられた2つのつまずきがありました。無人で毎日実行する仕組みほど、実装後に気づく仕様の落とし穴が運用リスクに直結しやすいと感じています。動画のナレーション生成に使っているVOICEVOXや、売買ロジックの解説記事、運用状況ページもあわせてご覧ください。

この記事を書いた人

ラボ管理人

AIツール・ノーコード・Next.jsで実際に手を動かしながら、AIツクリラボを運営しています。

運営者情報を見る →