Amazonで商品を見るセール会場へ

YouTubeショートの関連動画を一括設定|Playwrightで自動化

ショートを何十本も出していると、関連動画の設定が追いつかなくなります。

この記事は、関連動画を本編へまとめて設定したい、Pythonが少し書ける人に向けて書きました。

結論から書くと、関連動画はYouTube Data API(公式のAPI)では設定できないので、YouTube Studioの画面操作をPlaywrightで自動化します。

ログインは人が行い、Playwrightはログイン済みのChromeにあとからつなぐ方式です。

運営者のMOTOKIのYouTubeチャンネルで試すと、ショート98本の確認と設定が約17分で終わり、未設定28本と設定ミス1本も見つかりました。

  • 試した環境:Windows 11、Python 3.13、Playwright 1.62(Python版)、手元のGoogle Chrome、YouTube Data API v3(読むだけ)
  • 試した対象:運営者のチャンネルのショート98本(YouTube Studioの表示言語は日本語)
  • 画面の撮影:2026年11月2日(YouTubeヘルプなど公式の記載もこの日に読んだもの)
  • 確かめていないこと:表示言語が英語のStudio、macOS、60秒を超えるショートがあるチャンネル
目次

最短の手順と早見表

サンプルのスクリプトを使って、ショートの関連動画を設定するまでの最短の流れです。

スクリプトの全文は、記事の後半に載せています。

YouTube Studioの画面は変わることがあるので、使う前に1本だけで試してください。

  1. Playwrightを入れる(コマンドは下のコードブロック)
  2. 専用のChromeを開き、そのChromeでYouTube Studioに一度ログインする
  3. どのショートにどの本編を入れるかを、対応表のCSVにまとめる
  4. 今の関連動画を読むだけの確認を流して、直す本数を先に知る
  5. 1本だけ設定して、Studioの画面で結果を確かめる
  6. 問題がなければ全部を設定する(済んだものは飛ばす)
pip install playwright
python shorts_related.py chrome
python shorts_related.py scan pairs.csv
python shorts_related.py set pairs.csv --only <ショートID>
python shorts_related.py set pairs.csv
やりたいことまず試すだめなときの次の一手
関連動画をまとめて設定する対応表を作り、画面操作を自動化するAPIの項目を探しても無いので、画面操作に切り替える
ログインしたまま操作する専用プロフィールのChromeにあとからつなぐつながらないときは専用のChromeを開き直す
本編を選び間違えないサムネイルのURLにある動画IDで選ぶ題名だけで選ばない
直す本数を先に知る読むだけの確認を流す「なし」は未設定として数える
途中で止まったとき3回まで開き直す失敗が3本続いたら止めて画面を見る
スポンサーリンク

ショートの関連動画とは?APIで設定できない理由

関連動画は、ショートから本編へ飛べるリンク

関連動画は、ショートの再生画面に出るリンクです。

チャンネル名の下に表示され、視聴者はタップして別の動画へ移れます。

ショートの説明欄にURLを書いても、視聴者はクリックできません。

本編へ案内したいなら、関連動画がいちばん確実な入り口になります(参考:YouTubeヘルプ「視聴者とリンクを共有する」)。

使える条件は、YouTubeヘルプの「YouTubeショート動画に関連動画を追加する」で次の2つが挙げられています。

  • 上級者向け機能を使えるアカウントであること
  • 選べる動画は、公開または限定公開のもの

長い動画の最後に本編や登録を案内したいときは、終了画面を使います。

終了画面に使う画像の大きさは、「YouTubeの終了画面のサムネイルのサイズ」の記事で解説しました。

YouTube Data APIには設定する項目がない

YouTube Data APIで動画の情報を扱うのは、videosリソースです(YouTube Data APIの公式リファレンス(videos))。

ところが、ここにはショートの関連動画にあたる項目がありません。

題名や説明、公開設定はAPIで書き換えられます。

