【GAS】YouTube自動アップロード② APIを使用した公開予約とサムネイル設定方法

YouTube自動投稿 API

こんにちは!
おでぃーです。

前回の第1回では、Googleの無期限リフレッシュトークンやそれを使ったアクセストークンの取得方法を紹介しました。

今回はその続きである第2回!

ついに動画をアップロードする方法を紹介していきます。

第2回は、

  • 動画のアップロード
  • タイトル、説明文、タグ、カテゴリーの設定
  • 「子供向けではない」設定
  • 公開予約設定
  • サムネイル設定

について解説します。

裏話になりますが、、

無期限リフレッシュトークンもそうですが、動画アップロードや公開予約、サムネイル設定の1つ1つに関してのGASノウハウはゼロに等しいほど見当たりません。。(pythonならいくつかありましたが)

公式ドキュメントを読んでも、情報は足りないわ、分かりにくいわ。

なので私自身も実はかなり苦戦しました笑

だからこそ、この記事を読んでいただく方には同じような思いをしてほしくないと思っています。

誰でもYouTube投稿の自動化が実現できるので、ぜひ最後まで読んでやってみてくださいね!

ちなみに、スプレッドシートと連携した自動投稿ツールは別途販売しています。

「定期投稿を実現したいけどそこまで作るのはハードルが高い、、」

「すぐに使って自動化させたい!」

という方にはおすすめです!

この記事を書いた人
おでぃー

GASを使った業務自動化に関するテクニックを発信。
本業ではプロダクト開発でエンジニアのディレクションをしながら、副業にてブログ運営やツール・スキルの販売を実施。
ネット広告運用経験からSNS領域の自動化に注力。

おでぃーをフォローする

ファイルをGoogle Driveにアップロード

投稿に使用する動画ファイルとサムネイル画像ファイルは、Google Driveにアップロードしておきます。

GASでは、APIを通じて扱うファイルを同じGoogleサービスであるGoogle Driveから直接使用するかたちになっています。

スクリプト作成

GASエディタで新しく3つのgsファイルを作成し、下記のスクリプトをそれぞれコピペします。

function uploadVideo() {
  //アクセストークンを取得するための関数を実行
  var access_token = getAccessToken(); 
  
  //公開予約時間の整備
  var today = new Date();
  var hour = today.getHours();
  today.setHours(hour + 3);
  var iso8601 = Utilities.formatDate(today, 'UTC', "yyyy-MM-dd'T'HH:mm:ss.S'Z'");

  //アップロード情報を整備
  var title = 'タイトル'
  var description = '説明分';
  var tags = 'タグ'
  var categoryId = 'カテゴリーID' //カンマ区切り
  var fileToUpload = '動画ファイル名'
  var thumbnailName = 'サムネイル画像ファイル名' 

  //ファイル情報を整備
  var file = DriveApp.getFilesByName(fileToUpload).next();
  var fileSize = file.getSize();
  var fileSizeString = fileSize.toString();
  var fileBlob = file.getBlob();

  //アップロード情報リクエストの送信
  var payload = {
    "snippet": {
      "title": title,
      "description": description,
      "tags": [tags],
      "categoryId": categoryId
    },
    "status": {
      "publishAt": iso8601, //公開予約時間
      "privacyStatus": "private",
      "embeddable": true,
      "license": "youtube",
      'selfDeclaredMadeForKids': false //子供向けではない
    }
  };
  
  var payloadString = JSON.stringify(payload);

  var headers = {
    "Authorization": "Bearer " + access_token,
    "Content-Type": "application/json; charset=utf-8",
    "X-Upload-Content-Length": fileSizeString,
    "X-Upload-Content-Type": "application/octet-stream"
  };
  
  var options = {
    method: "post",
    contentType: "application/json",
    headers: headers,
    payload: payloadString,
    muteHttpExceptions: true
  };
  
  var url = "https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status,contentDetails";
  var response = UrlFetchApp.fetch(url, options);
  
  //レスポンスから動画アップロード用URLを取得
  var headers = response.getHeaders();
  var uploadUrl = headers['Location'];

  //動画サイズが50MB未満かどうかで処理を分岐
  var chunkSize =49 * 1024 * 1024;

  //50MB未満の場合
  if (fileSize <= chunkSize) {
    
    //動画アップロードリクエストを送信
    var headers = {
      "Authorization": "Bearer " + access_token,
      "Content-Type": "application/octet-stream"
    };
  
    var options = {
      method: "put",
      contentType: "application/octet-stream",
      headers: headers,
      payload: fileBlob
    };
    
    try {
      var response = UrlFetchApp.fetch(uploadUrl, options);
      Logger.log("Response: " + response.getContentText());
    } catch (e) {
      Logger.log("Failure");
      throw e;
    }
    
  //50MB以上の場合
  }else{

    //動画をチャンクに分けるための関数を実行
    var chunksBlobs = readInChunks(file);
    
    //リクエスト送信用にチャンクごとのサイズ数を整備
    var chunkNum  = Math.floor(fileSize / chunkSize);
    var lastChunk =  fileSize % chunkSize;
    var chunks = [];

    for(var i = 0; i < chunkNum; i++){
      chunks.push(chunkSize);
    }

    chunks.push(lastChunk);
      

    var firstByte = 0;
    var lastByte = 0;
    var totalByte = fileSize.toString();
    
    //チャンクごとに動画アップロードリクエストを送信
    for (var i = 0; i < chunks.length; i++) {
      var chunk = chunks[i];
      lastByte = firstByte + chunk - 1;

      Logger.log(firstByte.toString());
      Logger.log(lastByte.toString());
      Logger.log(totalByte);

      var headers = {
        "Authorization": "Bearer " + access_token,
        "Content-Range": "bytes " + firstByte.toString() + "-" + lastByte.toString() + "/" + totalByte,
        "Content-Type": "application/octet-stream"
      };

      var chunkBlob = chunksBlobs[i];
      
      var options = {
        method: "put",
        contentType: "application/octet-stream",
        headers: headers,
        payload: chunkBlob
      };
      
      try {
        var response = UrlFetchApp.fetch(uploadUrl, options);
        Logger.log("Response: " + response.getResponseCode());
        var headers = response.getHeaders();
        

      } catch (e) {
        Logger.log("Response: " + response.getResponseCode());
        var headers = response.getHeaders();
        Logger.log("OK");
        firstByte = lastByte + 1;
      }
    }
  }
    
  //ビデオIDを取得
  var videoId = JSON.parse(response.getContentText()).id;

  //サムネイル設定関数を実行
  if(thumbnailName){
    setVideoThumbnail(videoId , thumbnailName);
  }

  Logger.log("Success");
}
function readInChunks(file) {
  var fileSize = file.getSize();
  var chunk = 49 *1024 * 1024 
  var fileId = file.getId();

  var accessToken = ScriptApp.getOAuthToken();
  var baseUrl = "https://www.googleapis.com/drive/v3/files/";

  // チャンク数を数える
  var start = 0;
  var end = chunk -1;
  var loop = Math.floor(fileSize / chunk);
  loop = fileSize % chunk > 0 ? loop + 1 : loop;

  // チャンクにわける
  var chunkBlobs = [];
  var url2 = baseUrl + fileId + "?alt=media";
  for (var i = 0; i < loop; i++) {
    Logger.log(start.toString());
    Logger.log(end.toString());
    var headers = {
      Authorization: "Bearer " + accessToken,
      Range: "bytes=" + start + "-" + end,
    };
    var params2 = {
      method: "get",
      headers: headers,
      muteHttpExceptions: true,
    };
    var response = UrlFetchApp.fetch(url2, params2);
    var chunkBlob = response.getBlob();
    chunkBlobs.push(chunkBlob);

    start += chunk;

    if(i != loop-2){
      end = start + chunk- 1;
    }else{
      end = fileSize;
    }
  }
  return chunkBlobs;
}
function setVideoThumbnail(videoId , imageFileName) {
  var imageFile = DriveApp.getFilesByName(imageFileName).next();
  var thumbnailBlob = imageFile.getBlob();

 //サムネイル画像を設定
  var thumbnailUrl = "https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=" + videoId;

  var headers = {
    Authorization: "Bearer " + access_token,
    "Content-Type": thumbnailBlob.getContentType()
  };

  var options = {
    method: "POST",
    headers: headers,
    payload: thumbnailBlob.getBytes(),
    muteHttpExceptions: true
  };

  var response = UrlFetchApp.fetch(thumbnailUrl, options);
  Logger.log(response.getContentText());
}

