GASでGoogle Drive動画をInstagramリールへ自動投稿!完全ガイド

Google Driveの動画をInstagramリールに自動投稿!GASとGraph APIで実現する効率化術

Instagramでのリール投稿は、エンゲージメントを高めるために非常に有効な手段です。しかし、動画ファイルを一つ一つ手作業でアップロードし、キャプションを付けて投稿するのは手間がかかる作業です。特に、複数のアカウントを運用していたり、定期的に大量のコンテンツを投稿する必要がある場合、その負担は無視できません。

この記事では、Google Apps Script (GAS) を使用して、Google Driveに保存した動画ファイルを自動的にInstagramリールとして投稿する仕組みを構築する方法を解説します。提供されたスクリプトコードを基に、その事前準備、コードの仕組み、実行方法、そして応用例までを網羅的にご紹介します。

この自動化ツールを活用すれば、コンテンツ作成と投稿作業を分離し、大幅な効率化を実現できます。ぜひ最後までご覧いただき、ご自身のInstagram運用にお役立てください。

目次

なぜこの自動化が必要なのか?目的とメリット

この仕組みの最大の目的は、Instagramリール投稿の自動化と効率化です。具体的には、以下のようなメリットが考えられます。

  • 作業時間の短縮: 動画ファイルをGoogle Driveに保存しておけば、あとはスクリプトが自動的に取得し、投稿処理を行ってくれます。手動でのアップロードやキャプション入力の手間が省けます。
  • 計画的な投稿: GASの時間ベーストリガーと組み合わせることで、特定の時間に自動投稿を行う「予約投稿」のような使い方が可能になります。
  • コンテンツの一元管理: 投稿したい動画を全てGoogle Driveの一箇所に集約して管理できます。
  • 定期的な投稿の促進: ルーチンワークとなる投稿作業を自動化することで、コンスタントなコンテンツ発信をサポートします。

特に、動画コンテンツを大量に扱うクリエイターや、複数のアカウントを運用するビジネスにとって、この自動化は強力な武器となるでしょう。

自動投稿の実現に必要なもの

この自動投稿システムを構築するためには、いくつかの準備が必要です。以下に必要なものをリストアップします。

  • Instagramビジネスアカウントまたはクリエイターアカウント: Instagram API(Graph API)を利用するには、これらのアカウントタイプが必須です。
  • 上記InstagramアカウントにリンクされたFacebookページ: Instagramアカウントの設定で、関連付けるFacebookページを選択します。Graph APIはこのFacebookページを経由してInstagramにアクセスします。
  • Meta開発者アカウントとMetaアプリ:
    • Meta for Developersサイトで開発者登録を行います。
    • Instagram Graph APIを利用するためのMetaアプリを作成します。
    • 作成したアプリに「Instagram Graph API」製品を追加し、必要な権限(instagram_content_publish, instagram_basic, pages_show_list, pages_read_engagementなど)を設定します。
  • Googleアカウント: Google Apps ScriptおよびGoogle Driveを利用します。
  • 投稿したい動画ファイル: Google Drive上に保存されている必要があります。Instagramリールは通常、MP4またはMOV形式が推奨されます。また、アスペクト比や長さにも推奨要件があります(垂直方向の9:16、最大90秒など)。

これらの事前準備を整えることで、GASからInstagram Graph APIを呼び出すための「鍵」と「接続先情報」が手に入ります。

最も重要な鍵!アクセストークンとアカウントIDの取得手順

GASスクリプトからInstagram Graph APIを利用するためには、「アクセストークン」(API利用権限を証明するもの)と「InstagramビジネスアカウントID」(どのInstagramアカウントに投稿するかを指定するもの)が必要です。これらはMetaのグラフAPIエクスプローラを使用して取得します。

ステップ1-1: グラフAPIエクスプローラを開く

Meta for Developers にアクセスし、グラフAPIエクスプローラを開きます。

ステップ1-2: Metaアプリを選択

画面右側の「Metaアプリ」ドロップダウンから、事前準備で作成したMetaアプリを選択します。

ステップ1-3: ユーザーアクセストークンを生成

  1. 「ユーザーまたはページ」で「トークンを取得」(または「ユーザーアクセストークンを取得」)を選択します。
  2. 「アクセス許可」セクションで、必要な権限を選択します。最低限、以下の権限は必須です。
    • instagram_content_publish (リール投稿に必要)
    • instagram_basic (基本的なアカウント情報取得に必要)
    • pages_show_list (連携しているFacebookページ一覧を取得に必要)
    • pages_read_engagement (ページに関連するエンゲージメント読み取りに必要 – ID取得に間接的に必要となる場合あり)
  3. 「アクセストークンを生成」(または「Generate Access Token」)ボタンをクリックします。
  4. Facebookの認証画面が表示されたら、ログインし、求められる権限をアプリに許可します。

成功すると、グラフAPIエクスプローラの上部にユーザーアクセストークンが表示されます。このトークンを安全な場所にコピーしておきます。これがGASスクリプトの USER_ACCESS_TOKEN になります。 このトークンには有効期限があるため、期限切れの場合は再取得が必要です。

ステップ1-4: FacebookページIDの特定