関連動画だけは、YouTube Studioの画面で1本ずつ選ぶしかないのです。

YouTube Studioのショートの「動画の詳細」画面。右下の「関連動画」の欄に、本編の題名「WordPressなのに自社で更新でき…」が入っている
2026-11-02 撮影。YouTube Studio(日本語表示)、運営者のチャンネル。ショートの編集画面。右下に「関連動画」の欄がある

1本あたりの手間は小さくても、ショートが100本近くあると話は別です。

そこで、この画面操作をそのまま自動化することにしました。

自動化の全体像

3つの工程に分ける

作業は次の3つに分けました。

  1. 対応表を作る:どのショートに、どの本編を入れるかを決める(APIで読むだけ)
  2. 今の状態を読む:各ショートの関連動画に、いま何が入っているかを画面で確かめる(変更しない)
  3. 設定する:違うものだけ選び直して保存し、開き直して確かめる

いきなり設定から始めないのがポイントです。

2の「読むだけ」の工程を挟むと、何本を直すことになるのかが先にわかります。

使ったもの

種類使ったもの役割
言語Python 3.13全体の処理
ブラウザー操作Playwright 1.62(Python版)YouTube Studioの画面を操作
ブラウザーGoogle Chrome(手元にあるもの)ログインした状態を保つ
APIYouTube Data API v3対応表を作る(読むだけ)

Playwrightは、ブラウザーを自動で動かすライブラリーです。

Playwrightはpip install playwrightで入ります。

今回は手元のChromeを使うため、playwright installでブラウザーを入れる必要はありません。

同じブラウザーの自動操作をSeleniumで試す手順は、Selenium 4の導入とログイン操作で紹介しています。

準備:ログインしたChromeにPlaywrightをつなぐ

Playwrightに直接ログインさせない理由

YouTube Studioを開くには、Googleアカウントへのログインが必要です。

ログイン画面をスクリプトで通そうとすると、パスワードをプログラムに持たせることになります。

また、自動操作のブラウザーからだと、Googleにログインを止められる場合があります。

そこで役割を分けました。

  • ログイン:人が、ふだんどおりChromeで行う
  • 操作:Playwrightが、ログイン済みのChromeにあとからつないで行う

こうすると、パスワードはスクリプトのどこにも出てきません。

専用のプロフィールでChromeを開く

ふだん使っているChromeとは別に、自動操作専用のプロフィールでChromeを開きます。

--remote-debugging-portを付けると、外からつなげる状態で起動します。

import subprocess

CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
PORT = 9333

subprocess.Popen([
    CHROME,
    f"--remote-debugging-port={PORT}",
    "--user-data-dir=studio-profile",  # 専用のプロフィール(フォルダー)
    "--no-first-run",
    "https://studio.youtube.com/",
])

開いたChromeで、YouTube Studioに一度ログインしておいてください。

ログイン状態はstudio-profileフォルダーに残るため、次からはログインし直す必要はありません。

--user-data-dirは省かずに付けてください。

Chrome 136からは、いつものプロフィールのままではリモートデバッグで起動できません(Chrome for Developersの告知)。

connect_over_cdpでつなぐ

Playwright側は、connect_over_cdpで起動済みのChromeにつなぎます。

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp("http://127.0.0.1:9333", timeout=30000)
    pages = [x for x in browser.contexts[0].pages if "studio.youtube.com" in x.url]
    page = pages[-1] if pages else browser.contexts[0].new_page()

timeoutを付けているのは、つながったまま固まることがあったためです。

詳しくは、後半の「つまずいたところと対処」で説明します。

どのショートにどの本編を入れるか決める

説明欄にある本編のURLを使う

運営者のチャンネルでは、ショートの説明欄に毎回本編のURLを書いています。

そこで、説明欄から本編の動画IDを取り出して対応表を作りました。

