見出し画像

リスト化されたチャンネルの過去動画をリスト化

いきなりマトリョーシカみたいなこと言い始めていますが、要は切り抜きしたいストリーマーさんの過去動画をリストアップしようってことです。

前提として前回の記事で作成したリストを利用しますので、自動化したい方は前回の記事から追ってくださいませ。


利用ツール

利用するツールは以下のツールです。

  • GAS(GoogleAppsScript)

  • Notion(とNotionAPI)

  • スプレッドシート

利用方法

前回の記事でスプレッドシートに切り抜きしたい以下の内容をまとめたリストを作成しました。

  • チャンネル名(正式名称)

  • ストリーマーさんのチャンネルURL

  • こちらがアップロードする切り抜きチャンネルURL

  • 今後様々なファイルを保存するためのフォルダURL

前回のGoogleFormでいうところのスクリプトエディタは「Apps Script」になります。

プログラムコード

今回は最低限のリスト化するだけのプログラムを記載します。
今後の機能追加でプログラム追加する可能性はありますが、一旦は今回だけのプログラムで十分だと思います。

以下のプログラムをコピペして変更点・追加は各々変更してください

/**
 * 一番最初に動く関数
 */
function make_stream_data_main() {
  let const_values = new Master_Values()
  /**
   * スプレッドシートに記載されている内容を取得し、最新の内容(最終行)の動画チャンネルリンクから動画情報を取得する。
   * 取得した動画内容の数だけNotionとGoogleDriveに情報を追加する。
   */
  let sheet_range = get_sheet_range(const_values.sheet_id, const_values.sheet_name);
  if (sheet_range.length <= 1) {
    return 0;
  }

  /**
   * スプレッドシートでリスト化されているチャンネルをリストの上から順番に下記の内容を実行する
   * ・Notionのストリーム一覧に追加されている内容を取得する
   * ・実際にYoutubeにあるストリームの履歴を取得する
   * ・Notionに追加されていないストリームがあればデータとして追加する
   */
  for (let i = 1; i < sheet_range.length; i++) {
    let youtube_channel_name = sheet_range[i][0]

    let stream_notion_data = get_notion_by_gas_array(const_values.notion_api_key, const_values.stream_DB_id, youtube_channel_name)
    let videos
    videos = make_stream_datas(stream_notion_data, sheet_range[i])
    if (videos.length > 0) {
      meke_event_for_any_service(videos, sheet_range[i], sheet_range[i][3])
    }
  }
}
/**
 * スプレッドシートIDとシート名が与えられる。
 * そのシートの最終行と最終列から記載されているすべての範囲を二次元配列にして返す。
 */
function get_sheet_range(sheet_id, sheet_name) {
  let sheet = SpreadsheetApp.openById(sheet_id).getSheetByName(sheet_name);

  let last_column = sheet.getLastColumn();
  let last_row = sheet.getLastRow();

  let sheet_range = sheet.getRange(1, 1, last_row, last_column).getValues();
  return sheet_range;
}

function make_stream_datas(notion_datas, sheet_range) {
  let const_values = new Master_Values()
  /**
   * 特定のチャンネルで取得したNotionのデータが一つ以上存在するか確認する。
   * (1つも無ければ新しく作業が開始されるため)
   */
  if (notion_datas.length > 0) {
    /**
     * Notionにデータが一つ以上存在するとき、配信日で要素が含まれる列をNotionの二次元配列から特定する。
     * Notionのデータを取得する際に配信日でソートしているので、最初のデータの日付がNotionに登録されている日付の中でも最新のものである。
     * Youtubeで処理する際にiSO8601形式での日付にする必要があるので変更する
     */
    let streaming_notion_num_time = search_sequence_num_in_array(notion_datas[0], const_values.stream_day_word)

    let notion_serch_word_video_url = search_sequence_num_in_array(notion_datas[0], const_values.notion_video_url_word)

    let channel_streaming_day = notion_datas[1][streaming_notion_num_time]
    channel_streaming_day = change_iso8601_day(channel_streaming_day)

    /**
     * チャンネルのリンクと先ほど取得した最新の日付からそれ以降に実施されたストリーム情報を取得する。
     * 取得した内容が問題なく追加できるか確認し、情報追加する
     */
    let videos = get_channel_videos_main(sheet_range[1], channel_streaming_day);
    videos = check_duplication(videos, notion_datas, notion_serch_word_video_url)
    return videos
  } else {
    /**
     * Notionにデータが存在しない時、2000/01/01以降にされたストリームをYoutubeから取得する
     * (2000/01/01にはYoutubeが存在していなかったことから特定のチャンネルの動画を古い物から取得することに同じこと)
     */
    let channel_streaming_day = "2000-01-01T23:59:59Z"
    let videos = get_channel_videos_main(sheet_range[1], channel_streaming_day);
    return videos
  }
}

