YouTube Data API v3 × GAS × Geminiで実現!再生リストの自動並び替えツールを自作する全手順

2026-05-09

GAS Gemini YouTube

YouTube、Google Apps Script(GAS)、Geminiのアイコンが矢印でつながれたサイバーな背景の画像。中央に「YouTube Data API v3 × GAS × Geminiで実現!再生リストの自動並び替えツールを自作する全手順」というテキストが配置されている。
YouTubeの再生リスト、動画が増えれば増えるほど「並び替え」が苦行になりますよね。100曲、200曲あるアニソンリストを「作品順」や「放送時期順」に整理したいけれど、手動はあまりにも大変過ぎます……。

そこで、Google Apps Script (GAS) と AI(Gemini)を組み合わせて、再生リストを爆速で整理する「最強の管理ツール」を開発しました。実際に運用して成功した操作手順を完全公開します!


ツールの全体構成

単にプログラムでYouTubeを操作するだけでなく、「AIの得意なこと(並び替えの考案)」と「スプレッドシートの得意なこと(正確なデータの紐付け)」を役割分担させているのが最大の特徴です。


役割

  • YouTube Data API v3:YouTube上の再生リストを操作するための窓口
  • Google Apps Script (GAS):全体の司令塔。APIを叩き、取得したデータをスプレッドシートへ出力
  • Googleスプレッドシート:データの保管庫兼ユーザーインターフェース。
  • Gemini (AI):並び替え案のロジック担当。曖昧な要望でも上手くとらえて、最適な順序を計算できる可能性がある。

ツールの全体構成図


YouTube再生リスト自動並び替えツールの全体構成図。YouTube Data API v3、Google Apps Script (GAS)、Googleスプレッドシート、Gemini AIの4つの要素が連携するフロー。1.リスト取得から5.反映までの手順が図解されている。

本記事の前提条件

  • Google AI Plusプラン以上:GoogleスプレッドシートのGemini関数を利用する想定のため
※ただし、Gemini関数を使わなくてもツール自体は利用できます。


ツール操作の5ステップ

ツールの導入から反映までの全手順を、実際の画面と共に説明します。
今回は私が普段よく聞いている以下のアニソン再生リストをいい感じに並べ替えてみたいと思います。



【STEP 1】YouTube APIの有効化とセットアップ

まずは、Googleスプレッドシートの「拡張機能」→「Apps Script」を開きます。


operation1

Apps Scriptの画面で、サービスの「+」を選択します。

operation2


サービスを追加画面で「YouTube Data API v3」を追加し、IDは「YouTubePlayListControl」とします。デフォルトのIDでも問題ありませんが、その場合は次のコードでサービス名の設定をそのIDに変更してください。

operation3


追加したら、コード.gsに以下のようにコードを貼り付けて保存します。

operation4


