【GAS】YouTubeの動画の統計を取る

# できるもの

設定した時間おきに、動画の「集計日時、再生回数、いいね数、コメント数」が、
スプレッドシートの指定した列に追加されていきます。
一気に複数の動画を集計することも可能です。

動画ID(URLから特定可能)がわかればどんな動画も集計できると思います。

ちなみに、スプレッドシートの機能でデータをグラフにすると見てて楽しいです。

:::note alert
他の方の動画の統計を投稿者の了承なく取っていいのか、またその統計結果を許可を取らずに公開していいのかどうかは、私にはわかりません。
詳しい人いたら教えてください。

また、この記事を参考にしたことで何かあっても責任は取りません。
そこのところはご了承ください。
<sub>今のところ特に何もないから大丈夫だとは思うけど...</sub>
:::


## 注意

この記事を書いている間にコードを1から作り直したので、一部作り直す前のコードや説明が入っているかもしれません。

時系列としては、
1. この記事を書き始める
1. スプレッドシートもGASのプロジェクトも1から作り直す
1. 期限が迫っていることに気づいてこの記事の続きを書く

というふうになっています。

そのためミスがある可能性が高いです。
すみません。


# 新しくプロジェクトを作る

まずは、集計したデータを出力する**スプレッドシートを用意**します。
このスプレッドシートに動画ののデータが出力されます。

また、このスプレッドシートの「拡張機能」タブからApps Scriptを選択して、
GASのエディタ画面を開いておいてください。

![gas_editor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588083/6100d108-be42-2962-9cea-a168f44c8c93.png)


## YouTube API を入れる

GASの画面で、左側にある「サービス」の隣の＋をクリックします。
そしてそこから **「YouTube Data API v3」を選択** します。
バージョンはv3、IDは`YouTube`のままで大丈夫です。

サービスのところに`YouTube`が追加されていたら成功です。
この`YouTube`はオブジェクトになっていて、動画データはこれを使って取ります。

![youtube_api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588083/241b66b0-c898-6c93-4087-216e40e8445b.png)