function search_sequence_num_in_array(target_array, match_word) {
  /**
   * 配列の中から検索したい完全一致ワードが存在しないか確認する。
   * 存在しない場合-1を返す
   */
  let return_num = -1
  for (let i = 0; i < target_array.length; i++) {
    if (target_array[i] == match_word) {
      return_num = i
      break
    }
  }
  return return_num
}

function change_iso8601_day(day) {
  /**
   * 日付をiso8601形式に変更する
   */
  let jst_time = new Date(day);
  return jst_time;
}

function check_duplication(videos, notion_data, check_num) {
  /**
   * ストリームが終了直後などは時間のデータ部分に誤差が発生し、追加済みのデータがNotionに再追加されることがあるので確認を行う。
   */
  let return_array = []
  for (let i = 0; i < videos.length; i++) {
    for (let j = 0; j < notion_data.length; j++) {
      if (videos[i][1] == notion_data[j][check_num]) {
        break
      }
      if (j == notion_data.length - 1) {
        return_array.push(videos[i]);
      }
    }
  }
  return return_array
}

function meke_event_for_any_service(videos, youtuber_info, start_folder_url) {
  /**
   * 与えられた動画情報の個数分プログラムを動かす。
   * 動画情報の中から実際の配信開始時間の年・月・日を取得する。
   * 取得した日付のフォルダをGoogleDriveに作成する。
   * 時間のフォルダに新規でスプレッドシートを作成し、そのスプレッドシートのURLを取得する。
   * 最後に時間のフォルダのURLと、スプレッドシートのURL、動画情報とチャンネル主の情報をNotionに追加するために関数add_notion_stream_pageに与える。
   */
  for (let i = 0; i < videos.length; i++) {
    let streaming_year = Utilities.formatDate(new Date(videos[i][2]), "JST", "yyyy")
    let streaming_month = Utilities.formatDate(new Date(videos[i][2]), "JST", "MM")
    let streaming_day = Utilities.formatDate(new Date(videos[i][2]), "JST", "dd")
    let year_folder = search_or_make_folder(streaming_year, start_folder_url)
    let month_folder = search_or_make_folder(streaming_month, year_folder)
    let day_folder = search_or_make_folder(streaming_day, month_folder)
    let new_spreadsheet = make_sheet_url(videos[i], day_folder)
    add_notion_stream_page(videos[i], youtuber_info, new_spreadsheet, day_folder);
  }
}

function search_or_make_folder(name, folder_url) {
  /**
   * 作成するフォルダの名称と作成する親フォルダのURLが与えられる。
   * フォルダのURLからフォルダIDを取得する。
   * 取得したフォルダに既に作成するフォルダの名称が存在していれば既存のフォルダのURLを返す。
   * まだ作成されていなかったらその名称で新しくフォルダを作成する。
   * そして作成されたフォルダのURLを返す
   */
  let folder_id = folder_url.split('/folders/')[1];
  let master_folder = DriveApp.getFolderById(folder_id)
  let made_folder = master_folder.getFoldersByName(name);
  if (made_folder.hasNext()) {
    let folder_url = made_folder.next().getUrl();
    return folder_url
  } else {
    master_folder.createFolder(name);
    made_folder = master_folder.getFoldersByName(name);
    let folder_url = made_folder.next().getUrl();
    return folder_url
  }
}