貼り付けるコードは以下です。
一番上の'ここに再生リストのIDを入力'の部分にあなたが並べ替えしたい再生リストのIDに置き換えてください。
また、今回はアニソンをシリーズごとに固めて並び替えしたいので、92行目でシーズンを出してもらうようなプロンプトにしてますが、他の条件で並び替えをしたい場合はこの92行目のGeminiプロンプトを変更すると良いです。

  1. /**
  2.  * 【設定】並び替え・管理したい再生リストのID
  3.  */
  4. const PLAYLIST_ID = 'ここに再生リストのIDを入力';
  5. /**
  6.  * サービス名の設定
  7.  */
  8. const YT = YouTubePlayListControl;
  9. /**
  10.  * カスタムメニューの作成
  11.  */
  12. function onOpen() {
  13.   const ui = SpreadsheetApp.getUi();
  14.   ui.createMenu('YouTubeツール')
  15.     .addItem('⚠️ 初回セットアップ実行', 'setupTool')
  16.     .addSeparator()
  17.     .addItem('1. リストを取得(数式準備込)', 'fetchPlaylistVideos')
  18.     .addItem('2. 削除・非表示動画をリストから一掃', 'purgeInvalidVideos')
  19.     .addItem('3. 並べ替え後シートの順序をYouTubeに反映', 'applyNewOrder')
  20.     .addSeparator()
  21.     .addItem('📊 ログを手動でリセット', 'resetQuotaLog')
  22.     .addToUi();
  23. }
  24. /**
  25.  * ⚠️ 初回セットアップ実行
  26.  */
  27. function setupTool() {
  28.   const ss = SpreadsheetApp.getActiveSpreadsheet();
  29.   
  30.   const sheets = [
  31.     { name: '再生リスト取得', headers: ['現在の順序(0始)', '動画タイトル', '動画ID', 'アニメシリーズ+シーズン', 'PlaylistItemID', 'ステータス', '存在チェック'] },
  32.     { name: '並べ替え後', headers: ['現在の順序(0始)', '動画タイトル', '動画ID', 'アニメシリーズ+シーズン', 'PlaylistItemID', 'ステータス', '並べ替え後の順序'] }
  33.   ];
  34.   sheets.forEach(s => {
  35.     let sheet = ss.getSheetByName(s.name) || ss.insertSheet(s.name);
  36.     sheet.clear().appendRow(s.headers);
  37.     sheet.getRange("1:1").setBackground("#eeeeee").setFontWeight("bold");
  38.   });
  39.   let log = ss.getSheetByName('クォータログ') || ss.insertSheet('クォータログ');
  40.   log.clear().getRange("A1:B4").setValues([
  41.     ["項目","数値"],
  42.     ["消費累計", 0],
  43.     ["残り枠", 10000],
  44.     ["最終更新", new Date()]
  45.   ]);
  46.   log.getRange("B2:B3").setBackground("#eeeeee");
  47.   log.getRange("A6:F6").setValues([["日時", "内容・動画名", "消費", "累計", "エラーメッセージ", "送信リクエスト(JSON)"]])
  48.      .setBackground("#eeeeee").setFontWeight("bold");
  49.   
  50.   ss.getSheetByName('再生リスト取得').activate();
  51.   SpreadsheetApp.getUi().alert('セットアップ完了!');
  52. }
  53. /**
  54.  * 1. リストを取得
  55.  */
  56. function fetchPlaylistVideos() {
  57.   const ss = SpreadsheetApp.getActiveSpreadsheet();
  58.   const sheetGet = ss.getSheetByName('再生リスト取得');
  59.   const sheetAfter = ss.getSheetByName('並べ替え後');
  60.   if (!sheetGet || !sheetAfter) return;
  61.   
  62.   sheetGet.clear();
  63.   sheetGet.appendRow(['現在の順序(0始)', '動画タイトル', '動画ID', 'アニメシリーズ+シーズン', 'PlaylistItemID', 'ステータス', '存在チェック']);
  64.   sheetGet.getRange("1:1").setBackground("#eeeeee").setFontWeight("bold");
  65.   let nextPageToken = '';
  66.   let count = 0;
  67.   let allRowsData = [];
  68.   while (nextPageToken != null) {
  69.     try {
  70.       const res = YT.PlaylistItems.list('snippet,contentDetails,status', {
  71.         playlistId: PLAYLIST_ID,
  72.         maxResults: 50,
  73.         pageToken: nextPageToken
  74.       });
  75.       
  76.       // 【修正】取得に成功したタイミングで1ユニット記録
  77.       logQuota("リスト取得API成功 (50件分ページ取得)", 1);
  78.       const lastRowInSheet = sheetGet.getLastRow();
  79.       const pageRows = res.items.map((item, index) => {
  80.         const targetRow = lastRowInSheet + index + 1;
  81.         const title = item.snippet.title;
  82.         
  83.         const prompt = "このセルの動画の曲のアニメタイトルを表示して。アニメ放送シーズンも検索して確認して表示してください。表示形式は、「アニメタイトル」+「(シーズン)」としてください。ただし、「アニメタイトル」は「第2期」などシーズンを示すワードを省く";
  84.         const geminiFormula = `=GEMINI("${prompt}", B${targetRow}:C${targetRow})`;
  85.         const presenceCheckFormula = `=COUNTIF('並べ替え後'!A:A, A${targetRow})`;
  86.         
  87.         const privacy = item.status.privacyStatus;
  88.         let statusLabel = "公開";
  89.         if (title === 'Deleted video' || privacy === 'privacyStatusUnspecified') {
  90.           statusLabel = "削除済み";
  91.         } else if (privacy === 'private') {
  92.           statusLabel = "非公開";
  93.         } else if (privacy === 'unlisted') {
  94.           statusLabel = "限定公開";
  95.         }
  96.         // 個別の取得進捗ログ(消費は0)
  97.         logQuota(` └ 取得完了: ${title}`, 0);
  98.         return [count++, title, item.contentDetails.videoId, geminiFormula, item.id, statusLabel, presenceCheckFormula];
  99.       });
  100.       if (pageRows.length > 0) {
  101.         sheetGet.getRange(sheetGet.getLastRow() + 1, 1, pageRows.length, 7).setValues(pageRows);
  102.         allRowsData = allRowsData.concat(pageRows);
  103.       }
  104.       nextPageToken = res.nextPageToken;
  105.       
  106.     } catch (e) {
  107.       logQuota("❌取得エラー", 0, e.message);
  108.       break;
  109.     }
  110.   }
  111.   // 「並べ替え後」シートに「同期用数式」と「ROW連番」を準備
  112.   sheetAfter.getRange(2, 1, sheetAfter.getMaxRows()-1, 7).clearContent();
  113.   const totalVideos = allRowsData.length;
  114.   if (totalVideos > 0) {
  115.     const formulas = [];
  116.     for (let i = 0; i < totalVideos; i++) {
  117.       const r = i + 2;
  118.       formulas.push([
  119.         `=IFERROR(INDEX('再生リスト取得'!B:B, MATCH($A${r}, '再生リスト取得'!$A:$A, 0)), "番号未入力")`,
  120.         `=IFERROR(INDEX('再生リスト取得'!C:C, MATCH($A${r}, '再生リスト取得'!$A:$A, 0)), "")`,
  121.         `=IFERROR(INDEX('再生リスト取得'!D:D, MATCH($A${r}, '再生リスト取得'!$A:$A, 0)), "")`,
  122.         `=IFERROR(INDEX('再生リスト取得'!E:E, MATCH($A${r}, '再生リスト取得'!$A:$A, 0)), "")`,
  123.         `=IFERROR(INDEX('再生リスト取得'!F:F, MATCH($A${r}, '再生リスト取得'!$A:$A, 0)), "")`,
  124.         `=ROW()-2`
  125.       ]);
  126.     }
  127.     sheetAfter.getRange(2, 2, totalVideos, 6).setFormulas(formulas);
  128.   }
  129.   SpreadsheetApp.getUi().alert('取得完了!');
  130. }
  131. /**
  132.  * 2. 削除・非表示動画をリストから一掃
  133.  */
  134. function purgeInvalidVideos() {
  135.   const ss = SpreadsheetApp.getActiveSpreadsheet();
  136.   const sheet = ss.getSheetByName('再生リスト取得');
  137.   const data = sheet.getDataRange().getValues().slice(1);
  138.   let count = 0;
  139.   const confirm = SpreadsheetApp.getUi().alert('確認', '非公開・削除済み動画を削除しますか?', SpreadsheetApp.getUi().ButtonSet.YES_NO);
  140.   if (confirm !== SpreadsheetApp.getUi().Button.YES) return;
  141.   data.forEach(row => {
  142.     if (row[5] === '非公開' || row[5] === '削除済み') {
  143.       try {
  144.         YT.PlaylistItems.remove(row[4]);
  145.         count++;
  146.         logQuota(`✅一掃成功: ${row[1]}`, 50);
  147.         Utilities.sleep(1000);
  148.       } catch (e) {
  149.          logQuota(`❌一掃失敗: ${row[1]}`, 0, e.message);
  150.       }
  151.     }
  152.   });
  153.   SpreadsheetApp.getUi().alert(`${count}件削除しました。`);
  154. }
  155. /**
  156.  * 3. 並べ替え後シートの順序をYouTubeに反映
  157.  */
  158. function applyNewOrder() {
  159.   const ss = SpreadsheetApp.getActiveSpreadsheet();
  160.   const sheetAfter = ss.getSheetByName('並べ替え後');
  161.   if (!sheetAfter) return;
  162.   const data = sheetAfter.getDataRange().getValues();
  163.   const videoList = data.slice(1);
  164.   let updateCount = 0;
  165.   let errorCount = 0;
  166.   SpreadsheetApp.getUi().alert('反映を開始します。G列が空欄の行はスキップされます。');
  167.   for (let i = 0; i < videoList.length; i++) {
  168.     const row = videoList[i];
  169.     const videoTitle = row[1];
  170.     const originalIdx = parseInt(row[0]);
  171.     
  172.     const targetIdxStr = row[6].toString();
  173.     if (targetIdxStr === "") continue; // 空欄スキップ
  174.     const targetIdx = parseInt(targetIdxStr);
  175.     
  176.     // 現在の位置(A列)と目標位置(G列)が一致していればスキップ
  177.     if (originalIdx === targetIdx) continue;
  178.     const playlistItemId = row[4].toString();
  179.     const videoId = row[2].toString();
  180.     if (!playlistItemId || playlistItemId === "番号未入力") continue;
  181.     const resource = {
  182.       "id": playlistItemId,
  183.       "kind": "youtube#playlistItem",
  184.       "snippet": {
  185.         "playlistId": PLAYLIST_ID,
  186.         "resourceId": { "kind": "youtube#video", "videoId": videoId },
  187.         "position": targetIdx
  188.       }
  189.     };
  190.     try {
  191.       YT.PlaylistItems.update(resource, 'snippet');
  192.       updateCount++;
  193.       logQuota(`✅移動成功: ${videoTitle} (pos:${targetIdx})`, 50);
  194.       Utilities.sleep(500);
  195.     } catch (e) {
  196.       errorCount++;
  197.       logQuota(`❌移動失敗: ${videoTitle}`, 0, e.message, JSON.stringify(resource));
  198.       if (e.message.indexOf('quota') !== -1) {
  199.         SpreadsheetApp.getUi().alert('API制限に達しました。処理を停止します。');
  200.         break;
  201.       }
  202.     }
  203.   }
  204.   SpreadsheetApp.getUi().alert(`終了。成功: ${updateCount}件 / エラー: ${errorCount}件`);
  205. }
  206. /**
  207.  * 📊 ログを手動でリセット
  208.  */
  209. function resetQuotaLog() {
  210.   const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('クォータログ');
  211.   if (sheet) {
  212.     sheet.getRange("B2").setValue(0);
  213.     sheet.getRange("B3").setValue(10000);
  214.     if (sheet.getLastRow() >= 7) sheet.getRange(7, 1, sheet.getLastRow() - 6, 6).clearContent();
  215.   }
  216. }
  217. // --- 共通内部ロジック ---
  218. function logQuota(operation, units, errorDetail = "", requestJson = "") {
  219.   const ss = SpreadsheetApp.getActiveSpreadsheet();
  220.   let logSheet = ss.getSheetByName('クォータログ');
  221.   if (!logSheet) return;
  222.   const now = new Date();
  223.   const lastUpdate = logSheet.getRange("B4").getValue();
  224.   if (lastUpdate instanceof Date && getQuotaDayId(lastUpdate) !== getQuotaDayId(now)) {
  225.     logSheet.getRange("B2").setValue(0);
  226.     logSheet.getRange("B3").setValue(10000);
  227.     if (logSheet.getLastRow() >= 7) logSheet.getRange(7, 1, logSheet.getLastRow() - 6, 6).clearContent();
  228.     logSheet.appendRow([now, "--- 日付変更リセット ---", 0, 0, "", ""]);
  229.   }
  230.   const currentUsed = logSheet.getRange("B2").getValue() || 0;
  231.   const newTotal = currentUsed + units;
  232.   logSheet.getRange("B2").setValue(newTotal);
  233.   logSheet.getRange("B3").setValue(10000 - newTotal);
  234.   logSheet.getRange("B4").setValue(now);
  235.   logSheet.appendRow([now, operation, units, newTotal, errorDetail, requestJson]);
  236. }
  237. function getQuotaDayId(date) {
  238.   const adjusted = new Date(date.getTime() - (16 * 60 * 60 * 1000));
  239.   return adjusted.getFullYear() + "/" + (adjusted.getMonth() + 1) + "/" + adjusted.getDate();
  240. }