流れは次のとおりです。

  1. YouTube Data APIで、自分のチャンネルの動画を全部取る
  2. 長さが60秒以下ならショート、それより長ければ本編とする
  3. ショートの説明欄から動画のURLを探し、本編の一覧にあるものを選ぶ

動画IDを取り出す部分は、正規表現1行で済みます。

import re

found = re.findall(r"(?:youtu\.be/|watch\?v=)([\w-]{11})", description)
main_ids = [vid for vid in found if vid in mains]  # 本編の一覧にあるものだけ

60秒を超えるショートがあるチャンネルでは、2の分け方を変えてください。

uploadsの再生リストだけだと動画が足りない

自分の動画を全部取るとき、ふつうはチャンネルのuploads(アップロード動画)の再生リストを使います。

ところが運営者のチャンネルでは、これだけでは足りませんでした。

重複して返ってくる動画があり、重複を除くと実際より少ない数しか取れなかったのです。

そこで、search().list(forMine=True, type="video")の結果も足して、重複を除きました。

これで全部の動画がそろいました。

対応表はCSVにする

サンプルのスクリプトでは、対応表をCSVで渡します。

short_id,main_id,main_title
9TxYMm-RhbI,H4NvDp-tQb4,WordPressなのに自社で更新できない?原因と改修方法を画面で解説
RNyytRf10LE,S71d0JFVuM8,自社ホームページを無料で診断する方法|URLを入れるだけで9項目をチェック

main_title(本編の題名)は、画面に今出ている動画と見比べるための列です。

YouTube Studioの画面を操作して設定する

編集画面の「関連動画」の欄を見つける

ショートの編集画面は、https://studio.youtube.com/video/<動画ID>/editで直接開けます。

「関連動画」の欄は、ytcp-dropdown-triggerという要素のうち「関連動画」と書かれたものです。

page.goto(f"https://studio.youtube.com/video/{short_id}/edit", wait_until="domcontentloaded")
field = page.locator(
    "ytcp-video-metadata-editor-sidepanel ytcp-dropdown-trigger",
    has=page.locator(".label-text", has_text="関連動画"),
)
field.wait_for(timeout=30000)

欄の文字を読めば、今入っている動画の題名がわかります。

題名が対応表と同じなら、そのショートは何もせずに次へ進む作りです。

選択画面では動画IDで選ぶ

欄をクリックすると「特定の動画の選択」という画面が開きます。

上に検索欄があり、その下に自分の動画がカードで並ぶ作りです。

「特定の動画の選択」の画面。上に「自分の動画を検索」の欄があり、その下に自分の動画のサムネイルと題名がカードで並んでいる
2026-11-02 撮影。YouTube Studio(日本語表示)、運営者のチャンネル。「関連動画」の欄を押すと開く、特定の動画の選択の画面

ここで題名を頼りに選ぶと、似た題名の動画を選び間違えるおそれがあります。

運営者のチャンネルにも、本編とショートで題名の頭が同じ動画がありました。

調べると、カードのサムネイルは背景画像で表示されていて、そのURLに動画IDが入っていました。

background-image: url("https://i9.ytimg.com/vi_webp/H4NvDp-tQb4/mqdefault.webp?...")

そこで、サムネイルのURLに本編の動画IDを含むカードを選ぶ作りにしています。

dialog = page.locator("ytcp-dialog.ytcp-video-pick-dialog")
box = dialog.locator("input[placeholder='自分の動画を検索']")
box.fill(main_title[:40])  # 題名で検索して候補をしぼる

card = dialog.locator(f"ytcp-entity-card:has(div.thumbnail[style*='/{main_id}/'])").first
card.click()
dialog.locator("tp-yt-paper-dialog").wait_for(state="hidden")

検索は候補をしぼるために使い、最後に決めるのは動画IDです。

これで選び間違いは起きません。

保存して、開き直して確かめる

カードを選ぶと、選択画面が閉じて欄に題名が入ります。

あとは右上の「保存」を押すだけです。

