YouTubeに動画が投稿されたらDiscordに通知する。

VTuberなどで活動をしていると「YouTubeに自分の動画を投稿したらDiscordのチャンネルに自動的で通知して欲しいな」といった考えが及んでここを読んでいる人もいるかと思いますが、そういった人のために少しでもお役立てられたらいいなと思い手順を以下に示します。

なお、ここではGoogleのスプレッドシートを活用してGASと連携させたAPIスクリプトを用意します。

2024.11.28 - GASコードを修正しました!

YouTube Data APIのクォータ制限を超える事象を確認しましたので、ここで掲示しているGASコードを修正しています。参照された方はお手元のコードと照らし合わせて適宜修正ください。

YouTube Data APIには、1日の利用可能なクォータが設定されており、各リクエストに対してポイントが割り当てられています(例えば、youtube.search.listのリクエストは100ポイント消費します)。 

作業手順

手順 1「動画リストの累積先としてスプレッドシートを使用する」

まずは最初にYouTubeに投稿された動画情報をリスト化し蓄積するためのスプレッドシートをGoogleドライブの任意場所に作成します。このリストが無いと過去に投稿された内容かどうかGASが判断できなくなり、毎回決められた時刻または間隔でDiscordに投稿されてしまいます。

名前は「getYouTubeData」など分かりやすいタイトルで付けましょう。

シート名は「SentVideos_001」などで付けましょう。

SentVideos_001のシートの1行目に「VideoID」というヘッダーを追加します。 

手順 2Apps Script を作成する

先ほどの手順1で用意した動画リストのスプレッドシート「getYouTubeData」を開きメニューから「拡張機能 / Apps Script」を選択します。

手順 3コードの名前を付与

Apps Script が作成されたら「コード」のボタンから任意のタイトルを付けましょう。

例えば「getyoutube」などです。

手順4サービスを追加

YoutubeのAPIを呼び出すためサービス」のボタンから「YouTube Data API v3」を選択してサービスを追加しましょう。

手順5Discord にウェブフックを追加する

GASからDiscordに情報を投稿できるようウェブフックを作成しましょう。

Discordのサーバー設定から「連携サービス / ウェブフック」を開き「新しいウェブフック」を押します。すると「アイコン」「お名前」「チャンネル」がそれぞれ出てきますので、任意の設定と入力をしましょう。Botが自動的に入力していく事になりますので、チャンネルは通知Bot用などで用意してあげるとよいかもしれません。

一通り設定出来たら「ウェブフックURLをコピー」を押下してGASのコードに入力する準備をします。

手順6コードを入力する

GASに中核となるコードを作成しましょう。コードは以下に用意しました。

GAS コード

const channel = "ここに任意のチャンネルIDを記載する"// チャンネルID

const WEBHOOK_URL_ARRAY = [ "ここにDiscordのウェブフックを指定する" ]; // ウェブフックID


// YouTubeのデータ取得設定

function getYouTubeData() {

    var startTime = new Date();

    Logger.log(`Script started at: ${startTime}`);


    // YouTube Data API リストのgetリクエスト

    var results;

    try {

        results = YouTube.Search.list('id,snippet', {

            "maxResults": 3,                   // 取得上限数

            "channelId": channel,              // チャンネルID

            "order": "date",                   // 並び替え(日付順)

            "type": "video",                   // 取得する種類(video:動画, playlist:再生リスト, channel:チャンネル)

            "eventType": "none",               // 配信のタイプ(none:投稿済み,upcoming:配信予定,live:配信中)

        });

    } catch (e) {

        Logger.log(`Error fetching YouTube data: ${e.message}`);

        return; // エラーの場合は処理を中断

    }


    if (!results || !results.items || results.items.length === 0) {

        Logger.log(`No videos found or API response invalid.`);

        return; // データが空の場合は処理を中断

    }


    Logger.log(`YouTube API finished at: ${new Date().getTime() - startTime.getTime()}ms`);

    videoDataSet(results.items); // 取得したデータをvideoDataSet関数に渡す

}