コードを貼り付けた後スプレッドシートの画面に戻ると、「YouTubeツール」タブが表示されています。初回のセットアップは「YouTubeツール」>「⚠️初回セットアップ実行」を選択します。

operation5


セットアップ実行が完了すると、以下の通り3つのタブとそれぞれヘッダが作成されます。

operation6


【STEP 2】現在のリスト取得とAI解析

セットアップが完了したら、「再生リスト取得」シートに再生リストを取得します。
また、以下のように「YouTubeツール」>「1.リストを取得(数式準備込)」を選択します。

operation7


問題なく完了すると、以下のように完了メッセージが出力されます。

operation8

また、「クォータログ」シートを確認すると、以下のようにログとクォータの消費量が確認できます。


operation9

ここで、クォータというのは、今回YouTube Data API v3を利用しますが、利用制限があります。その制限量をクォータという指標でチェックされます。1日に10,000クォータ使えます。

再生リスト取得であれば50件取得で1クォータ消費ですが、再生リストの並べ替えであれば1件50クォータ消費します。1日に10,000クォータということは、1日に200本程度しか動画を並び替えることができません。そのため、今日はどのくらい使ったかを確認するためのログを用意しています。

クォータ消費は太平洋時間の午前0時(PT)、日本時間だとJST17:00頃のリセットになります。実際はサマータイムなどもあってずれたりしますが、JST17:00頃にはリセットされると覚えておけば十分です。