保存が終わると、「保存」ボタンは押せない状態に戻ります。

これを待ってから次へ進むようにしました。

page.locator("ytcp-button#save").click()
page.wait_for_function(
    "() => document.querySelector('ytcp-button#save')?.getAttribute('aria-disabled') === 'true'"
)

最後に、編集画面を開き直して欄を読み、本編が入っているかを確かめます。

保存の通信が失敗していても、ここで気づけます。

つまずいたところと対処

未設定の欄には「なし」と出る

最初に全部のショートを読んだとき、29本が「別の動画が入っている」という判定になりました。

画面を見ると、そのほとんどは未設定で、欄に「なし」と表示されていただけでした。

今は「なし」を未設定として扱っています。

lines = [s for s in lines if s not in ("関連動画", "なし")]

「interrupted by another navigation」で止まる

page.goto()を続けて呼ぶと、次のエラーで止まることがありました。

Page.goto: Navigation to ".../edit" is interrupted by another navigation to ".../edit"

YouTube Studioの画面自身が切り替わっている途中に、次のページを開こうとしたのが原因です。

少し待ってから開き直すと通るので、3回まで試すようにしました。

Chromeにつながったまま固まる

あるとき、connect_over_cdpが戻ってこなくなりました。

Chromeの画面は開いているのに、操作できるページが1つも見えない状態です。

専用のChromeを閉じて開き直すと直りました。

ログインは専用のプロフィールに残っているので、入り直す必要はありません。

固まったまま待ち続けないよう、timeout=30000を付けています。

離れるときの確認ダイアログ

保存の前に失敗して次のショートへ移ると、「このサイトを離れますか?」という確認が出ます。

未保存の変更はこのスクリプトが入れたものだけなので、離れてよい場面です。

page.on("dialog", lambda d: d.accept())で、自動で閉じるようにしました。

実行結果:98本中29本を直した

運営者のチャンネルのショート98本で試した結果です。

状態本数対応
正しい本編が入っていた69本確認だけ
未設定(「なし」)28本本編を設定した
別の動画が入っていた1本本編に直した

設定ミスの1本は、補助金の本編を入れるべきショートでした。

実際には、まったく関係のないショートが入っていたのです。

画面で1本ずつ見ていたときには、気づけていませんでした。

かかった時間は次のとおりです。

  • 98本の確認と、29本の設定:約17分
  • 1本を設定して確かめるまで:約20秒

98本すべてで、保存したあと開き直して本編が入っていることを確かめました。

使う前に知っておきたい注意点

画面が変わると動かなくなる

このやり方は、YouTube Studioの画面の作りに頼っています。

要素の名前や文言が変われば動かなくなります。

動かなくなったときに備えて、次の2つを入れておくと安心です。

  • 失敗したら画面を保存する(page.screenshot())
  • 失敗が3本続いたら止める

表示言語は日本語にしておく

「関連動画」「なし」「自分の動画を検索」など、画面の文言で要素を探しています。

YouTube Studioの表示言語が英語だと見つかりません。

速く・大量に動かさない

自分のアカウントで、自分がふだんしている操作を代わりに行うだけです。

それでも、短時間に大量の操作をするのは避けてください。

今回は1本ずつ順に処理し、1本ごとに1秒待っています。

いきなり全部を流さず、まず1本だけで試してください。

プロフィールのフォルダーを人に渡さない

専用のstudio-profileフォルダーには、ログインした状態が入っています。

Gitに入れたり、人に渡したりしないでください。

サンプルのスクリプト(全文)

この記事のコードをまとめたスクリプトです。

shorts_related.pyという名前で保存し、同じフォルダーに対応表のpairs.csvを置いて使います。

ログ(log.json)と専用のプロフィールも、同じフォルダーにできます。

