GAS x YouTube Data API v3 ではてブで人気のYouTube動画のプレイリストを作るの巻

この記事ははてなエンジニアアドベントカレンダー 2023の7日目の記事です。

6日目の記事は id:tomato3713の「組み込み用途向けのGo言語のサブセットTinyGoによるM5Stack Basicの制御を試す - tomato3713’s blog」でした。


はてなブックマークでは、日々インターネット上の注目コンテンツが更新されています。様々なサイトにブックマークがつけられていますが、その中にはもちろんYouTubeの動画もあります。はてなブックマークのページから各動画ページへ遷移して動画を観ても良いんですが、もしかすると特定期間の人気エントリに入った動画をプレイリストにしたらおもしろいのでは?と考えて、Google Apps Scriptでコードをかいてみました。

全体の方針

できあがったプレイリスト

2023年10月の人気エントリに入っていたYouTube動画のプレイリストです。やはりカテゴリを指定しないと雑多感ありすぎて連続してみたいという気持ちにならないですね...。

ということで「テクノロジー」のカテゴリを指定してみました。1ヶ月という期間だと動画の数が少なかったので期間を1年にして、話題の変化を知りたかったので「2022年」と「2023年」でそれぞれ作ってみました。2023年はChatGPT関連の動画が多く見られますね。

「アニメとゲーム」「エンタメ」で、10月の動画リストを作ってみました。「アニメとゲーム」の方はだいぶ分かりやすいですね。

コード

function exec() {
  createHBYouTubePlaylist('2023-10-01','2023-10-30','アニメとゲーム');
}

function createHBYouTubePlaylist(startdate, enddate, category , users) {
  let title = startdate + 'から' + enddate + 'にはてブで人気エントリになった動画';
  if (category) {
    title += " (" + category+ ")";
  }
  const results = createPlaylist(title,title); // 「非公開」でプレイリストを作成
  const videos = getYouTubeHotentry(startdate, enddate, category , users);
  let pattern = /https?:\/\/www\.youtube\.com\/watch\?v=(.*)/g;
  for (var i = 0 ; i < videos.length; i++) {
    var result = videos[i].match(pattern);
    let videoId = RegExp.$1;
    try {
      YouTube.PlaylistItems.insert({
        snippet: {
          playlistId: results.id,
          resourceId: {kind:'youtube#video', videoId: videoId},
        }
      },['snippet']);
    } catch (e) {
      console.log(e);
    }

  }
}

function createPlaylist(title,description,privacyStatus) {
  const results = YouTube.Playlists.insert(
    {
      snippet: {
        title: title || 'プレイリスト',
        description: description || 'プレイリストの説明文'
      },
      status: {
        privacyStatus: privacyStatus || 'private'
      }
    },
    ['snippet','status']
  );
  return results;
}

function getYouTubeHotentry(dateBegin, dateEnd , category , users ) {
  dateBegin = dateBegin || '2023-01-01';
  dateEnd = dateEnd || '2023-01-31';

  const categories = {
    "世の中" : 1,
    "政治と経済" : 1,
    "暮らし" : 1,
    "学び" : 1,
    "テクノロジー" : 1,
    "おもしろ" : 1,
    "エンタメ" : 1,
    "アニメとゲーム" : 1
  };
  category = (categories[category]) ? category : null;

  users = users || '5';

  const rss = XmlService.getNamespace("", 'http://purl.org/rss/1.0/');
  const ns_rdf = XmlService.getNamespace("rdf","http://www.w3.org/1999/02/22-rdf-syntax-ns#");
  const ns_dc = XmlService.getNamespace("dc","http://purl.org/dc/elements/1.1/");
  let videos = [];
  let page = 1;
  while (videos.length < 30) {
    let url = "https://b.hatena.ne.jp/q/site%3Awww.youtube.com%2Fwatch?safe=on&target=text&mode=rss&users="+ users +"&sort=popular&date_end="+ dateEnd +"&date_begin=" + dateBegin + "&page=" + page;
    let responseText = UrlFetchApp.fetch(url).getContentText();
    let xml = XmlService.parse(responseText);
    let root = xml.getRootElement();
    let items = root.getChildren('item', rss);
    if (items.length == 0) {
      break;
    }
    items.forEach((item) => {
      if (category == null || item.getChild('subject',ns_dc).getValue() == category) {
        videos.push(item.getAttribute("about",ns_rdf).getValue())
      }
    });
    page ++;
  }
  return videos.slice(0,30);
}

使い方

Googleドライブの「+新規」> 「その他」 > 「Google Apps Script」を選択

エディタのエリアに上記コードをコピペして貼り付け

左の「サービス + 」をクリックして表示されたダイアログで「YouTube Data API v3」を選択して「追加」

execの中の日付、カテゴリを書き換えて、メニューの関数プルダウンから「exec」を選択し、「実行」をクリック