詳しくは[自分で調べてください](https://www.google.com/search?q=Youtube+data+api+GAS)(無責任


# データを取得する

引数に動画IDを取る、`fetchData`という関数を作ります。

:::note
動画IDとは、たとえば https://www.youtube.com/watch?v=aaaaaaaaaa というURLの動画あったとしたら、動画IDは`aaaaaaaaaa`です。
URLの`v=`以降の部分を抜き出してください。
:::

以下の関数を実装してください。

```js
function fetchData(videoId) {
    // データを取得
    const data = YouTube.Videos.list('statistics', { id: videoId });
    return data.items[0];
}
```

これでこの関数を実行すると動画の情報が取れます。

2行目の`YouTube.Videos.list('statistics', {id: videoId});`という部分で、YouTube Data APIを使っています。
第二引数の`id`プロパティには、動画IDを入れるみたいです。

どんな関数があるのかなどは、以下のリファレンスを参考にしてください。

https://developers.google.com/youtube/v3/docs?authuser=0&hl=ja

https://developers.google.com/youtube/v3/docs/videos?authuser=0&hl=ja#properties

https://developers.google.com/apps-script/advanced/youtube?authuser=0&hl=ja

* * *

一応`fetchData`をテストしておきます。

```js
function test() {
  console.log(fetchData('4WXs3sKu41I'));
}
```

以上のように`test`関数を実装して、実行してみてください。
この際**権限がリクエストされますが、承認してください。**

それっぽいデータがログに流れたら成功です。


## listの第一引数

さて、`fetchData`関数の以下の行↓ですが、
```js
const data = YouTube.Videos.list('snippet,statistics', { id: videoId});
```
なにやら`list`関数の第一引数に`'snippet,statistics'`を入れています。
これは何かというと、必要なデータのみを取得できるように設定するやつみたいです。

まず、`data.items[0]`には下のリファレンスにあるデータ構造が入っています。

https://developers.google.com/youtube/v3/docs/videos?authuser=0&hl=ja#resource-representation

このデータ構造の中には`snippet,contentDetails,status`などのプロパティが入っています。
そして、この中で**必要なデータのプロパティのみを第一引数に登録**することで、
なんか転送するデータ量が削減できる的な感じですかね...?(わかってない)

例えば、以下のような感じです。
```js
// この場合、
const data = YouTube.Videos.list('snippet', { id: '4WXs3sKu41I' });
// data.items[0]には、
/*
{ id: '4WXs3sKu41I',
  etag: 'O4vzIBxJ9AM91JQlVW1wr63XHvk',
  kind: 'youtube#video',
  snippet: 
   { title: 'Google — Year in Search 2022',
     thumbnails: { 略 },
     channelTitle: 'Google',
     tags:  [ 略 ],
     channelId: 'UCK8sQmJBp8GCxrOtXWBpyEA',
     liveBroadcastContent: 'none',
     defaultAudioLanguage: 'en-US',
     publishedAt: '2022-12-08T10:09:46Z',
     categoryId: '28',
     localized: { 略 },
     description: 略 } }
*/
// みたいにsnippetプロパティが入ってる

////////////////////////////////////////////////

// この場合、
const data = YouTube.Videos.list('statistics', { id: '4WXs3sKu41I' });
// data.items[0]には、
/*
{ kind: 'youtube#video',
  etag: 'RnuQby7YcD8Sc_1kPh7RhlMWlm4',
  id: '4WXs3sKu41I',
  statistics: 
   { viewCount: '296846910',
     favoriteCount: '0',
     commentCount: '4513',
     likeCount: '75733' } }
*/
// のようにstatisticsプロパティが入ってる
```

また、カンマ(`,`)で区切ることによって複数指定可能です。

ここに使える文字列は[リファレンス](https://developers.google.com/youtube/v3/docs/videos?authuser=0&hl=ja)にわかりやすく書いてありますが、
一応まとめておきます。

| 文字列 | 意味 |
| :-- | :--- |
snippet | 動画に関する基本情報<br>タイトル、説明、カテゴリなど
contentDetails | 動画コンテンツに関する情報<br>動画の長さや、字幕が使用可能かどうかなど
status | ステータスに関する情報<br>動画の公開状況、ライセンスなど
statistics | 動画に関する統計情報<br>再生回数、いいね数、コメント数など
player | 動画を埋め込む際の情報<br>埋め込みコードなど
topicDetails | 動画のトピック(カテゴリ?)に関する情報<br>wikipediaのリンクなど
recordingDetails | 撮影された動画の情報、{}が返ってくることも<br>撮影日時、撮影場所など
fileDetails | 動画ファイルに関する情報<br>動画の所有者のみ取得可能
processingDetails | 動画アップロードに関する進捗状況<br>動画の所有者のみ取得可能
suggestions | 動画の品質やメタデータの改善に関する情報<br>動画の所有者のみ取得可能
liveStreamingDetails | ライブに関する情報<br>ライブの開始時刻や同時視聴者数など<br>ライブ動画でのみ取得可能
localizations | 動画のメタデータの翻訳版<br>翻訳されたタイトルや説明など?


# シートに追加する

動画IDとシートの範囲(range)を入れると、データがシートに入る関数を作ります。

```js
function addDataToRange(videoId, range) {
    const data = fetchData(videoId).statistics; // データを取得
    const dataArray = [
        new Date(), // 現在時刻
        data.viewCount, // 再生回数
        data.likeCount, // 高評価数
        data.commentCount // コメント数
    ];
    range.setValues([dataArray]); // []で囲むことで二次元配列にする
}
```

この関数は、内部で`fetchData()`を実行しています。
そして、取得したデータを`setValues`で範囲にセットしています。

一応テストします。

```js
function test() {
    addDataToRange('4WXs3sKu41I', SpreadsheetApp.getActiveSheet().getRange('A3:D3'));
}
```

この関数を実行すると権限がリクエストされますが、**承認してください**。
シートのA3からD3にそれっぽいデータがセットされたら成功です。


# 最終行を返す関数を作る

そしたら今度は指定した列の最終行にデータを追加していきます。
...と思ったのですが、それをやる関数が調べても出てこなかったため、とりあえず指定した列の**最終行を返す関数**を作ります。

```js
// 指定した列の最終行を返す関数
function getLastRow(column, sheet) {
  // getLastRowは空白を含む最終行を返す、シートの長さを調べるのに使ってる
  const range = sheet.getRange(`${column}1:${column}${sheet.getLastRow()}`);

  // getNextDataCellで最終行を取得している
  const result = range.getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
  
  if(range.getValues()[1][0] == '') {
    // 最終行が1だとうまくいかなかったため
    return 1;
  } else {
    return result;
  }
}
```

まず、`range.getNextDataCell(SpreadsheetApp.Direction.DOWN)`を使って、
データがある最終行のセルを取得しています。
次に、`getRow()`を使って、そのセルの行数(=最終行の行数)を取得しています。

以下のサイトを参考にしました。
このサイトの方がわかりやすいので、詳しくはこちらを見てください。

https://moripro.net/gas-get-specified-lastcol-lastrow/

* * *

この関数も一応テストします。

まず、スプレッドシートの内容を以下のようにします。

|  | A 
| :-- | :-- |
1 | aaa
2 | bbb

そして、`test`関数を以下のようにします。

```js
function test() {
    console.log(lastRow('A', SpreadsheetApp.getActiveSheet()));
}
```

そしたらこの関数を実行してください。
「`2`」と出力されたらうまく動いています。


# テンプレートを入れる関数を作る

例えば以下のようなシートがあったとします。

|  | A | B | C | D |
| :-- | :-- | :-- | :-- | :-- |
1 | 
2 | 
3 |

ここのA1セルに`videoId:trigger`という文字を入れます。

|  | A | B | C | D |
| :-- | :-- | :-- | :-- | :-- |
1 | 4WXs3sKu41I:1d
2 | 
3 |

そしたら以下のように表示される機能を、今から作ります。

|  | A | B | C | D |
| :-- | :-- | :-- | :-- | :-- |
1 | 4WXs3sKu41I:1d | videoId = 4WXs3sKu41I | trigger = 1d |
2 | 日時 |再生回数 | いいね数 | コメント数 
3 |

上の例だと、`4WXs3sKu41I`という動画IDの動画を、`1d`つまり1日おきに集計してくれるようになります。
というかこれからします。

1行目に`videoId:trigger`という形式に沿った文字を入力すると、上のように文字を挿入する機能を、ここでは(なぜか)テンプレートと呼んでいます。

実装はこんな感じです。

```js
// onEditはスプレッドシートにユーザーが変更を加えた時に実行される
function onEdit(e) {
  if(e.range.getRow() === 1) { // 変更されたセルの列が1行目なら
    const cellValues = e.range.getValue()?.split(':'); // [videoId, trigger]
    if(cellValues.length < 2) return; // 形式に沿っていなければreturn
    
    // 隣に動画IDとtriggerを表示
    const nextRange = e.range.getSheet().getRange(e.range.getRow(), e.range.getColumn() + 1, 1, 2);
    nextRange.setValues([['videoId = ' + cellValues[0], 'trigger = ' + cellValues[1]]]);

    // 下に日時等を表示
    const bottomRange = e.range.getSheet().getRange(e.range.getRow()+1, e.range.getColumn(), 1, 4);
    bottomRange.setValues([['日時', '再生回数', 'いいね数', 'コメント数']]);
  }
}
```

`onEdit`関数は少し特殊で、スプレッドシートにユーザーが変更を加えた時に実行されます。
第一引数にはイベントオブジェクトが入ります。...多分()

あとはコメントの通り、隣に動画IDとトリガーを示す文字を入れて、下に日時や再生回数という文字を入れています。
この[日時,再生回数,略]というセルの下に、集計されたデータが入ります。


# メイン処理を作る

ということで、ようやく実際に動かす処理を作ります。
...と言いたいところなのですが、実はちょっと違います。
今から作るのは、スプレッドシートから該当するトリガーの動画IDを探して、その通りにデータを取ってきてシートに入れる関数です。

```js
// おかしいな...この変数どの時点で定義してたっけ...
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

// 関数名は何も考えていなかった模様
function action(triggerStr) { // triggerStrには、例えば1dや15mといった文字が入る
  for (const sheet of spreadsheet.getSheets()) {
    // これはA列のRangeオブジェクト
    const rowA = sheet.getRange(1,1,1,sheet.getLastColumn());
    
    for(const [column, str] of rowA.getValues()[0].entries()) {
      if(!str.includes('videoId = ')) continue;
      
      const trigger = rowA.getCell(1, column+2).getValue().replace('trigger = ', '');
      if(trigger !== triggerStr) continue; // トリガーが該当しなかったらさようなら
        
      // 動画のデータをセルに挿入
      const videoId = str.replace('videoId = ', '');
      const range = sheet.getRange(getLastRow(column, sheet)+1, column, 1, 4);
      addDataToRange(videoId, range);
    };
  }
}
```

この関数ではまず、テンプレートで追加されているはずの`videoId = 何か`と`trigger = なにか`というセルを(なぜか)探しています。

そして、そのセルが見つかったら、そこからどのトリガーで実行するかの文字列を抽出して、引数の`triggerStr`と一致するかを調べます。

一致する場合は`addDataToRange`関数でシートにデータを追加しています。


## トリガーにする関数を作る

上の`action`関数は以下のように使います。

```js
function oneMinutes() { // oneMinutes = 1分
    action('1m'); // 1m = 1分
}
```

上の関数を追加して、別画面からトリガーを「時間ベースのトリガー>1分」に設定すると、`videoId:1m`となっているところの動画IDでデータが追加されます。

|  | A | B | C | D |
| :-- | :-- | :-- | :-- | :-- |
1 | 4WXs3sKu41I:1m | videoId = 4WXs3sKu41I | trigger = 1m |
2 | 日時 |再生回数 | いいね数 | コメント数 
3 | 20xx/xx/xx x:xx:xx | xxxxxxx | xxxxxx | xxxxxx
4 | (上の1分後) | xxxxxxx(多分増えてる) | xxxxxx | xxxxxx
5 | 以下略

というわけで、こんな感じの関数を量産します。

```js
function oneMinutes ()  {
  action('1m');
}
 
function fiveMinutes ()  {
  action('5m');
}
 
function fifteenMinutes ()  {
  action('15m');
}
 
function thirtyMinutes ()  {
  action('30m');
}
 
function oneHour ()  {
  action('1h');
}
 
function sixHour ()  {
  action('6h');
}
 
function oneDay ()  {
  action('1d');
}
```

なんの変哲もない関数たちです。
上のはコピペでいいと思います。手打ちだとかなり面倒だし。

そしてここから、上で追加した関数にすべてトリガーを設定するという苦行が始まります。


## トリガーを設定する

ここまで来たら、あとはトリガーを設定するだけで集計が始まります。
といってもここに書くのは面倒なので、以下のページを参考にしてください。

https://auto-worker.com/blog/?p=6383

先ほど作った関数を、時間ベースのトリガーで実行するようにしてください。
例えば`fiveMinutes`なら5分おき、`oneDay`なら1日おきです。
これが結構面倒ですが、まあ頑張ってください。

これができたら多分集計ができるようになっていると思います。
この記事のコードは十中八九バグってると思うので、もしバグ等を見つけたら教えてくださると助かります。


# 参考

https://zenn.dev/sungvalley/articles/ccce17e076984f

https://qiita.com/comcha/items/5edda78c917bf70adb63

https://moripro.net/gas-get-specified-lastcol-lastrow/

https://auto-worker.com/blog/?p=6383