"""YouTube ショートの「関連動画」を YouTube Studio の画面操作でまとめて設定する(記事のサンプル)。

前提
  - pip install playwright(ブラウザーは手元の Chrome を使うので playwright install は不要)
  - YouTube Studio の表示言語が日本語(画面の文言で要素を探すため)
  - 対応表 pairs.csv(1行目は見出し): short_id,main_id,main_title

使い方
  python shorts_related.py chrome            専用の Chrome を開く → その Chrome で Studio にログインしておく
  python shorts_related.py scan pairs.csv    今の関連動画を読むだけ(変更しない)
  python shorts_related.py set pairs.csv     関連動画を設定する(済んだものは飛ばす。記録は log.json)
  python shorts_related.py set pairs.csv --only <ショートID>   1本だけ試す
"""
import csv
import json
import os
import subprocess
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
PROFILE = os.path.join(HERE, "studio-profile")  # ログイン情報が入るので Git などに入れない
CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
PORT = 9333
LOG = os.path.join(HERE, "log.json")


def open_chrome():
    subprocess.Popen([CHROME, f"--remote-debugging-port={PORT}", f"--user-data-dir={PROFILE}",
                      "--no-first-run", "--no-default-browser-check", "https://studio.youtube.com/"])


def studio_page(p):
    # つながったまま固まることがある。30秒で諦めて、専用の Chrome を開き直す
    browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{PORT}", timeout=30000)
    pages = [x for x in browser.contexts[0].pages if "studio.youtube.com" in x.url]
    page = pages[-1] if pages else browser.contexts[0].new_page()
    # 失敗して未保存のまま次へ移るときの「このサイトを離れますか?」は離れる
    page.on("dialog", lambda d: d.accept())
    return page


def open_edit(page, vid):
    """ショートの編集画面を開き、「関連動画」の欄を返す。"""
    url = f"https://studio.youtube.com/video/{vid}/edit"
    for k in range(3):
        try:
            page.goto(url, wait_until="domcontentloaded")
            break
        except Exception as e:
            # Studio 自身の画面遷移と重なると "interrupted by another navigation" になる
            if "interrupted" not in str(e) or k == 2:
                raise
            page.wait_for_timeout(3000)
    if "accounts.google.com" in page.url:
        sys.exit("Studio からログアウトしています。専用の Chrome でログインし直してください")
    field = page.locator("ytcp-video-metadata-editor-sidepanel ytcp-dropdown-trigger",
                         has=page.locator(".label-text", has_text="関連動画"))
    field.wait_for(timeout=30000)
    page.wait_for_timeout(1500)
    return field


def current(field):
    """欄に出ている動画の題。未設定のときは「なし」と出るので空にする。"""
    lines = [s.strip() for s in field.inner_text().split("\n") if s.strip()]
    lines = [s for s in lines if s not in ("関連動画", "なし")]
    return lines[0] if lines else ""


def set_one(page, row):
    field = open_edit(page, row["short_id"])
    if current(field) == row["main_title"]:
        return "設定済み(変更なし)"
    field.scroll_into_view_if_needed()
    field.click()
    dialog = page.locator("ytcp-dialog.ytcp-video-pick-dialog")
    box = dialog.locator("input[placeholder='自分の動画を検索']")
    box.wait_for(timeout=15000)
    box.fill(row["main_title"][:40])
    # サムネイル画像の URL に入っている動画 ID で選ぶ(題の似た動画を選び間違えない)
    card = dialog.locator(f"ytcp-entity-card:has(div.thumbnail[style*='/{row['main_id']}/'])").first
    card.wait_for(timeout=15000)
    card.click()
    dialog.locator("tp-yt-paper-dialog").wait_for(state="hidden", timeout=15000)
    if current(field) != row["main_title"]:
        raise RuntimeError(f"選んだあとの表示が違う:{current(field)}")
    page.locator("ytcp-button#save").click()
    # 保存が終わると「保存」ボタンが押せない状態に戻る
    page.wait_for_function("() => document.querySelector('ytcp-button#save')?.getAttribute('aria-disabled') === 'true'",
                           timeout=30000)
    page.wait_for_timeout(1500)
    after = current(open_edit(page, row["short_id"]))  # 開き直して確かめる
    if after != row["main_title"]:
        raise RuntimeError(f"保存後に開き直すと違う:{after or '(未設定)'}")
    return "設定した(開き直して確認)"