初回のみ権限を求められるので内容を確認して「許可する」をクリック

注意点

YouTubeAPIは、1日の利用量がクォータ(Quota)という単位で決まっています。Googleに申請して特別対応してもらわなければ、基本的には1日10,000クォータが上限となります。APIのタイプごとに消費されるクォータが設定されていて、サーバーの処理の負荷が高いものについては消費されるクォータが多く設定されています。消費される量については以下のページで見ることができます。

YouTube Data API(v3) - 割り当て計算ツール  |  Google for Developers

例えば「再生リスト」のlistメソッド(再生リストの一覧を取得する)はREADなので消費量は「1」で、insertメソッド(再生リストを作る)はCREATEなので消費量は「50」という感じ。最も多いのが動画のinsertメソッドで1,600となっていますね。一日に6本をアップロードするだけで上限近くまでいっちゃいますね。

今回のスクリプトでは、1回の動作でプレイリストを1つinsert(50quota)して、そのプレイリストに動画を最大30本追加(50quota x 30 = 1500quota)するので、1,550quotaとなり、およそ動画を1本追加するのと同じくらいのコストを使うことになります。

Scrapboxで編集があったページを1日1回Slackに通知する

PublicになっているScrapboxを利用している場合は、RSS Feedを1日1回チェックして投稿するプログラム(lambdaでも、GASでも)を書いてハイ終了、です。

今回のケースでは、Scrapboxを業務利用していて、閲覧には認証が必要という状況。インターネット側からRSS Feedを取得することができない場合です。このケースでは、Slack連携Scrapboxへの書き込み終了して90秒後に当該箇所の情報をSlackへPostする)は利用できるので、これとGASを組み合わせて解決することにしました。

以下に手順をまとめます。

Step.1 Slack連携のPostを受け取ってSpreadSheetに記録するGASを作る

  1. 新しいGoogle Spreadsheetを作成し、見出し行として1行目に「日付」「時刻」「タイトル」「URL」「data」と入力
  2. 拡張機能」→「App Script」を開く
  3. 以下のコードを貼り付ける(だいぶ雑です)
  4. 「デプロイ」→「新しいデプロイ」
    • 種類は「ウェブアプリ」、アクセスできるユーザーは「全員」を指定
    • 初回作成時はデータへのアクセス許可を求められるので承認
    • 表示されたURLを変更通知を受け取りたいSlackの「Project Settings」の「Notifycations」から「Add Slack notification」として追加

Step.2 SpreadSheetの内容から前日分の情報を抽出してSlackへPostする

  1. SlackでWebhookのエンドポイント作成
  2. Step.1で作ったGASの左側の歯車メニュー「プロジェクトの設定」から「スクリプト プロパティ」に「SLACK_WEBHOOK_URL」として先程のエンドポイントを入力して保存
  3. 左側の時計メニュー「トリガー」から「トリガーを追加」
    • 実行する関数は「postPagesEditedAtYesterday」、イベントのソースは「時間主導型」を選択して実行したいタイミングを指定して保存

これで完成。
こんな面倒なことをしなくても、RSS Feedが直接読める環境であればStep.2のトリガーによる定期実行のタイミングでFeedを取得して昨日更新分をまとめてPostすればいい。

GASでXML(RSS)を取得して処理する

ossanfmのRSS feedを取得するコード

const ns_itunes = XmlService.getNamespace("itunes","http://www.itunes.com/dtds/podcast-1.0.dtd");

const response = UrlFetchApp.fetch('https://ossan.fm/feed.xml');
const xml = XmlService.parse(response.getContentText());

const root = xml.getRootElement();
const channel = root.getChild('channel');

const podcastName = channel.getChild('title').getText(); 

// channelの下のitem群を取得
channel.getChildren('item').forEach((item,i) => {
    let title = item.getChild("title").getText();
    let link = item.getChild("link").getText();
    let enclosure = item.getChild("enclosure").getAttribute("url").getValue(); //属性値を取得
    let duration = item.getChild("duration",ns_itunes).getText(); // itunes:duration タグを処理
    let date = Utilities.formatDate(new Date(item.getChild("pubDate").getText()), 'Asia/Tokyo', 'yyyy/MM/dd');//日付の処理
    console.log(title,link,date,enclosure,duration);
})

メモ

  • 外部のRSSを取得
    • UrlFetchApp.fetch(url)を利用する
  • XMLをparse
    • XmlService.parse(text)を利用する
  • タグの値を取得
    • Element.getChild(tagname).getText() を利用する
  • 属性値を取得
    • getAttribute(attributeName)を利用する
  • 特殊な名前空間のタグを取得
    • XmlService.getNamespace(prefix, uri) / Element.getChild(tagname, namespace) を利用
    • itunes:durationの場合
      • prefixにitunesを指定、getChildのtagnameにはdurationを指定