ES2018のAsyncIterationとfor-await-ofでAPIのページング処理をする

# はじめに
ES2018について調べてたらfor文で回せるAsyncIterationを知ったので
APIのページング処理ができたらスッキリ書けるのではないかと思って実装してみた~~が~~

# Async Iteration

詳しくは他にわかりやすい解説記事があるのでざっくりと説明すると状態を非同期で返すイテレータ~~それだけ~~

[ES2018 Async Iteration](https://qiita.com/printf_moriken/items/f11c63296c60bb5d6ee2)

# YouTube Data API
APIはなんでもいいですが、よくあるnextPageTokenなどを使う複雑なYouTubeDataAPI
完了状態がわかりやすい再生リストの動画を取得する[PlaylistItems: list](https://developers.google.com/youtube/v3/docs/playlistItems/list?hl=ja) を使う
[googleapis](https://www.npmjs.com/package/googleapis)などのライブラリを使ってもいいが
AsyncIteratorに相性が良さそうな[node-fetch](https://www.npmjs.com/package/node-fetch)を使う

# APIの仕様

`https://www.googleapis.com/youtube/v3/playlistItems`に必要なクエリを載せてGETリクエストを送信

必要なAPIキーを指定する`key`とクエリは欲しいデータを示す`part`と再生リストを指定する`playlistId`、
さらに効率よく取得するために`maxResults`で１度に帰ってくる動画の数を最大の50にします

すべて取得したい場合は帰ってきたデータにあるpageTokenを指定して再度リクエスト
これをひたすら繰り返し、pageTokenがなくなったら終了

詳しくは公式ドキュメントを参照(日本語)
[PlaylistItems: list | YouTube Data API (v3) | Google Developers](https://developers.google.com/youtube/v3/docs/playlistItems/list?hl=ja)

# 実装

APIキーは環境変数`APIKEY`に入ってるものとして実装していきます
環境変数を設定するか適宜書き換えてください
APIキーの取得方法は[こちら](https://qiita.com/chieeeeno/items/ba0d2fb0a45db786746f)

再生リストはなんでもいいですが、ページング処理の恩恵が分かるように動画数が多い再生リストを
探した結果、GoogleDevelopersにちょうどいいプレイリストがあったのでこれを使います

Google I/O 2018 - All Sessions
<https://www.youtube.com/playlist?list=PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y>

`playlist?list=`の後に続く文字列が再生リストのIDですね

まずは普通にnode-fetchを使って取得してみます

すべてを表示してしまうとあっという間に文字列だらけになって
見づらくなるのでサンプルは配列の長さのみ出力します

## 普通に取得

```js
const fetch = require('node-fetch')
const querystring = require('querystring')

const qs = '?' + querystring.stringify({
  part: 'snippet',
  playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
  maxResults: 50,
  key: process.env.APIKEY,
})

fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
  .then(res => res.json())
  .then(data => console.log(data.items.length))
  .catch(err => console.error(err))
```

これで最初の50件を取得できる
今まではこれ以上必要な場合は以下のように取得する部分を関数で囲って再帰的に実行していました

## 再帰的に実行して取得する

```js
const fetch = require('node-fetch')
const querystring = require('querystring')

function getListItems(callback, items = [], pageToken) {
  const qs = '?' + querystring.stringify({
    part: 'snippet',
    playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
    maxResults: 50,
    pageToken: pageToken,
    key: process.env.APIKEY,
  })

  fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
    .then(res => res.json())
    .then(data => {
      items = items.concat(data.items)
      if (!data.nextPageToken) callback(null, items)
      else getListItems(callback, items, data.nextPageToken)
    })
    .catch(err => callback(err))
}

getListItems((err, items) => {
  if (err) console.error(err)
  else console.log(items.length)
})
```

再帰的に実行されると知っていれば読めるものの、ぱっと見どう動いてるのかよくわからないので
本題であるfor文(for-await-of)で綺麗に書けそうなAsyncIteratorで実装していきます

## asyncIterable & for-await-of で取得する

```js
const fetch = require('node-fetch')
const querystring = require('querystring')

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let pageToken = ''
    const qs = '?' + querystring.stringify({
      part: 'snippet',
      playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
      maxResults: 50,
      pageToken: pageToken,
      key: process.env.APIKEY,
    })

    return {
      async next() {
        const data = await fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
          .then(res => res.json())
          .catch(err => console.error(err))
        pageToken = data.nextPageToken
        return { value: data.items, done: pageToken }
      },
    }
  },
}

!(async () => {
  const items = []
  for await (const item of asyncIterable) {
    items = items.concat(item)
  }
  console.log(items.length)
})()
```

for-await-of文で１ページづつ同期的に回せるようになったものの
ページごとになにか処理でもしない限り特に嬉しいことはないですね

なので、取得できたらメモリ上に置いといて、for文で１つづつとりだし
それがなくなったら再度取得するようにしてみます

## asyncIterable & for-await-of で１つづつ回す

```js
const fetch = require('node-fetch')
const querystring = require('querystring')

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let list = []
    let pageToken = ''
    const qs = '?' + querystring.stringify({
      part: 'snippet',
      playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
      maxResults: 50,
      pageToken: pageToken,
      key: process.env.APIKEY,
    })

    return {
      async next() {
        if (!list.length && pageToken) {
          const data = await fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
            .then(res => res.json())
            .catch(err => console.error(err))
          list = data.items
          pageToken = data.nextPageToken
        }
        return { value: list.shift(), done: pageToken || list.length }
      },
    }
  },
}

!(async () => {
  for await (const item of asyncIterable) {
    console.log(item.title)
  }
})()
```

動画１つづつに対して処理をするのならかろうじて用途があるのかなって感じです

# 結論

[便利なライブラリ](https://www.npmjs.com/package/youtube-playlist-info)があるので使いましょう

# あとがき

ブログとかをページ単位でスクレイピングして保存するのなら、まだ使いみちありそうかな？