def main():
    cmd = sys.argv[1] if len(sys.argv) > 1 else ""
    if cmd == "chrome":
        open_chrome()
        return
    if cmd not in ("scan", "set") or len(sys.argv) < 3:
        sys.exit(__doc__)
    rows = list(csv.DictReader(open(sys.argv[2], encoding="utf-8-sig")))
    if "--only" in sys.argv:
        rows = [r for r in rows if r["short_id"] == sys.argv[sys.argv.index("--only") + 1]]
    log = json.load(open(LOG, encoding="utf-8")) if os.path.exists(LOG) else {}

    from playwright.sync_api import sync_playwright
    with sync_playwright() as p:
        page = studio_page(p)
        fails = 0
        for i, row in enumerate(rows, 1):
            if cmd == "scan":
                now = current(open_edit(page, row["short_id"]))
                state = "一致" if now == row["main_title"] else ("未設定" if not now else "別の動画")
                print(f"{i:3d} {state} {row['short_id']} -> {now}", flush=True)
                continue
            if log.get(row["short_id"], "").startswith("設定"):
                continue
            try:
                result = set_one(page, row)
            except Exception as e:
                result = f"失敗:{type(e).__name__}: {str(e).splitlines()[0][:150]}"
                fails += 1
            log[row["short_id"]] = result
            json.dump(log, open(LOG, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
            print(f"{i:3d} {row['short_id']} {result}", flush=True)
            if fails >= 3:
                sys.exit("失敗が3本になったので止めました")
            time.sleep(1)


if __name__ == "__main__":
    main()

使うときのコマンドは次のとおりです。

python shorts_related.py chrome            # 専用の Chrome を開き、Studio にログインする
python shorts_related.py scan pairs.csv    # 今の関連動画を読むだけ
python shorts_related.py set pairs.csv --only <ショートID>   # 1本だけ試す
python shorts_related.py set pairs.csv     # 全部を設定する(済んだものは飛ばす)

Chromeの場所が違うPCでは、CHROMEの行を書き換えてください。

スポンサーリンク

まとめ

ショートの関連動画は公式APIで設定できません。

それでも、YouTube Studioの画面操作を自動化すれば、まとめて設定できます。

ポイントは3つです。

  • ログインは人、操作はPlaywright:起動済みのChromeにconnect_over_cdpでつなぐ
  • 選ぶのは動画ID:選択画面のサムネイルのURLで本編を見分ける
  • 読むだけの確認を先に:今の状態を見てから、違うものだけ直す

運営者のチャンネルでは、新しいショートを上げるたびに対応表を作り直して流しています。

設定が済んだショートは飛ばすので、新しく上げた分だけを設定します。

次にやることは、ショートの本数が増えてきたら、一度「今の状態を読むだけ」を試すことです。

設定したつもりで漏れていたショートが見つかるかもしれません。

動画の作り方や書き出しの記事は、動画編集の使い方まとめから探せます。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

動画編集のおすすめ書籍

動画編集PCの選び方

周辺機器の優先度

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

須澤 元貴のアバター 須澤 元貴 代表社員

MOTOKI合同会社 代表社員。
アニメ・映像業界とゲーム業界で、2D・UI・3D・アニメーション制作、映像編集、ツール制作に携わってきました。
現在はWordPressを中心にWebサイトの制作とカスタマイズを手がけています。
著書に『Unity デザイナーズ・バイブル』『Unityゲーム プログラミング・バイブル 2nd Generation』(ボーンデジタル)。
岡山県出身、埼玉県在住。

目次