ショートを何十本も出していると、関連動画の設定が追いつかなくなります。
この記事は、関連動画を本編へまとめて設定したい、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本だけで試してください。
- Playwrightを入れる(コマンドは下のコードブロック)
- 専用のChromeを開き、そのChromeでYouTube Studioに一度ログインする
- どのショートにどの本編を入れるかを、対応表のCSVにまとめる
- 今の関連動画を読むだけの確認を流して、直す本数を先に知る
- 1本だけ設定して、Studioの画面で結果を確かめる
- 問題がなければ全部を設定する(済んだものは飛ばす)
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本ずつ選ぶしかないのです。

1本あたりの手間は小さくても、ショートが100本近くあると話は別です。
そこで、この画面操作をそのまま自動化することにしました。
自動化の全体像
3つの工程に分ける
作業は次の3つに分けました。
- 対応表を作る:どのショートに、どの本編を入れるかを決める(APIで読むだけ)
- 今の状態を読む:各ショートの関連動画に、いま何が入っているかを画面で確かめる(変更しない)
- 設定する:違うものだけ選び直して保存し、開き直して確かめる
いきなり設定から始めないのがポイントです。
2の「読むだけ」の工程を挟むと、何本を直すことになるのかが先にわかります。
使ったもの
| 種類 | 使ったもの | 役割 |
|---|---|---|
| 言語 | Python 3.13 | 全体の処理 |
| ブラウザー操作 | Playwright 1.62(Python版) | YouTube Studioの画面を操作 |
| ブラウザー | Google Chrome(手元にあるもの) | ログインした状態を保つ |
| API | YouTube 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を取り出して対応表を作りました。
流れは次のとおりです。
- YouTube Data APIで、自分のチャンネルの動画を全部取る
- 長さが60秒以下ならショート、それより長ければ本編とする
- ショートの説明欄から動画の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で選ぶ
欄をクリックすると「特定の動画の選択」という画面が開きます。
上に検索欄があり、その下に自分の動画がカードで並ぶ作りです。

ここで題名を頼りに選ぶと、似た題名の動画を選び間違えるおそれがあります。
運営者のチャンネルにも、本編とショートで題名の頭が同じ動画がありました。
調べると、カードのサムネイルは背景画像で表示されていて、その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で本編を見分ける
- 読むだけの確認を先に:今の状態を見てから、違うものだけ直す
運営者のチャンネルでは、新しいショートを上げるたびに対応表を作り直して流しています。
設定が済んだショートは飛ばすので、新しく上げた分だけを設定します。
次にやることは、ショートの本数が増えてきたら、一度「今の状態を読むだけ」を試すことです。
設定したつもりで漏れていたショートが見つかるかもしれません。
動画の作り方や書き出しの記事は、動画編集の使い方まとめから探せます。