再生リスト取得が完了すると「再生リスト取得」シートに指定の再生リスト情報が出力されます。
「再生リスト取得」シートD列を確認してもらうと、数式が入っていることがわかると思います。ここはGeminiに並べ替えの情報を出力してもらう列にしています。
以下画面の赤枠の「生成して挿入」のボタンを押します。

operation10

すると、各セルのGeminiプロンプトが動作してくれて、アニメシリーズ名と放送シーズンを出力してくれます。これは便利!
このプロンプトの実行後は、各セルをざっと確認するようにしてください。今回は動画のタイトルからの情報で情報を出してくれているので、曲名が被っていたり、動画タイトルの情報が少なかったりするとGeminiが分からずに間違った回答を出します。そこは自分の再生リストである程度分かるでしょうから、数式ではなく、直接入力で補完してあげてください。

operation11


【STEP 3】Geminiに「最強の順序」を作ってもらう

この「再生リスト取得」シートができたらいよいよGeminiの出番です。
GeminiのアプリでもWebでもこのツールのスプレッドシートを添付してあげて、例えば以下のようなプロンプトを指定して並び替えてもらいます。

  1. 添付の再生リスト取得シートの内容を以下の条件で並び替えたCSVリストを作成してください。
  2. 【出力形式のルール】
  3. ・並べ替え後は、「現在の順序(0始)」の列のみを出力すること。
  4. ・各行の最後は改行すること。
  5. ・Markdown、ヘッダー、挨拶は一切不要。
  6. 【ソートの最優先ルール:シリーズの統一】
  7. ・『ひぐらし』と『ひぐらし解』、『とある科学』と『とある魔術』、『ソードアート・オンライン』の各期など、シリーズ作品はタイトルの表記が多少違っていても、必ず同一グループとして一箇所に固めてください。
  8. #優先順位
  9. 1. 先頭:『推しの子』関連
  10. 2. 最後:ライブのシメにふさわしい超人気曲
  11. 3. シリーズ作品を同一グループとして固める(最重要)
  12. 4. 同一シリーズ内では放送シーズンが古い順
  13. 5. 曲が盛り上がる順
  14. 6. アニメ作品の一般的な人気順