function make_sheet_url(video_info, drive_url) {
  let const_values = new Master_Values()
  /**
   * 作成するシートの名称と文フォルダのURLが与えられる。
   * 与えられたURLからフォルダのIDを取得する。
   * 取得したフォルダの中に作成する予定の名称のスプレッドシートが存在するかを確認する。
   * すでに作成されていた場合、URLを返す。
   * まだ作成されていなかった場合、その名称で新しくスプレッドシートを作成する。
   * 作成されたスプレッドシートのURLを返す
   */
  let sheet_name = video_info[0]
  let folder_id = drive_url.split('/folders/')[1];
  let master_folder = DriveApp.getFolderById(folder_id);
  let search_file = master_folder.getFilesByName(sheet_name);
  if (search_file.hasNext()) {
    let sheet_url = search_file.next().getUrl();
    return sheet_url;
  } else {
    let sheet = SpreadsheetApp.create(sheet_name);
    sheet.addEditor(const_values.add_mail_address);
    let sheet_url = sheet.getUrl();
    let sheet_id = sheet.getId();

    let file = DriveApp.getFileById(sheet_id);
    file.moveTo(master_folder);

    return sheet_url;
  }
}
function get_notion_by_gas_array(token, db_id, chennel_name) {
  /**
   * NotionAPIを利用してNotioinのDB情報をJSON形式で取得する
   * 取得後はGASで利用しやすいように二次元配列に書き換える
   */
  let notion_db_content_by_JSON = get_notion_data(token, db_id, chennel_name);
  let notion_db_content_by_gas = tlanslate_values_for_DB(notion_db_content_by_JSON);

  return notion_db_content_by_gas;
}

function get_notion_data(token, db_id, chennel_name) {
  /**
   * NotionAPIを利用し、指定したDBの情報を取得する。
   * 情報の取得は一回で取得できる上限が決まっているので、それ以上に存在している場合繰り返し処理で取得する
   */
  const get_notion_url = 'https://api.notion.com/v1/databases/' + db_id + '/query';
  let headers = {
    'content-type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer ' + token,
    'Notion-Version': '2021-08-16',
  };

  let notion_database_data_sheed = [];
  let check_next_cursor = undefined;
  let check_has_more = true;
  while (check_has_more) {
    let payload = {
      'start_cursor': check_next_cursor,
      "page_size": 50,
      "filter": {
        "property": "配信者名",
        "select": {
          "equals": chennel_name
        }
      },
      "sorts": [
        {
          "property": "配信日",
          "direction": "descending"
        }
      ]
    }

    let options = {
      'method': 'post',
      'headers': headers,
      // "muteHttpExceptions": true,
      "payload": JSON.stringify(payload),
    };
    let check_error = 0;
    let responce
    let test
    try {
      check_error++;
      responce = UrlFetchApp.fetch(get_notion_url, options);
      test = JSON.parse(responce);
    } catch (e) {
      check_error = 0;
    }
    check_has_more = test.has_more
    check_next_cursor = test.next_cursor
    notion_database_data_sheed = notion_database_data_sheed.concat(test);
  }

  return notion_database_data_sheed;
}

function tlanslate_values_for_DB(responce) {
  /**
   * NotionのDBの各要素を二次元配列にする。
   * 各要素はGASで扱いやすいように加工し、各ページのページIDを先頭に付随させておく
   */
  let return_array = [];
  if (responce[0].results.length == 0) {
    return 0
  }
  let property_array = Object.keys(responce[0].results[0].properties);
  property_array.push("page_id");
  return_array.push(property_array);

  for (let i = 0; i < responce.length; i++) {
    for (let j = 0; j < responce[i].results.length; j++) {
      let return_sheed_array = [];
      for (let k = 0; k < property_array.length - 1; k++) {
        let string = property_array[k];
        return_sheed_array.push(translate_notion_data_for_DB(responce[i].results[j].properties[string]));
      }
      return_sheed_array.push(responce[i].results[j].id);
      return_array.push(return_sheed_array);
    }
  }
  return return_array;
}

