[GAS] Youtubeチャンネル内の動画情報を取得したいならSearch.listとVideos.listを使用する

# はじめに
::: note
本記事は執筆時時点の情報となります。2022年11月にハンドルが導入され、ハンドルURLはYouTubeチャンネルの表面上のデフォルトURLとなりました。しかしながら、YouTube Data API(Ver.3)では、返されるデータも含め、ハンドルを直接扱えるものはありません。（2023年3月現在）

その為現時点では、非公式なスクレイピング行為を除き、チャンネルIDを取得する方法として以下のものが考えられます。

1. 当該チャンネルページのHTMLソースを直接確認する
2. `Search.list`で対象をチャンネルに限定した上でハンドル名で検索する（ハンドル名で絞り込む検索演算子がないため完全一致の保証なし）

[ハンドルの概要 - YouTube ヘルプ](https://support.google.com/youtube/answer/11585688)
:::

YouTubeチャンネルに含まれる動画から、各種データを取得したいという相談がありました。相談者は**WEBスクレイピング**を考えていたようですが、公式のAPIがあるので**それ**はお断りしました。**同種の案件**は受けることはないですが、APIを利用した公式のデータ取得に関してはやぶさかではありませんので備忘録として残しておきたいと思います。

なお、前提としてスクリプトを設置するプロジェクトにおいては、拡張サービスにて`YouTube Data API`を追加する必要があります。追加、設定方法につきましては、拙記事ではありますが[こちら](https://qiita.com/STSHISHO/items/f6fb68f82183a805ca83)を参考にしていただければと思います。[^1]

[^1]: Magicodeから記事移動後、若干の文言修正をしています。

# 目的
特定のYouTubeチャンネルに含まれる動画に関する情報を取得し、スプレッドシートで管理したい。
具体的には以下の項目を取得したい。

+ サムネイル画像
+ 動画タイトル
+ 動画の説明
+ アップロード日時
+ 動画再生回数
+ 高く評価された数
+ お気に入りの数
+ コメント数

::: note
低く評価された数は公式で自身管理の動画以外は取得できないように変更されました。
サンプルコードでは割愛していますが、取得項目としてご要望が多い動画URLは、動画IDを取得し、標準URLに成形します。
:::

# ロジックについて
以下のようなフローアウトラインとなります。

1. `Search.list`でチャンネルIDによる指定チャンネル内の動画ID取得
1. `Videos.list`で動画ごとの詳細情報を取得

::: note
**補足**
`Channels.list`からアップロード済み動画リストのIDを取得した後、`PlaylistItems.list`を経由しリスト内の動画IDを取得する、という方法を紹介したブログもありました。（当該ブログへのリンクは控えさせていただきます）

ただ、この方法はAPIリクエスト数、処理負荷、実行時間、１日あたりの**Quota**の消費など諸々考慮するとおすすめできません。
:::

## チャンネルIDについて
特定のチャンネルということで、すでに**チャンネルIDがわかっている前提**となります。

お問い合わせ時によくあるのは、URLはわかっているけどチャンネルIDがわからないというものです。以下のような標準のURL形式なら、`channel/`以降のUCで始まる英数字をコピペ頂くか、数式等で抽出も可能です。

```Text
 https://youtube.com/channel/UC*******************
```

上記以外のカスタムURLの場合、当該チャンネルのHTMLソース内の記述部分を探して取得する必要があります。

::: note
スクレイピングでチャンネルIDを取得するツールもあるようですが、利用規約的にどうかと思いますので紹介はいたしません。
:::

カスタムURLについては以下をご確認ください。

[チャンネル URL について - YouTube ヘルプ](https://support.google.com/youtube/answer/6180214?hl=ja)

## ロジック詳細
以下、ステップごとにサンプルコードを記述していますが、エラー処理等は割愛しています。

### 1-1. `Search.list`よりチャンネル内動画のID取得
データ総量削減のため、必須項目である`part`には`id`のみを設定しています。また、初期状態では１リクエストあたり最大５件しか取得できませんので、５０件まで取得するようにしています。

```Javascript
const channelPart  = 'id'; // id関連情報のみ取得する設定
const channelQuery = {
    channelId: channelId,
    type: 'video',
    maxResults: 50, // 最大取得件数の設定
    pageToken:''
};
```

動画数が５０件超の場合、`nextPageToken`を確認し、再帰処理を行うようにしています。ただし、チャンネルによっては１日あたりの**Quota**を超えてしまう可能性があるため、別途繰り返し上限数を設定するか、取得期間の設定をするなどの対応が必要になります。

なお、当方で受注する際には初期状態で１チャンネルにつき５００件までに制限していますが、運用状況や実行時間などの様子を見て、適宜上限数の調整をしていただいています。

### 1-2. 取得したIDリストをカンマ区切りテキスト化しておく

次の処理用に、取得した動画ID５０件を`toString()`により一括でカンマ区切りテキストにしています。また、ループ処理用として、最終的に一次元配列で返すようにしています。

つまり、下記`getVideoIds`の戻り値`videoIds`の１要素は最大５０件分の動画IDのカンマ区切りテキストとなります。

```Javascript:動画IDリストの取得
const getVideoIds = (ids, res) => {
    // videoIdをカンマ区切りテキストにして一次元配列の要素としてセットする
    ids.push(res.items.flatMap(item => item.id.videoId).toString());
    let next = res.nextPageToken || false;

    // 再帰処理
    if(next){
        channelQuery.pageToken = next;
        getVideoIds(ids, YouTube.Search.list(channelPart, channelQuery));
    }
    return ids;
};

const videoIds = getVideoIds([], YouTube.Search.list(channelPart, channelQuery));
```

::: note
**補足**
`Search.list`及び`Videos.list`ともに、`query`内で複数IDを指定する際はカンマ区切りテキストにする必要があります。指定できる最大値の明記はありませんが、やはり５０件が上限となります。
:::

::: note
ご指摘頂いた重複について、当方環境での現象確認はできませんでしたが、コメント欄に動作確認済みのサンプルコードを提示しておきます。必要に応じて該当部分を差し替えていただければと思います。
:::

### 2-1. `Videos.list`の`part`設定
取得項目の内容から、必須項目である`part`には`snippet`と`statistics`を設定する必要があります。`id`はなくても構いませんが、今回は含めておきます。

```Javascript
const videoPart = 'id,snippet,statistics'; // idはなくても良い
```

### 2-2. 動画ごと詳細データの取得
以下の`getVideoData`にて、取得する項目の順番や整形を行っています。それぞれ取得できなかった場合には「-」や「0」を返すようにしています。

サムネイル画像につきましては、標準画質のURLを取得し、`IMAGE関数`にてスプレッドシート側で処理する前提となります。必要に応じて低画質、高画質を選択できますし、URLのみにすることもできます。

```Javascript: 動画ごと詳細データの取得
const getVideoData = item => [
    `=IMAGE("${item.snippet.thumbnails.default.url}",1)` || '-', // サムネイル画像
    item.snippet.title || '-', // 動画タイトル
    item.snippet.description || '-', // 動画の説明
    new Date(item.snippet.publishedAt) || '-', // アップロード日時
    item.statistics.viewCount || 0, // 動画再生回数
    item.statistics.likeCount || 0, // 高く評価された数
    item.statistics.favoriteCount || 0, // お気に入りの数
    item.statistics.commentCount || 0 // コメント数
];
```

### 2-3. スプレッドシートへの展開準備処理
以下の`getDescriptions`にて、スプレッドシートへの展開用に最終的に二次元配列で返すようにしています。

```Javascript: 動画詳細データ取得後シート展開用に二次元配列として返す
const getDescriptions = ids => ids.flatMap(id => {
    let query = {id: id};
    let res   = YouTube.Videos.list(videoPart, query);
    return res.items.map(item => getVideoData(item));
});
```

### 参考ロジック

以下、全体像となります。ご参考まで。

```Javascript: Sample.gs
const channelId    = __ チャンネルID __;

/** Search.list
---------------------------------------- */
const channelPart  = 'id';
const channelQuery = {
    channelId: channelId,
    type: 'video',
    maxResults: 50,
    pageToken:''
};

// 動画IDリストの取得
const getVideoIds = (ids, res) => {
    // videoIdをカンマ区切りテキストにして一次元配列の要素としてセットする
    ids.push(res.items.flatMap(item => item.id.videoId).toString());
    let next = res.nextPageToken || false;

    // 再帰処理
    if(next){
        channelQuery.pageToken = next;
        getVideoIds(ids, YouTube.Search.list(channelPart, channelQuery));
    }
    return ids;
};

/** Videos.list
---------------------------------------- */
const videoPart = 'id,snippet,statistics'; // idはなくても良い

// 動画ごと詳細データの取得
const getVideoData = item => [
    `=IMAGE("${item.snippet.thumbnails.default.url}",1)` || '-', // サムネイル画像
    item.snippet.title || '-', // 動画タイトル
    item.snippet.description || '-', // 動画の説明
    new Date(item.snippet.publishedAt) || '-', // アップロード日時
    item.statistics.viewCount || 0, // 動画再生回数
    item.statistics.likeCount || 0, // 高く評価された数
    item.statistics.favoriteCount || 0, // お気に入りの数
    item.statistics.commentCount || 0 // コメント数
];

// 動画詳細データ取得後シート展開用に二次元配列として返す
const getDescriptions = ids => ids.flatMap(id => {
    let query = {id: id};
    let res   = YouTube.Videos.list(videoPart, query);
    return res.items.map(item => getVideoData(item));
});

/** Main Process
---------------------------------------- */
// 動画IDリストの取得
const videoIds = getVideoIds([], YouTube.Search.list(channelPart, channelQuery));

// チャンネル内の動画情報取得
const descriptions = getDescriptions(videoIds);

// スプレッドシートへ展開

/* 以降の処理は割愛 */
```

なお、`descriptions`取得後の処理に関しましては、一般的なシートへの展開処理となりますので、割愛しています。

#### 補足
約１０００件の動画詳細情報取得について動作検証した際、`getDescriptions`では`Array.flatMap`を使うより、`Array.forEach`を使うほうが約３秒ほど早く終了しました。以下が参考コードです。

```Javascript
const getDescriptions = ids =>  {
    const descriptions = [];

    ids.forEach(id => {
        let query = {id: id};
        let res   = YouTube.Videos.list(videoPart, query);
        res.items.forEach(item => descriptions.push(getVideoData(item)));
    });

    return descriptions;
};
```

# 最後に
シートへの展開時、データの並び順はデフォルトで検索クエリの関連性が高い順となっています。データの並び替えはシート上でもできますが、`Search.list`の`query`に`order`プロパティを追加し、任意の並び順にすることもできます。

コードボリュームも多くないので、その他色々ご要望に合わせたカスタマイズも容易かと思います。

誰かの役に立てば幸いです。

# [参考・引用]
+ [Search  |  YouTube Data API  |  Google Developers](https://developers.google.com/youtube/v3/docs/search)
+ [Videos  |  YouTube Data API  |  Google Developers](https://developers.google.com/youtube/v3/docs/videos)