プロンプトのコツとしては、AIは文字列を正しく転記することは苦手です。そのため、「再生リスト取得」シート自体を並べ替えさせようとすると、動画IDやPlaylistItemIDが正しく転記されません。AIは特に意味の無い文字列の転記はかなり苦手のようです。そのため、「現在の順序(0始)」列のみに絞って並べ替えをさせます。
再生リスト取得シートと並べ替え後シートを別にすることで、ここが実質IDみたいな扱いもできますので、この転記が簡単な数字だけを並べ替えさせることでけっこう上手くいく確率が上がります。
プロンプトを以下のようにGeminiに貼り付けて実行します。

operation12

200曲もあると相当時間がかかりますが、ギリギリ返ってきます。失敗したら再実行で・・・w



【STEP 4】並べ替え結果の反映とカスタマイズ

成功したら以下のボタンでコピーしてください。改行が多くなっているので、一旦サクラエディタに貼り付けた後に「並べ替え後」シートに貼り付けた方がいいです。

operation13

サクラエディタなどを経由して以下のように「並べ替え後」シートのA列に貼り付けます。

operation14

すると、B列以降は再生リスト取得時に数式が入っているので、B列以降の情報も転記されます。(Geminiが苦手な転記を数式でフォローしてあげます。)

また、「並び替え後」シートの一番下まで確認して、Geminiがソート後に番号出力を漏らしていないかも確認してください。漏れていた場合、どの番号が漏れているのかの存在チェックができるように「再生リスト取得」シートの「存在チェック」列を用意してますので、これで「0」になってて無いものを「並び替え後」シートに転記してGeminiの不足分を補完してあげてください。