function translate_notion_data_for_DB(data) {
  /**
   * Notionのプロパティごとに必要なデータを取得する
   */
  if (data.length > 0) {
    data = data[0];
  }
  if (data.type === "checkbox") {
    return data.checkbox;
  } else if (data.type === "created_by") {
    return data.created_by.name;
  } else if (data.type === "created_time") {
    let return_created_time = Utilities.formatDate(new Date(data.created_time), "JST", "yyyy-MM-dd HH:mm:ss");
    return return_created_time;
  } else if (data.type === "date") {
    if (data.date != null) {
      return data.date.start;
    } else {
      return data.date;
    }
  } else if (data.type === "email") {
    return data.email;
  } else if (data.type === "files") {
    return data.files;
  } else if (data.type === "formula") {
    let data_formula_type = data.formula.type;
    if (data_formula_type === "date" && data.formula[data_formula_type] != null) {
      return data.formula[data_formula_type].start;
    } else {
      return data.formula[data_formula_type];
    }
  } else if (data.type === "last_edited_by") {
    return data.last_edited_by;
  } else if (data.type === "last_edited_time") {
    return data.last_edited_time;
  } else if (data.type === "multi_select") {
    if (data.multi_select.length > 0) {
      let data_multi_select_array = [];
      for (let i = 0; i < data.multi_select.length; i++) {
        data_multi_select_array.push(data.multi_select[i].name);
      }
      return data_multi_select_array;
    } else {
      return data.multi_select;
    }
  } else if (data.type === "number") {
    return data.number;
  } else if (data.type === "people") {
    if (data.people.length > 0) {
      let data_people_array = [];
      for (let i = 0; i < data.people.length; i++) {
        data_people_array.push(data.people[i].name);
      }
      return data_people_array;
    } else {
      return data.people;
    }
  } else if (data.type === "phone_number") {
    return data.phone_number;
  } else if (data.type === "relation") {
    let return_array = [];
    if (data.relation.length > 0) {
      for (let i = 0; i < data.relation.length; i++) {
        return_array.push(data.relation[i].id);
      }
      return return_array;
    } else {
      return null;
    }
  } else if (data.type === "rich_text") {
    if (data.rich_text.length > 0) {
      let return_string = "";
      for (let i = 0; i < data.rich_text.length; i++) {
        return_string += data.rich_text[i].plain_text;
      }
      return return_string;
    } else {
      return null;
    }
  } else if (data.type === "rollup") {
    let rollup_type = data.rollup.type;
    let return_rollup = data.rollup[rollup_type];
    if (return_rollup == null) {
      return return_rollup;
    } else {
      return translate_notion_data_for_DB(return_rollup);
    }
  } else if (data.type === "select") {
    if (data.select != null) {
      return data.select.name;
    } else {
      return data.select;
    }
  } else if (data.type === "status") {
    if (data.status != null) {
      return data.status.name;
    } else {
      return data.status;
    }
  } else if (data.type === "title") {
    if (data.title.length > 0) {
      let return_string = "";
      for (let i = 0; i < data.title.length; i++) {
        return_string += data.title[i].plain_text;
      }
      return return_string;
    } else {
      return data.title;
    }
  } else if (data.type === "url") {
    return data.url;
  } else {
    return null;
  }
}

function add_notion_stream_page(video_content, youtuber_info, sheet_url, folder_url) {
  /**
   * 動画の詳細情報と配信者の情報、動画のために作成されたフォルダのURLとスプレッドシートのURLを与えられる。
   * 与えられた情報からNotionに送るためのJSONデータを関数make_notion_stream_json_dataを用いて作成する。
   * 作成されたJSONデータを関数make_new_notion_pageに与えることでNotionの指定されたDBに新しくページを作成できる。
   */
  let const_values = new Master_Values()
  let json_data
  json_data = make_notion_stream_json_data(video_content, youtuber_info, sheet_url, folder_url);
  make_new_notion_page(json_data, const_values.notion_api_key);
}

