Youtubeチャット取得

コメントビューア用。
OAuthが要るようなAPIは使わず、未登録で取得できるコメントを取得。
あくまで執筆時点の情報です。仕様が変わる可能性もあるのであしからず。

元々解析済みだったのですが、2021/01/29にYoutubeにガッツリ仕様変更入って調査をしたのでついでに書きます。

---

## 1. 配信ページを取得

Youtubeの動画URLは、ライブ配信、アップロード動画に関わらず、同じURL体系をしています。
例: `https://www.youtube.com/watch?v=K0x0tZ4kRzs`

URLのうちv=`xxxxx`を便宜上LiveIDと呼びますが、このLiveIDはライブ配信を行うたびに変わるため、コメントビューアを作る場合はLiveID以外の情報でページを取得する必要があります。
※限定公開動画の場合はアカウントログインして頑張るか、LiveID直接指定が必要です

その方法は単純で、以下URLにアクセスするだけでOKです。
`https://www.youtube.com/channel/{チャンネルID}/live`
配信が始まっていない場合はチャンネルトップが表示され、配信が始まっている場合は`watch?v=xxxx`と同様のページが表示されます。

## 2. 初期パラメータ取得
配信ページに埋め込まれている、チャット取得に必要な初期パラメータを取得します。
いずれもページ冒頭のscriptタグに埋め込まれているため、配信ページのソースから文字列マッチングを駆使して拾います。
以下はjsによるサンプル。

```js
      const key = htmlsrc.match(/innertubeApiKey":".*?"/)[0]
        .split(':')[1]
        .replace(/"/g, '');

      const continuation = htmlsrc.match(/continuation":".*?"/)[0]
        .split(':')[1]
        .replace(/"/g, '');
// エラーハンドリングは実装者の裁量に任せます
```

## 3. コメント取得(リクエスト)

YoutubeチャットはPOSTで取得します。
リクエストボディは以下。

```js
const body = {
  context: {
    client: {
      clientName: 'WEB', // 指定しないと怒られる
      clientVersion: '2.20210126.08.02', // 割となんでもよさそう
      timeZone: 'Asia/Tokyo', // 返ってくるタイムスタンプに影響ありそう
      utcOffsetMinutes: 540,  // 返ってくるタイムスタンプに影響ありそう
      mainAppWebInfo: {
        graftUrl: `https://www.youtube.com/live_chat?continuation=`, // ここのcontinuationパラメータが空でも通る
      },
    },
    request: {
      useSsl: true,
    },
  },
  continuation: "xxxxxxxxx", // さっき取得したcontinuation
};
// 実際にブラウザで配信ページを開いた場合、上記に加えてテレメトリっぽいデータがマシマシに乗っかっています。
```

上記のJSONを、以下のURLに対してPOSTします。headerの`Content-Type: application/json`もお忘れなく。
`https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key={key}` {key}はさっき取得したやつ

## 4. コメント取得(レスポンス)
コメビュにとってはどうでもいい情報が大量に載っていて、ざっくり言うと以下の`GetLiveChatResponse` です。
https://github.com/pasta04/unacast/blob/master/src/main/youtube-chat/%40types/yt-response.d.ts

以下、必要なところだけをかいつまんだサンプルレスポンスです。

```js
const response = {
  continuationContents: {
    liveChatContinuation: {
      // 配列ですが、基本的に0番目だけ取得でよさそうです
      continuations: [
        {
          // 個人チャンネルの配信はこっちの属性で指定されることが多い
          invalidationContinuationData: {
            continuation: 'xxxxxxxxxxxxxxxxx', // 次のコメント取得の時に指定するcontinuation
          },
          // 公式チャンネルの配信はこっちの属性で指定されることが多い
          timedContinuationData: {
            continuation: 'xxxxxxxxxxxxxxxxx', // 次のコメント取得の時に指定するcontinuation
          }
        },
      ],
      // 以下はレスポンスに含まれないことがあるので注意
      // チャットなどの分だけactionsの要素が含まれます
      // 初回取得時のみ、配列の末尾要素にチャットルール用のメッセージが入り込むので適宜除外してください
      actions: [
        {
          item: {
            // チャットの種類によって入ってたり入ってなかったり
            liveChatTextMessageRenderer: {
              message: {
                runs: [
                  {
                    text: 'ここにチャットメッセージ',
                  },
                  // メッセージに特殊絵文字（画像）が含まれている場合、テキストと画像が配列の個別要素として返却されます
                ],
              },
              authorName: {
                simpleText: '投稿者のなまえ',
              },
              authorPhoto: {
                thumbnails: [
                  {
                    url: 'アイコン画像のURL',
                  },
                ],
              },
            },
            // スパチャもこの辺に属性が増えてます
          },
        },
      ],
    },
  },
};
```

あとは上記レスポンスで取得した`continuation`を使って`3. コメント取得(リクエスト)`のPOSTを投げれば、次のチャットメッセージを拾うことができます。