ここからは、自分の好みの並べ替え順になっているかを順に確認していきます。
シリーズごとに並び替えたり、順番変えたり、などなど。Excelでの行入れ替えで塊で並び替えできたりするため、再生リストの手動並べ替えよりは圧倒的に楽だと思います。明確に並べ替えたい要件があるならGeminiに並び替えてもらう必要性も無いですね。

operation15


【STEP 5】YouTubeへの反映

並び替えが完了したら、以下の通り「YouTubeツール」>「3. 並べ替え後シートの順序をYouTubeに反映」を選択して、ソート処理を実行してください。
「並び替え後」シートのG列の「並び替え後の順序」列の番号になるようにソート処理をしていきます。


operation16


ソートが完了したらメッセージが出力されますが、どこまで完了したのかは「クォータログ」シートのログを確認してください。クォータが足りなくなったりしたら、その部分から翌日再実行する必要があります。
その場合は、「並び替え後」シートのG列の「並び替え後の順序」列の番号を成功したところは値削除して再実行してください。値の無い行は処理スキップして全体の処理を進めます。

また、クォータ節約のため、「現在の順序」列と「並び替え後の順序」列の順番が同じ場合は処理をスキップします。ログに出てない場合はスキップされていますので、ご注意ください。

operation17


運用で役立つ「こだわり機能」

  • 空欄スキップ機能:「並べ替え後」シートのG列の値を消した行は、処理から除外されます。APIのクォータ(1日の上限)が切れても、翌日そこから簡単に再開できます。
  • クォータログ:今日のAPIで処理できる量を可視化したことで、本日あとどのくらい処理できるのか、翌日にしないといけないのかが一目で把握できます。
  • 削除・非表示動画をリストから一掃機能:削除された動画や非公開動画をリストから一括削除する機能も搭載しています。削除されていない動画でも再生リストから削除したい場合は、「再生リスト取得」シートの「ステータス」列を「削除済み」に変えて実行してください。



まとめ

YouTube公式機能では不可能な「AIによるシリーズ順・放送時期順の爆速整理」が、このツールなら手動並び替えの苦行をしなくても完結します。一度セットアップしてしまえば、あとはAIと対話して貼り付けるだけ。

あなたのアニソンリストやライブ予習リストを、ぜひ「理想の順序」に生まれ変わらせてみてください!