function make_notion_stream_json_data(video_info, youtuber_info, sheet_url, folder_url) {
  let const_values = new Master_Values()
  let json_data = {
    parent: {
      database_id: const_values.stream_DB_id,
    },
    "properties": {
      "動画名": {
        "title": [
          {
            "text": {
              "content": video_info[0],
            }
          }
        ]
      },
      "ストリームURL": {
        "url": video_info[1],
      },
      "配信日": {
        "date": {
          "time_zone": "Asia/Tokyo",
          "start": video_info[2],
          "end": null
        }
      },
      "スプレッドシートURL": {
        "url": sheet_url,
      },
      "配信者名": {
        "select": {
          "name": youtuber_info[0]
        }
      },
      "フォルダURL": {
        "url": folder_url,
      },
      "アップロードチャンネルURL": {
        "url": youtuber_info[3],
      },
      "チャンネルURL": {
        "url": youtuber_info[1],
      },
    }
  }
  return json_data;
}

function make_new_notion_page(json_data, api_key) {
  const api_url = 'https://api.notion.com/v1/pages';

  const options = {
    method: 'POST',
    headers: {
      'Content-type': 'application/json',
      'Authorization': "Bearer " + api_key,
      'Notion-Version': '2021-05-13',
    },
    // 'muteHttpExceptions': true,
    payload: JSON.stringify(json_data),
  };
  const res = UrlFetchApp.fetch(api_url, options);
}
function get_channel_videos_main(channel_url, notion_stream_time) {
  /**
  * YoutubeチャンネルのURLが送られてくる。
  * チャンネルIDに変更し、YoutubeDataAPIを利用し、必要なストリームの情報を取得する
  */
  let channel_id = get_youtube_channel_id(channel_url);
  let videos = get_all_video(channel_id, notion_stream_time);
  return videos;
}

function get_youtube_channel_id(url) {
  /**
   * YoutubeチャンネルのURLが送られてくる。
   * 特定の文字列の部分で分割するとチャンネルIDの部分を取得できる、
   */
  const html = UrlFetchApp.fetch(url).getContentText();
  const text = Parser.data(html).from('title="RSS" href="').to('">').build();
  const channel_id = text.split('=')[1];
  return channel_id;
}

function get_all_video(channel_id, notion_stream_time) {
  /**
   * 与えられたチャンネルIDからチャンネルに存在するライブ配信が終了した動画を取得する。
   * 1回で1個の情報を取得するがそれ以上ある場合はpage_tokenに次の2個目のトークンを記録する。
   * それ以降は動画情報が存在する限りdo-whileで複数回動かす。
   * ただし取得した動画情報が前日以前に配信された情報の場合は追加しない
   * 取得した情報は動画IDを含むのでその動画IDを関数serch_video_infoに送り、細かな情報を取得する。
   * 一つの動画の情報は一次元配列にして返される。
   * 返された情報は配列に追加することで二次元配列にする。
   */
  let const_values = new Master_Values()

  let return_array = [];
  let page_token = "";
  let latest_video_check = true
  do {
    let search_url = const_values.youtube_base_url + `search?key=${const_values.youtube_api_key}&part=snippet,id&channelId=${channel_id}&pageToken=${page_token}&type=video&eventType=completed&order=date&maxResults=1`
    let responce = UrlFetchApp.fetch(search_url)
    let text = responce.getContentText()
    let info = JSON.parse(text)
    for (let i = 0; i < info.items.length; i++) {
      let video_id = info.items[i].id.videoId;
      let video_info = serch_video_info(video_id)

      if (video_info[2] <= notion_stream_time) {
        latest_video_check = false
        break
      } else {
        return_array.unshift(video_info);
      }
    }
    page_token = info.nextPageToken;
  } while (page_token != null && latest_video_check)

  return return_array;
}

function serch_video_info(video_id) {
  let const_values = new Master_Values()
  /**
   * 与えられた動画IDから動画の情報を取得する。
   * 動画情報は一次元配列にして返されるが順番は以下の順番
   * ・動画のタイトル
   * ・動画のリンク
   * ・動画(ライブ配信)が配信開始された時間
   * またYoutubeリンクは関数video_linkに動画IDを送ることで取得される。
   * 動画(ライブ配信)が配信開始された時間はYoutubeからISO8601形式で与えられるので関数change_iso8601_dayを用いてGASで利用できる形式に変化させる。
   * 関数video_linkは「Youtube関連.gs」ファイルにある。
   * 関数change_iso8601_dayは「Youtube関連.gs」ファイルにある。
   */
  let search_url = const_values.youtube_base_url + `videos?key=${const_values.youtube_api_key}&part=snippet,liveStreamingDetails&id=${video_id}`
  let responce = UrlFetchApp.fetch(search_url)
  let text = responce.getContentText()
  let info = JSON.parse(text)
  let return_array = [];

  return_array.push(info.items[0].snippet.title);
  
  let video_url = video_link(video_id);
  return_array.push(video_url);
  
  let start_time = change_iso8601_day(info.items[0].liveStreamingDetails.actualStartTime);
  return_array.push(start_time);
  
  return return_array
}