function videoDataSet(items) {

    if (!items || items.length === 0) {

        Logger.log(`No items to process in videoDataSet.`);

        return; // データが空の場合は処理を中断

    }


    var sheet;

    try {

        sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('SentVideos_001');

    } catch (e) {

        Logger.log(`Error accessing the spreadsheet: ${e.message}`);

        return; // 処理を中断

    }


    if (!sheet) {

        try {

            sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet('SentVideos_001');

            sheet.appendRow(['VideoID']); // ヘッダーを追加

        } catch (e) {

            Logger.log(`Error creating the sheet: ${e.message}`);

            return; // 処理を中断

        }

    }


    var data = sheet.getRange("A2:A").getValues().flat();

    var sentVideoIds = new Set(data.filter(String)); // 送信済み動画IDのセット


    for (var i = 0; i < items.length; i++) {

        var item = items[i];

        var videoId = item.id.videoId;

        if (!sentVideoIds.has(videoId)) {

            const liveTitle = item.snippet.title;

            const liveUrl = `https://www.youtube.com/watch?v=${videoId}`;

            const payload = {

                "username": "YouTube",

                "content": `${liveTitle}\n${liveUrl}`

            };

            postDiscord(payload);            // 動画の詳細投稿

            sheet.appendRow([videoId]);      // スプレッドシートに動画IDを記録

        }

    }

}


// Discordへの投稿

function postDiscord(payload) {

  for (var i = 0; i < WEBHOOK_URL_ARRAY.length; i++) {

    var WEBHOOK_URL = WEBHOOK_URL_ARRAY[i];

    try {

      var options = {

        "method": "post",

        "contentType": "application/json",

        "payload": JSON.stringify(payload),

        "muteHttpExceptions": true

      };

      var response = UrlFetchApp.fetch(WEBHOOK_URL, options);

      var responseCode = response.getResponseCode();

      if (responseCode !== 204) {       // Discordは成功時にステータスコード204を返す

        Logger.log(`Error posting to Discord: ${responseCode} ${response.getContentText()}`);

      }

    } catch (e) {

      Logger.log(`Exception: ${e.message}`);

    }

  }

}

コードには3つの要素として「チャンネルID」と「ウェブフックID」、そして「シート名」を指定する必要があります。このコードではシート名が不明な場合には紐づけが出来るシートを自動で追加する機能を実装しています。

では早速ウェブフックIDをコードに追加しましょう。ウェブフックのIDについては、先ほどの手順5で取得できたかと思いますので、コードの2行目にウェブフックをペーストします。

YouTubeのIDの取得方法はYouTubeチャンネルの概要欄から「チャンネルを共有」のボタンを押下します。「チャンネルIDをコピー」が選択できるようになりますので押下して取得します。コピーができたらGASのコード入力に戻ってチャンネルIDをペーストします。
例えば、私のチャンネルIDであれば「UC7ig1kLH30YH7cEfxE83ZWA」というのがチャンネルIDになります。
コードの1行目にチャンネルIDをペーストしてください。

コードの入力が完了したら「プロジェクトの保存」をしてください。

手順7実行する」

ここまで入力出来たら実際に実行してテストしてみましょう。

実行が成功すると右の図のようにDiscordに投稿が成功したことが分かります。

手順8「チャンネルを自動確認させる

作成したAPIは実行したときのみに動作しますので、これを繰り返し確認させるためのトリガーを設定してあげましょう。

Apps Script のメニューから目覚まし時計のアイコン「トリガー」を押下。少しすると右下に「+トリガーを追加」というボタンを押下します。
そうすると様々な設定ができる項目が出てきます。例えば15分ごとに確認させたい場合は「時間ペースのトリガーのタイプを選択」から「分ペースのタイマー」を選び「時間の感覚を選択」から15分を選んでください。

このようにトリガーの設定は非常に手軽ですので、自分のチャンネルに合った使い方でDiscordを充実させていきましょう!

最後に「プロジェクトの保存」をしてください。

困ったら

Tips 1 : API の使用量に注意する。

YouTube Data APIは1日あたりの使用量は10,000まで利用できます。 

Tips 2 : スプレッドシート活用のため検索速度の悪化

ここで紹介している仕様ではスプレッドシートに情報が累積されていくため、長期間の運用や検索先のVideoIDが増えていくにつれて、追加されていくレコードが増えていきます。これによって動画情報の重複を検索する際に1件1件検索しています。
この場合ではVideoIdのカラムは配列に格納し、indexOfで検索することで処理を効率化することが出来ます。