Instagramビジネス/クリエイターアカウントは、Facebookページと連携している必要があります。API経由での操作はこの連携したFacebookページIDを起点に行うため、そのIDを取得します。

  1. グラフAPIエクスプローラで、以下のリクエストを実行します。
    • HTTPメソッド: GET
    • リクエストURL: me/accounts
    • アクセストークン: ステップ1-3で取得したユーザーアクセストークンを使用します。
  2. 実行ボタンを押すと、レスポンスが表示されます。data 配列の中に、あなたが管理するFacebookページの一覧が表示されます。
  3. 投稿したいInstagramアカウントにリンクされているFacebookページの名前を確認し、そのページの id の値を控えます。これが後のリクエストで使用するFacebookページIDです。
// レスポンス例の一部
{
  "data": [
    {
      "name": "あなたのFacebookページ名",
      "id": "YOUR_FACEBOOK_PAGE_ID" // ★ このIDを控える ★
    }
    // ... 他のページ情報 ...
  ]
}

ステップ1-5: InstagramビジネスアカウントIDの取得

いよいよ、最終的な投稿先となるInstagramビジネスアカウントのIDを取得します。

  1. グラフAPIエクスプローラで、以下のリクエストを実行します。
    • HTTPメソッド: GET
    • リクエストURL: /{控えたFacebookページID}?fields=instagram_business_account (例: /YOUR_FACEBOOK_PAGE_ID?fields=instagram_business_account
    • アクセストークン: ステップ1-3で取得したユーザーアクセストークンを使用します。
  2. 実行ボタンを押すと、レスポンスが表示されます。
  3. レスポンス中の instagram_business_account オブジェクト内にある id の値を取得します。これがGASスクリプトの INSTAGRAM_BUSINESS_ACCOUNT_ID になります。
// レスポンス例
{
  "instagram_business_account": {
    "id": "YOUR_INSTAGRAM_BUSINESS_ACCOUNT_ID" // ★ このIDを取得! ★
  },
  "id": "YOUR_FACEBOOK_PAGE_ID"
}

これで、GASスクリプトで必要となる認証情報とアカウント識別情報が全て揃いました。これらのIDとトークンは機密情報として取り扱い、漏洩しないように注意してください。

投稿動画ファイルの準備

Google Drive上の動画ファイルは、Instagram Graph APIがインターネット経由でアクセスできる必要があります。以下の手順で準備します。

ステップ2-1: 動画ファイルのアップロード

投稿したい動画ファイル(MP4またはMOV形式推奨)をGoogle Driveの任意のフォルダにアップロードします。リールに適した形式や長さ、アスペクト比であることを確認しましょう。

ステップ2-2: フォルダIDの取得

アップロードした動画ファイルが保存されているGoogle Driveフォルダを開きます。ブラウザのアドレスバーに表示されているURLから、フォルダIDをコピーします。
例: https://drive.google.com/drive/folders/THIS_IS_THE_FOLDER_IDTHIS_IS_THE_FOLDER_ID の部分です。これがGASスクリプトの TARGET_GOOGLE_DRIVE_FOLDER_ID になります。

ステップ2-3: ファイルの共有設定 (非常に重要!)

このステップが最も重要です。GASスクリプトが取得した動画ファイルのURLは、Google Driveの認証を伴わない直接ダウンロードURL(webContentLink)となります。InstagramのサーバーはGoogleアカウントで認証できないため、このURLがインターネット上の誰でもアクセスできる状態である必要があります。

フォルダ内の投稿したい各動画ファイル、またはフォルダそのものに対して、「リンクを知っている全員が閲覧者」として共有設定を行います。

  1. ファイルまたはフォルダを選択します。
  2. 右クリックし、「共有」を選択します。
  3. 「リンクを取得」セクションで、「制限付き」となっている部分をクリックします。
  4. 表示されるドロップダウンから「リンクを知っている全員」を選択します。
  5. 権限が「閲覧者」になっていることを確認し、「完了」をクリックします。

注意: この設定を行うと、そのファイル(またはフォルダ内の全てのファイル)のURLを知っていれば誰でもファイルの内容を閲覧できるようになります。公開されても問題ない動画ファイルのみをこのフォルダに配置するようにしてください。

Google Apps Script (GAS) のセットアップ

事前準備とファイルの用意ができたら、いよいよGASプロジェクトを設定し、コードを貼り付けます。

ステップ3-1: 新しいGASプロジェクトを作成

Google Driveで「新規」 > 「その他」 > 「Google Apps Script」を選択して新しいプロジェクトを作成します。

ステップ3-2: Drive API拡張サービスを有効化

GASからGoogle Driveのファイル情報を取得するために、Drive APIサービスを有効にする必要があります。

  1. スクリプトエディタを開きます。
  2. 左側のメニューにある「サービス」の隣の「+」アイコンをクリックします。
  3. 表示されたリストから「Drive API」を選択します。
  4. 「追加」ボタンをクリックします。(識別子はデフォルトの Drive のままで構いません)

ステップ3-3: スクリプトコードの貼り付け

以下のGASプログラムコードを、新しく作成したGASプロジェクトのスクリプトエディタに貼り付けます。既存のコード(myFunctionなど)は削除して構いません。

// --------------------------------------------------------------------------------
// 設定項目: ここを編集してください
// --------------------------------------------------------------------------------

// 1. Facebookユーザーアクセストークン (ステップ1-3で取得したトークン)
const USER_ACCESS_TOKEN = "ここにステップ1-3で取得したユーザーアクセストークンを貼り付け";

// 2. 投稿対象のInstagramビジネスアカウントID (ステップ1-5で取得したID)
const INSTAGRAM_BUSINESS_ACCOUNT_ID = "ここにステップ1-5で取得したInstagramビジネスアカウントIDを貼り付け";

// 3. 投稿する動画が含まれるGoogle Drive上のフォルダID (ステップ2-2で取得したID)
const TARGET_GOOGLE_DRIVE_FOLDER_ID = "ここにGoogle DriveのフォルダIDを貼り付け";

// 4. リールのキャプション (投稿ごとに固定、または動画名などから動的に生成も可能)
const REEL_CAPTION = "GASからの自動投稿テスト! #リール #自動化";

// 5. フィードにも投稿するかどうか (true または false)
const SHARE_TO_FEED = true;

// 6. (任意) カスタムカバー画像の公開URL (指定しない場合は動画のフレームから自動選択)
// const COVER_IMAGE_URL = "https://example.com/your_public_cover_image.jpg";

// 7. (任意) サムネイルとして使用する動画の時点 (ミリ秒単位)。COVER_IMAGE_URLがない場合に考慮されます。
// const THUMB_OFFSET_MS = 1500; // 例: 1.5秒時点

// --------------------------------------------------------------------------------
// グローバル定数 (編集不要)
// --------------------------------------------------------------------------------
const FACEBOOK_GRAPH_API_VERSION = "v20.0"; // 安定しているAPIバージョンを推奨 (例: v19.0, v20.0, v21.0)
const GRAPH_API_BASE_URL = `https://graph.facebook.com/${FACEBOOK_GRAPH_API_VERSION}`;

// --------------------------------------------------------------------------------
// メイン関数: この関数を実行してリールを投稿します
// --------------------------------------------------------------------------------
function postReelFromGoogleDrive() {
  try {
    // 設定値のチェック
    if (USER_ACCESS_TOKEN === "ここにステップ1-3で取得したユーザーアクセストークンを貼り付け" || !USER_ACCESS_TOKEN) {
      Logger.log("エラー: `USER_ACCESS_TOKEN` を設定してください。");
      return;
    }
    if (INSTAGRAM_BUSINESS_ACCOUNT_ID === "ここにステップ1-5で取得したInstagramビジネスアカウントIDを貼り付け" || !INSTAGRAM_BUSINESS_ACCOUNT_ID) {
      Logger.log("エラー: `INSTAGRAM_BUSINESS_ACCOUNT_ID` を設定してください。");
      return;
    }
    if (TARGET_GOOGLE_DRIVE_FOLDER_ID === "ここにGoogle DriveのフォルダIDを貼り付け" || !TARGET_GOOGLE_DRIVE_FOLDER_ID) {
      Logger.log("エラー: `TARGET_GOOGLE_DRIVE_FOLDER_ID` を設定してください。");
      return;
    }

    Logger.log(`使用するInstagramアカウントID: ${INSTAGRAM_BUSINESS_ACCOUNT_ID}`);

    // 1. フォルダからランダムな動画ファイルIDを取得
    Logger.log(`フォルダID '${TARGET_GOOGLE_DRIVE_FOLDER_ID}' からランダムな動画ファイルを選択中...`);
    const selectedVideoFileId = getRandomVideoFileIdFromFolder(TARGET_GOOGLE_DRIVE_FOLDER_ID);

    if (!selectedVideoFileId) {
      Logger.log(`エラー: フォルダID '${TARGET_GOOGLE_DRIVE_FOLDER_ID}' 内に投稿可能な動画ファイルが見つかりませんでした。処理を終了します。`);
      return;
    }
    Logger.log(`ランダムに選択された動画ファイルID: ${selectedVideoFileId}`);


    // 2. Google Driveから動画の公開URLを取得
    Logger.log(`Google Driveから動画ファイル (ID: ${selectedVideoFileId}) の情報を取得中...`);
    let videoUrl;
    let videoFileName;
    try {
      const file = Drive.Files.get(selectedVideoFileId, { fields: "webContentLink, mimeType, name" });
      if (!file || !file.webContentLink) {
        throw new Error(`動画ファイル (ID: ${selectedVideoFileId}) の webContentLink が取得できませんでした。ファイルが存在し、「リンクを知っている全員が閲覧者」として共有されているか確認してください。`);
      }
      videoUrl = file.webContentLink;
      videoFileName = file.name;
      Logger.log(`動画ファイル名: ${videoFileName}`);
      Logger.log(`取得した動画URL: ${videoUrl}`);
      Logger.log(`動画MIMEタイプ: ${file.mimeType}`);

      if (file.mimeType !== "video/mp4" && file.mimeType !== "video/quicktime" && file.mimeType !== "video/mov") {
        Logger.log(`警告: 動画のMIMEタイプが ${file.mimeType} です。InstagramはMP4またはMOV形式を推奨しています。問題が発生する可能性があります。`);
      }
    } catch (e) {
      Logger.log(`Google Driveからの動画URL取得中にエラーが発生しました: ${e.message}`);
      Logger.log("ヒント: GASプロジェクトでDrive APIサービスが有効になっているか、ファイルIDが正しいか、ファイルが「リンクを知っている全員が閲覧者」として共有されているか確認してください。");
      return;
    }

    let dynamicCaption = REEL_CAPTION; // ここでは固定キャプションを使用。 `${videoFileName} を投稿!` なども可能。

    // 3. メディアコンテナを作成
    Logger.log("Instagramメディアコンテナを作成中...");
    const creationId = createInstagramMediaContainer(INSTAGRAM_BUSINESS_ACCOUNT_ID, USER_ACCESS_TOKEN, videoUrl, dynamicCaption);
    if (!creationId) {
      Logger.log("メディアコンテナの作成に失敗しました。ログを確認してください。");
      return;
    }
    Logger.log(`メディアコンテナID: ${creationId} を作成しました。`);

    // 4. アップロードステータスを確認 (ポーリング)
    Logger.log("動画のアップロードと処理ステータスを確認中 (最大約5分)...");
    const uploadSuccessful = checkUploadStatus(creationId, USER_ACCESS_TOKEN);
    if (!uploadSuccessful) {
      Logger.log("動画のアップロードまたは処理に失敗しました。Instagram側の処理に時間がかかっているか、動画に問題がある可能性があります。ログを確認してください。");
      return;
    }
    Logger.log("動画のアップロードと処理が正常に完了しました。");

    // 5. メディアを公開
    Logger.log("リール動画を公開中...");
    const publishedMediaId = publishInstagramReel(INSTAGRAM_BUSINESS_ACCOUNT_ID, USER_ACCESS_TOKEN, creationId);
    if (publishedMediaId) {
      Logger.log(`リール動画が正常に投稿されました! 公開メディアID: ${publishedMediaId}`);
      Logger.log(`投稿はInstagramアプリで確認してください。 (URL目安: https://www.instagram.com/reel/${publishedMediaId}/ )`);
    } else {
      Logger.log("リール動画の公開に失敗しました。ログを確認してください。");
    }

  } catch (error) {
    Logger.log(`スクリプト全体でエラーが発生しました: ${error.toString()}\nスタックトレース: ${error.stack || 'N/A'}`);
  }
}

/**
 * 指定されたGoogle Driveフォルダ内からランダムに動画ファイルIDを1つ取得します。
 */
function getRandomVideoFileIdFromFolder(folderId) {
  try {
    let videoFiles = [];
    let pageToken;
    const videoMimeTypes = ["video/mp4", "video/quicktime", "video/mov"]; // Instagramがサポートする動画形式
    const mimeTypeQuery = videoMimeTypes.map(mime => `mimeType='${mime}'`).join(' or ');

    // Drive API を使用してファイルリストを取得
    do {
      // フォルダ内の指定されたMIMEタイプのファイルかつゴミ箱に入っていないものをクエリ
      const query = `'${folderId}' in parents and (${mimeTypeQuery}) and trashed=false`;
      const response = Drive.Files.list({
        q: query,
        fields: "nextPageToken, files(id, name, mimeType)",
        pageToken: pageToken,
        pageSize: 100 // 一度に取得するファイル数
      });

      if (response.files && response.files.length > 0) {
        videoFiles = videoFiles.concat(response.files);
      }
      pageToken = response.nextPageToken;
    } while (pageToken && videoFiles.length < 1000); // ページネーション処理と、念のため取得件数に上限を設定

    if (videoFiles.length === 0) {
      Logger.log(`フォルダID '${folderId}' 内に対象の動画ファイル (${videoMimeTypes.join(', ')}) が見つかりませんでした。`);
      return null;
    }

    Logger.log(`フォルダ '${folderId}' 内に見つかった動画ファイル数: ${videoFiles.length}`);
    videoFiles.forEach(file => Logger.log(`  - ファイル名: ${file.name}, ID: ${file.id}, MIMEタイプ: ${file.mimeType}`));

    // 見つかったファイルの中からランダムに1つを選択
    const randomIndex = Math.floor(Math.random() * videoFiles.length);
    const selectedFile = videoFiles[randomIndex];

    Logger.log(`ランダムに選択された動画ファイル: ${selectedFile.name} (ID: ${selectedFile.id})`);
    return selectedFile.id;

  } catch (e) {
    Logger.log(`フォルダ (ID: ${folderId}) からの動画ファイルID取得中にエラーが発生しました: ${e.toString()}`);
    Logger.log(`スタックトレース: ${e.stack || 'N/A'}`);
    return null;
  }
}

/**
 * Instagramメディアコンテナを作成します。
 * Instagram Graph API の /media エンドポイントを使用します。
 * @param {string} igUserId InstagramビジネスアカウントID
 * @param {string} accessToken Facebookユーザーアクセストークン
 * @param {string} videoUrl Google Driveの動画公開URL
 * @param {string} caption リールのキャプション
 * @returns {string|null} 作成されたメディアコンテナID または null (エラー時)
 */
function createInstagramMediaContainer(igUserId, accessToken, videoUrl, caption) {
  const endpoint = `${GRAPH_API_BASE_URL}/${igUserId}/media`;
  const payload = {
    media_type: "REELS",
    video_url: videoUrl,
    caption: caption,
    share_to_feed: SHARE_TO_FEED, // フィードにもシェアするか
    access_token: accessToken
  };
  // オプションのカバー画像やサムネイルオフセットを追加
  if (typeof COVER_IMAGE_URL !== 'undefined' && COVER_IMAGE_URL) payload.cover_url = COVER_IMAGE_URL;
  else if (typeof THUMB_OFFSET_MS !== 'undefined' && THUMB_OFFSET_MS) payload.thumb_offset = THUMB_OFFSET_MS;

  const options = {
    method: "post",
    payload: payload,
    muteHttpExceptions: true // エラー時でもレスポンスを取得
  };

  try {
    Logger.log(`メディアコンテナ作成リクエスト: POST ${endpoint} Payload: ${JSON.stringify(payload, null, 2).replace(accessToken, "[ACCESS_TOKEN_REDACTED]")}`);
    const response = UrlFetchApp.fetch(endpoint, options);
    const responseCode = response.getResponseCode();
    const responseBody = response.getContentText();
    Logger.log(`メディアコンテナ作成レスポンス (${responseCode}): ${responseBody}`);

    if (responseCode === 200) {
      const jsonResponse = JSON.parse(responseBody);
      // レスポンスから作成されたコンテナIDを取得
      return jsonResponse.id;
    } else {
      // エラーが発生した場合、詳細をログ出力
      logApiError("メディアコンテナ作成エラー", responseBody);
      return null;
    }
  } catch (e) {
    Logger.log(`メディアコンテナ作成リクエスト中に例外が発生: ${e.message}`);
    return null;
  }
}

/**
 * メディアコンテナのアップロード/処理ステータスを確認します。
 * Instagram APIは非同期で動画を処理するため、公開前にステータス確認が必要です。
 * @param {string} creationId 確認するメディアコンテナID
 * @param {string} accessToken Facebookユーザーアクセストークン
 * @returns {boolean} 処理が成功したかどうか (FINISHED なら true)
 */
function checkUploadStatus(creationId, accessToken) {
  const endpoint = `${GRAPH_API_BASE_URL}/${creationId}`;
  const params = { fields: "status_code,status", access_token: accessToken };
  const MAX_RETRIES = 30; // 最大試行回数 (30回)
  const RETRY_INTERVAL_MS = 10000; // 試行間隔 (10秒) -> 合計で最大 30 * 10秒 = 300秒 (5分)

  for (let i = 0; i < MAX_RETRIES; i++) {
    Utilities.sleep(RETRY_INTERVAL_MS); // 指定時間待機

    const queryString = Object.keys(params).map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`).join('&');
    const requestUrl = `${endpoint}?${queryString}`;

    try {
      Logger.log(`ステータス確認リクエスト (${i + 1}/${MAX_RETRIES}): GET ${requestUrl.replace(accessToken, "[ACCESS_TOKEN_REDACTED]")}`);
      const response = UrlFetchApp.fetch(requestUrl, { muteHttpExceptions: true });
      const responseCode = response.getResponseCode();
      const responseBody = response.getContentText();
      Logger.log(`ステータス確認レスポンス (${responseCode}): ${responseBody}`);

      if (responseCode === 200) {
        const jsonResponse = JSON.parse(responseBody);
        const statusCode = jsonResponse.status_code;
        Logger.log(`現在のコンテナステータス: ${statusCode} (詳細: ${jsonResponse.status || 'N/A'})`);

        // ステータスコードを確認
        if (statusCode === "FINISHED") {
          // 処理完了
          return true;
        } else if (statusCode === "ERROR" || statusCode === "EXPIRED") {
          // エラーまたは期限切れ
          logApiError("コンテナステータスエラー", responseBody);
          return false;
        }
        // IN_PROGRESS や QUEUED の場合はループを続けて待機
      } else {
        // APIからのエラーレスポンス
        logApiError("ステータス確認APIエラー", responseBody);
        return false; // APIエラーの場合はリトライしても無駄なので終了
      }
    } catch (e) {
      // リクエスト自体に失敗した場合
      Logger.log(`ステータス確認リクエスト中に例外が発生: ${e.message}`);
      // 例外発生時はリトライせずに終了するか、例外の種類によってはリトライするか判断が必要だが、ここでは終了
      return false;
    }
  }

  // MAX_RETRIES 回試行しても FINISHED にならなかった場合
  Logger.log("ステータス確認がタイムアウトしました。コンテナ処理が完了しませんでした。");
  return false;
}

/**
 * 処理済みのメディアコンテナをInstagramに公開します。
 * Instagram Graph API の /media_publish エンドポイントを使用します。
 * @param {string} igUserId InstagramビジネスアカウントID
 * @param {string} accessToken Facebookユーザーアクセストークン
 * @param {string} creationId 公開するメディアコンテナID
 * @returns {string|null} 公開されたメディアのID (リールID) または null (エラー時)
 */
function publishInstagramReel(igUserId, accessToken, creationId) {
  const endpoint = `${GRAPH_API_BASE_URL}/${igUserId}/media_publish`;
  const payload = {
    creation_id: creationId, // 公開したいメディアコンテナのIDを指定
    access_token: accessToken
  };
  const options = {
    method: "post",
    payload: payload,
    muteHttpExceptions: true
  };

  try {
    Logger.log(`リール公開リクエスト: POST ${endpoint} Payload: ${JSON.stringify(payload).replace(accessToken, "[ACCESS_TOKEN_REDACTED]")}`);
    const response = UrlFetchApp.fetch(endpoint, options);
    const responseCode = response.getResponseCode();
    const responseBody = response.getContentText();
    Logger.log(`リール公開レスポンス (${responseCode}): ${responseBody}`);

    if (responseCode === 200) {
      const jsonResponse = JSON.parse(responseBody);
      // 公開成功時、投稿されたメディアのIDが返される
      return jsonResponse.id;
    } else {
      logApiError("リール公開エラー", responseBody);
      return null;
    }
  } catch (e) {
    Logger.log(`リール公開リクエスト中に例外が発生: ${e.message}`);
    return null;
  }
}

/**
 * APIエラーレスポンスを詳細にログ出力するためのヘルパー関数です。
 * @param {string} context エラーが発生した処理のコンテキスト (例: "メディアコンテナ作成エラー")
 * @param {string} responseBody APIからのレスポンスボディ (テキスト形式)
 */
function logApiError(context, responseBody) {
  Logger.log(`${context}. レスポンスボディ: ${responseBody}`);
  try {
    const errorJson = JSON.parse(responseBody);
    if (errorJson.error) {
      const err = errorJson.error;
      Logger.log(`  API エラーメッセージ: ${err.message || 'N/A'}`);
      Logger.log(`  API エラータイプ: ${err.type || 'N/A'}`);
      Logger.log(`  API エラーコード: ${err.code || 'N/A'}`);
      Logger.log(`  API エラーサブコード: ${err.error_subcode || 'N/A'}`);
      Logger.log(`  API fbtrace_id: ${err.fbtrace_id || 'N/A'}`); // Metaサポートへの問い合わせに役立つID
    }
  } catch (e) {
    Logger.log(`  レスポンスボディのJSONパースに失敗しました。`);
  }
}

ステップ3-4: 設定項目を編集

貼り付けたコードの上部にある以下の設定項目を、ステップ1およびステップ2で取得したご自身の情報に置き換えます。

const USER_ACCESS_TOKEN = "ここにステップ1-3で取得したユーザーアクセストークンを貼り付け";
const INSTAGRAM_BUSINESS_ACCOUNT_ID = "ここにステップ1-5で取得したInstagramビジネスアカウントIDを貼り付け";
const TARGET_GOOGLE_DRIVE_FOLDER_ID = "ここにGoogle DriveのフォルダIDを貼り付け";

REEL_CAPTIONSHARE_TO_FEED、オプションの COVER_IMAGE_URL, THUMB_OFFSET_MS も必要に応じて調整してください。

ステップ3-5: 保存

スクリプトを保存します(ファイル > 保存、または Ctrl+S / Cmd+S)。プロジェクト名を分かりやすいものに変更しておくと良いでしょう。

コード詳細解説

提供されたGASスクリプトは、Instagram Graph APIを利用してリールを投稿するための一連の処理を実行します。主要な関数ごとにその役割とロジックを見ていきましょう。

メイン関数: postReelFromGoogleDrive()

この関数がスクリプトのエントリーポイントです。リール投稿全体の流れを制御します。

  1. 設定値の確認: 最初に、必須の設定項目(USER_ACCESS_TOKEN, INSTAGRAM_BUSINESS_ACCOUNT_ID, TARGET_GOOGLE_DRIVE_FOLDER_ID)が正しく設定されているかを確認します。未設定の場合はエラーメッセージを出力して処理を終了します。
  2. 動画ファイルの選択: getRandomVideoFileIdFromFolder 関数を呼び出し、指定されたGoogle Driveフォルダから投稿する動画ファイルをランダムに1つ選択します。
  3. 動画URLの取得: 選択された動画ファイルのIDを使用して、Google Drive API(Drive.Files.get)からそのファイルの公開URL(webContentLink)を取得します。ファイルが存在しない場合や、共有設定が正しくない場合はエラーとなります。
  4. メディアコンテナの作成: createInstagramMediaContainer 関数を呼び出し、取得した動画URLとキャプション、その他の設定(フィードシェア、カバー画像など)を指定して、Instagram Graph APIにメディアコンテナの作成をリクエストします。この時点ではまだ投稿されず、Instagram側で動画ファイルのダウンロードと処理が始まります。
  5. アップロードステータスの確認: checkUploadStatus 関数を呼び出し、作成したメディアコンテナの処理ステータスが完了するまでポーリング(定期的な確認)を行います。Instagram側での動画処理には時間がかかることがあるため、この待機処理が必要です。処理失敗やタイムアウトの場合はエラーとなります。
  6. メディアの公開: 動画処理が正常に完了したら、publishInstagramReel 関数を呼び出し、処理済みのメディアコンテナを指定してInstagram Graph APIに公開をリクエストします。
  7. 結果のログ出力: 投稿が成功したか、または失敗したかの結果をGASのログに出力します。成功した場合は、投稿されたリールのメディアIDも表示されます。

エラーが発生した場合は、try...catch ブロックで捕捉し、詳細なエラー情報をログに出力するようになっています。

動画ファイル取得関数: getRandomVideoFileIdFromFolder(folderId)

この関数は、指定されたGoogle Driveフォルダ内から投稿に適した動画ファイル(MP4, MOV)をランダムに1つ選び出す役割を担います。

  • Drive.Files.list() メソッドを使用しています。これはGoogle Drive APIの一部で、GASの「サービス」でDrive APIを有効にすることで利用可能になります。
  • q パラメータで検索クエリを指定しています。'${folderId}' in parents で指定フォルダ内を検索し、(${videoMimeTypes.join(' or ')}) でMP4またはMOV形式のファイルをフィルタリングしています。trashed=false はゴミ箱に入っていないファイルを対象とします。
  • fields パラメータで取得するファイル情報(id, name, mimeType)を絞り込むことで、不要な情報取得を避け、処理を効率化しています。
  • ファイルが多数ある場合に備え、nextPageToken を使用したページネーション処理を行っています。
  • 取得した動画ファイルのリストから Math.random() を使ってランダムに1つを選択し、そのIDを返します。

API連携関数: createInstagramMediaContainer, checkUploadStatus, publishInstagramReel, logApiError

これらの関数は、Instagram Graph APIとの通信を担当します。GASの UrlFetchApp サービスを使用して、HTTPリクエスト(POSTやGET)を送信し、レスポンスを処理します。

  • createInstagramMediaContainer: /media エンドポイントに対し、動画URLやキャプションなどのパラメータを付けてPOSTリクエストを送信します。成功すると、後続の処理で必要となる「メディアコンテナID」がレスポンスとして返されます。
  • checkUploadStatus: メディアコンテナIDに対し、/creation_id エンドポイントにGETリクエストを繰り返し送信し、ステータスを確認します。status_code"FINISHED" になるのを待ちます。Utilities.sleep() は、APIへの連続アクセスを避け、サーバーに負荷をかけすぎないようにするための待機処理です。Instagram側での動画処理時間は動画のサイズなどによって変動するため、ある程度の待機とポーリングが必要になります。
  • publishInstagramReel: 処理が完了したメディアコンテナIDを /media_publish エンドポイントにPOSTリクエストで送信し、リールとして公開します。成功すると、公開されたリールのメディアIDが返されます。
  • logApiError: APIからのレスポンスでエラーが発生した場合に、レスポンスボディの詳細(エラーメッセージ、エラーコードなど)をログに出力するための補助関数です。トラブルシューティングの際に非常に役立ちます。

これらの関数では、APIのバージョン (FACEBOOK_GRAPH_API_VERSION) を指定しており、Metaのドキュメントで推奨される安定版を使用することが推奨されます。

スクリプトの実行と権限承認

コードの設定が完了したら、実際にスクリプトを実行してみましょう。

ステップ4-1: 関数を選択して実行

  1. GASスクリプトエディタの上部にある関数選択ドロップダウンから、postReelFromGoogleDrive 関数を選択します。
  2. 実行ボタン(▶アイコン)をクリックします。

ステップ4-2: 権限の承認 (初回実行時)

スクリプトがGoogle Driveのファイルにアクセスしたり、外部サービス(Instagram Graph API)に接続したりするには、ユーザーの許可が必要です。

  • 初回実行時には、「承認が必要です」というダイアログが表示されます。
  • 内容を確認し、「許可を確認」ボタンをクリックします。
  • ご自身のGoogleアカウントを選択します。
  • 「Googleが確認していません」といった警告が表示されることがありますが、「詳細を表示」または「安全ではないページに移動」などをクリックして進みます。(ご自身で作成・確認したスクリプトであれば問題ありません)
  • スクリプトが要求する権限(Google Driveへのアクセス、外部サービスへの接続など)の内容を確認し、「許可」ボタンをクリックします。

これで、スクリプトが実行されるための権限が付与されます。

ステップ4-3: 実行ログの確認

スクリプトが実行されている間、または完了した後に、その処理の経過を確認できます。

  • スクリプトエディタのメニューから「表示」 > 「ログ」(または「実行ログ」)を選択します。
  • スクリプト内で Logger.log() で出力されたメッセージが表示されます。処理の開始、各ステップの状況、取得したID、APIレスポンス、エラーメッセージなどが確認できます。

ログを確認しながら、スクリプトが意図した通りに動作しているか、またはどこで問題が発生しているかを把握することができます。

困ったときは?トラブルシューティング

スクリプトの実行中に問題が発生する可能性はゼロではありません。ここでは、よくあるトラブルとその原因、対処法をいくつか紹介します。

  • 「承認が必要です」エラーが繰り返し表示される: 権限承認が正しく完了していないか、実行しようとしているアカウントと権限を付与したアカウントが異なる可能性があります。ログアウト・ログインを試すか、再度権限承認プロセスを最初からやり直してみてください。
  • API関連のエラーログが出力される (レスポンスコードが200以外):
    • アクセストークンの有効期限切れ: ユーザーアクセストークンには有効期限があります(短期トークンは1時間、長期トークンは約60日)。期限が切れた場合は、ステップ1-3の手順でグラフAPIエクスプローラから新しいトークンを再生成し、GASスクリプトの設定を更新してください。
    • 必要な権限がない: Metaアプリやアクセストークン生成時に、必要な権限(instagram_content_publishなど)が正しく選択されていなかった可能性があります。Meta for Developersのアプリ設定や、グラフAPIエクスプローラでのトークン再生成時に権限を再確認してください。
    • Instagram側の問題: 投稿する動画がInstagramリールの要件(アスペクト比、長さ、ファイル形式、サイズなど)を満たしていない場合、API側で処理に失敗します。Metaの公式ドキュメントでリールの要件を確認してください。また、稀にInstagram側のAPIに一時的な問題が発生している可能性もあります。
    • アカウントの制限: Instagramアカウント自体に投稿制限がかかっている場合や、API経由の投稿がブロックされている場合もエラーとなります。Instagramアプリで直接投稿できるか確認してみてください。
    • APIバージョンの問題: FACEBOOK_GRAPH_API_VERSION に指定したバージョンが古すぎるか、逆に最新すぎて不安定な可能性があります。Metaのドキュメントで推奨されている安定版バージョンを確認し、スクリプトのバージョンを更新してみてください。
    • fbtrace_id がログに含まれている場合: これはMeta側でリクエストを追跡するためのIDです。APIのエラーメッセージに含まれている場合、Metaの開発者サポートに問い合わせる際にこのIDを提供すると、原因特定の助けになることがあります。
  • 「動画ファイルが見つかりませんでした」エラー:
    • TARGET_GOOGLE_DRIVE_FOLDER_ID が間違っている可能性があります。ブラウザのアドレスバーから正確なフォルダIDをコピーしたか確認してください。
    • 指定したフォルダ内に、Instagramリールとして投稿可能な動画ファイル(MP4またはMOV形式)が本当に存在するか確認してください。
  • 「webContentLink が取得できませんでした」エラー:
    • GASプロジェクトでDrive APIサービスが有効になっていない可能性があります。ステップ3-2を確認してください。
    • 最も可能性が高い原因は、動画ファイルの「リンクを知っている全員が閲覧者」という共有設定ができていないことです。ステップ2-3を再確認し、各ファイルまたはフォルダの共有設定を正しく行ってください。

エラーログを注意深く読むことが、問題解決への第一歩です。特にAPIからのレスポンスボディには、エラーの原因に関する詳細な情報が含まれていることが多いです。

さらなる応用に向けて

提供されたスクリプトは、Google Driveのフォルダからランダムに動画を選択して投稿する基本的な機能を提供します。しかし、GASの柔軟性を活かせば、さらに様々な応用が可能です。

  • 定期的な自動実行: GASのトリガー機能を使えば、このスクリプトを毎日特定の時間に自動実行するように設定できます。これにより、完全に手を離れた予約投稿システムを構築できます。
  • 投稿する動画の選択ロジックの変更: ランダムに選ぶのではなく、特定のファイル名パターンに一致するファイルを選択したり、ファイル名や更新日時順に古いものから順に投稿したり、あるいはGoogleスプレッドシートに投稿キューを作成して、そこにリストアップされたファイルIDを指定して投稿するといった高度な制御も可能です。
  • キャプションの動的な生成: 現在は固定キャプションですが、動画ファイル名の一部をキャプションに含めたり、スプレッドシートで動画ファイルとキャプションを紐づけて管理し、そこから取得して設定したりすることもできます。
  • 投稿完了後の処理: 投稿が成功した後に、スプレッドシートに投稿履歴を記録したり、Slackやメールで通知を送信したりすることも可能です。これにより、自動化された投稿の状況を簡単に把握できます。

これらの応用を実現するには、Google スプレッドシートサービスや他のGASサービス(MailApp, SlackAppなど、SlackはGASの組み込みサービスではないため外部ライブラリ等が必要な場合あり)の使い方、さらには複雑なデータ構造の扱いなどを学ぶ必要があります。

まとめ

この記事では、Google Apps ScriptとInstagram Graph APIを連携させ、Google Drive上の動画ファイルをInstagramリールとして自動投稿する仕組みの構築方法を解説しました。事前準備からAPIキーの取得、GASプロジェクトのセットアップ、コードの解説、実行方法、そしてトラブルシューティングまで、一連の流れを詳細に見てきました。

この自動化によって、これまで手作業で行っていたリール投稿の負担が大幅に軽減され、コンテンツ制作や他のマーケティング活動により時間を割けるようになります。

また、今回のスクリプトはGASと外部API連携の具体的な学習例としても非常に有用です。UrlFetchApp を使ったHTTPリクエストの送信、JSON形式のデータの解析、APIドキュメントの読み方、エラーハンドリング、そしてGoogleサービス(Drive)との連携など、クラウドプラットフォーム上での自動化開発に必要な多くの要素が含まれています。

ぜひ、このガイドを参考に、ご自身のInstagram運用を効率化し、さらに進んだGAS開発に挑戦してみてください。自動化の可能性は無限大です。

撮影に使用している機材【PR】

【無料】撮った写真でWEBページを作りませんか?

この記事が気に入ったら
フォローしてね!

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