function video_link(video_id) {
  let return_string = `https://www.youtube.com/watch?v=${video_id}`;
  return return_string;
}
class Master_Values{
  constructor(){
    this.sheet_id = PropertiesService.getScriptProperties().getProperty(`SS_sheet_id`)
    this.sheet_name = PropertiesService.getScriptProperties().getProperty(`main_sheet_name`)
    this.notion_api_key = PropertiesService.getScriptProperties().getProperty(`notion_integration_key`)
    this.stream_DB_id = PropertiesService.getScriptProperties().getProperty(`notion_stream_db_id`)
    this.stream_day_word = PropertiesService.getScriptProperties().getProperty(`notion_propaty_stream_day`)
    this.notion_video_url_word = PropertiesService.getScriptProperties().getProperty(`notion_check_video_url_word`)
    this.youtube_api_key = PropertiesService.getScriptProperties().getProperty(`youtube_api_key`)
    this.youtube_base_url = "https://www.googleapis.com/youtube/v3/"
    this.add_mail_address = PropertiesService.getScriptProperties().getProperty(`add_mail_address`)
  }
}

変更点

最後に記載されている部分のClass部分はGASにおけるスクリプトプロパティを利用しています。
スクリプトプロパティに下記の内容を記載して下さい。

  • SS_sheet_id(情報を記載するスプレッドシートのシートID)

  • main_sheet_name(配信者リストを記載するシート名)

  • notion_integration_key(NotionAPIのインテグレーションキー)

  • notion_stream_db_id(Notionで情報を追加するDBのID)

  • notion_propaty_stream_day(NotionのDBで配信日の日付のプロパティでのタイトル)

  • notion_check_video_url_word(NotionのDBで配信URLのプロパティでのタイトル)

  • youtube_api_key(自分でYoutubAPIKeyを取得して下さい)

  • add_mail_address(Googleの認証情報のサービスアカウントで取得したメールアドレスを登録)

Notionの設定

Notionのプロパティではそれぞれプロパティ名とプロパティ種類はこれにしました。

今後、切り抜きをする際に人力で切り抜いていい動画かどうか権利確認をする必要があります。
「権利チェック」の部分にはゲーム内容やBGMを確認して切り抜いてよいかどうかを確認してから人力でチェックするものになります。

次回作成するプログラムはこのチェックボックスにチェックがあることが前提となります。

よくあるエラー

・Exceeded maximum execution time
GASでの実行は一回につき6分までとなっております。
Youtubeのアーカイブが多数存在すると6分間では全ては取得できないのでエラーになります。

・Request failed for https://www.googleapis.com returned code 403.
YouutbeAPIなどは一つのアカウントで一日に実行できる量は決まっています。
この上限を超えようとするとエラーが発生します。
後述の毎日実行する形にすることで、エラー発生確率を減らせます。
最悪の場合、Googleの上限を増加させる申請を行ってください

気が向いたら申請方法などを記載する記事を記載します。

・スプレッドシートの作成上限エラー
スプレッドシートも一日に作成できる上限があります。

その他Googleサービス利用のための上限がありますが、毎日上限がリセットされるので、一日で全て行うよりも毎日定期実行されるようにしましょう。

最後にやること

GASのプロジェクトのトリガー設定を行います。

この設定で保存すれば作業は終了です。
明日以降も実行されるようにしましょう

次回の更新をご期待くださいませ

何か分からない部分があればコメントお願いいたします。
他の方も分からない部分の可能性もありますので、より分かりやすい記事に修正いたします。

この記事が気に入ったらサポートをしてみませんか?