そして、下記の部分にアップロード情報をそれぞれ設定します。

//アップロード情報を整備
  var title = 'タイトル'
  var description = '説明文';
  var tags = 'タグ' //カンマ区切り
  var categoryId = 'カテゴリーID' 
  var fileToUpload = '動画ファイル名'
  var thumbnailName = 'サムネイル画像ファイル名' 

その際、注意点が2点あります。

  • タグはカンマ区切りで設定します。
    例えば、「ビジネス」「金融」という2つのタグを設定したければ、’ビジネス,金融’のように設定します。
  • カテゴリーIDは動画カテゴリーを設定することができます。
    カテゴリーに対応するIDは下記の記事の「GASでスクリプトを記入」という項に対応表を記載しているので参考にしてください。

スクリプト実行

スクリプトは3つに分かれていますが、function uploadVideoのみを実行します。

他の2つはuploadVideoに組み込まれているため、必要なときに実行されます。

実行については下記の記事にて詳しく解説しているので、参考にしてください。

実際にYouTubeに動画がアップロードされているか、各設定がされているか確認してみましょう!

ちなみに、公開予約時間はuploadVideo実行時間の3時間後になるようになっています。

スクリプト解説

一連のアップロード処理には、再開可能なアップロードに記載されているAPIリクエストを使用しています。

全体の流れとしては、

  1. 動画のメタデータを送信
  2. レスポンスから動画アップロード用のURIを取得
  3. 取得したURIに対して動画を送信

になります。

3では、さらに動画サイズによってさらに2通りの分岐が発生します。

GASでは一度のリクエスト送信に50MBというサイズ制限があります。

そのため、通常のアップロード処理では50MB以上の動画をアップロードすることができません。

そこでドキュメントに記載されているように、1つの動画アップロードに対してリクエストを分割(チャンク化)させることで、50MB以上の動画であってもアップロードを実現させることができます。(ドキュメントのステップ4に該当)

もちろん50MB未満の動画であれば、チャンク化や分割アップロードすることなくアップロード可能です。(ドキュメントのステップ3に該当)

個人的には、チャンク化するのにかなり手こずりました。
(chatGPTに聞いても全然正しいコード教えてくれなかったので。。)

YouTube以外にも大きめのサイズのリクエスト送信が必要なときには使えるテクニックになり得ますね!

まとめ

今回はYouTube Data APIを使用したGASでの動画アップロード方法を解説しました。

これにスプレッドシート連携させて、タイトルやファイル名を順番に取得するような処理加え、トリガー設定で定期的に実行できれば、意外と手間のかかるアップロード作業を完全に自動化することができますね。

特に、ショート動画を毎日のように頻繁に投稿するアカウントにとっては必須級なのではないでしょうか。

ぜひ駆使して効率的なアカウント運営を目指しましょう!

コメント