youtube-dlのextractor書き方講座 2/4

# 前回の続き
また見てない方は[前回はここから](https://qiita.com/nao20010128nao/items/2dea7f263d0fd5ca29b9)どうぞ。
前回はextractorの基礎中の基礎を書きました。
次は、エラーハンドリングやユーザーに対してエラーや警告をどうやって表示するのかをやって行きます。

## 最終目標
[これを作ります。](https://github.com/nao20010128nao/ytdl-patched/blob/8648116f743d8ea9f58db7c2daf726903232ccda/youtube_dl/extractor/whowatch.py)

# 前回のページをもう一回ダウンロードさせてみる
ここで、前回のwhowatchの生放送を再度ダウンロードさせてみましょう。

```
$ python3 -m youtube_dl https://whowatch.tv/viewer/18667149
[WhoWatch] 18667149: Downloading JSON metadata
ERROR: An extractor error has occurred. (caused by KeyError('hls_url')); please report this issue on https://yt-dl.org/bug . Make sure you are using the latest version; see  https://yt-dl.org/update  on how to update. Be sure to call youtube-dl with the --verbose flag and include its complete output.
```

...ダウンロードできませんね

実際のウェブページではどのように見えているのでしょうか?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/a041b664-1aa8-f7a9-d60f-49a494921eb8.png)

同じ方の最新の生放送に変わりました。
そうです。同じURLの生放送は終了してしまったのです。

# 問題を切り分ける
前回までのコードに少し追加して、何が返ってきたのかを見てみることにします。

```python:whowatch.py
        live_data = self._download_json(api_url, video_id)

        # デバッグ用: live_dataを表示する
        self.to_screen(live_data)
```

その内容は、次のようになっています。

```python
{'error_code': 'L-004', 'error_message': 'その配信は終了しています。(L-004)'}
```

エラー状態の返答が分かったので、この状態かどうかを調べることでエラーハンドリングができます。
これが終わったら`self.to_screen(live_data)`はコメントアウトして下さい。

# extractorで綺麗にエラーを投げる
ここで、最初のエラーを見てみましょう。

```
$ python3 -m youtube_dl https://whowatch.tv/viewer/18667149
[WhoWatch] 18667149: Downloading JSON metadata
ERROR: An extractor error has occurred. (caused by KeyError('hls_url')); please report this issue on https://yt-dl.org/bug . Make sure you are using the latest version; see  https://yt-dl.org/update  on how to update. Be sure to call youtube-dl with the --verbose flag and include its complete output.
```

具体的な例外のクラス名は`KeyError`であると読み取れます。
dictにおける`KeyError`というのは、「キーが存在しない」場合を指します。[^pydoc_dict]

返答を見ても分かるとおり、`hls_url`はありません。この状態の時に`None`で返してくれれば簡単に区別できます。
少しdictのドキュメントを見ていくと、`get(key[, default])`という正に探していたメソッドがありますので、これを使います。

```python:whowatch.py
        # HLSのURL
        hls_url = live_data.get('hls_url')

        # hls_urlが無ければエラーを投げる
        if not hls_url:
            # ここでエラーを投げないと大変なことになる
```

こういう何らかのエラー(生放送が終了した、サーバーがなにかおかしい)を表すクラスとして、`ExtractorError`があります。

```python:whowatch.py
from ..utils import ExtractorError
```

これの具体的な引数はソースコードを見てほしいので、ここでは結論だけ示します。

```python:whowatch.py
        # hls_urlが無ければエラーを投げる
        if not hls_url:
            raise ExtractorError(live_data.get('error_message'), expected=True)
```

このようにすると、エラーは次のように変わります。

```
$ python3 -m youtube_dl https://whowatch.tv/viewer/18667149
[WhoWatch] 18667149: Downloading JSON metadata
ERROR: その配信は終了しています。(L-004)
```

この表示であれば分かりやすいと思います。
またバグの類ではないということを提示して、ダウンロードを諦めてもらうこともできます。

# おわりに
今回は、extractorのエラーの投げ方を書きました。
次回は、別の生放送を使って動画のフォーマット情報以外の情報の取り出しをやっていきたいと思います。

ここまでご覧いただきありがとうございました。

次: [第3巻](https://qiita.com/nao20010128nao/items/43d82cdd0278ae25f725)

## 付録: 今回のソースコード

```python:whowatch.py
# coding: utf-8
from __future__ import unicode_literals

from .common import InfoExtractor
from ..utils import ExtractorError


class WhoWatchIE(InfoExtractor):
    _VALID_URL = r'https?://whowatch\.tv/viewer/(?P<id>\d+)/?'

    def _real_extract(self, url):
        video_id = self._match_id(url)

        api_url = 'https://api.whowatch.tv/lives/' + video_id + '/play?referer=https%3A%2F%2Fwhowatch.tv%2F'
        # URL、IDの順で指定する
        live_data = self._download_json(api_url, video_id)

        # デバッグ用: live_dataを表示する
        # self.to_screen(live_data)

        # HLSのURL
        hls_url = live_data.get('hls_url')

        # hls_urlが無ければエラーを投げる
        if not hls_url:
            raise ExtractorError(live_data.get('error_message'), expected=True)

        # とりあえずHLSのフォーマットを検索する
        formats = self._extract_m3u8_formats(
            hls_url, video_id, ext='mp4', entry_protocol='m3u8_native',
            m3u8_id='hls')
        # 並び替える。これによって何も設定しない状態で最高画質をダウンロードするようにする
        self._sort_formats(formats)

        return {
          'id': video_id,
          # 鉤括弧があったので同時に外してしまう
          'title': live_data['share_info']['live_title'][1:-1],
          # フォーマット一覧
          'formats': formats,
          # これは生放送です
          'is_live': True,
        }
```

[^pydoc_dict]: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict `Return the item of d with key key. Raises a KeyError if key is not in the map.`