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

# はじめに
あの[RIAAにDMCAされた](https://github.blog/2020-11-16-standing-up-for-developers-youtube-dl-is-back/)[youtube-dl](https://github.com/ytdl-org/youtube-dl)です。
これのextractor(動画情報を抽出する部分)の書き方を適当に書きます。適当なので分からないところがあったらごめんなさい。

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

## youtube-dlの大まかなアーキテクチャ
youtube-dlは大きく分けて3段構成で、次のような感じになっています。
今回扱うextractorは動画情報を抽出するだけ。downloaderは動画を実際にダウンロードするだけ。postprocessorはダウンロードした動画に後処理を行います。[^pp]
downloaderやpostprocessorは動画配信プロトコルの大御所はほぼ全て実装しているので、あまり手を付ける必要はありません。


```
+-----------+     +------------+     +---------------+
| extractor | --> | downloader | --> | postprocessor |
+-----------+     +------------+     +---------------+
```

# ステップ0: どこにするか決める
とりあえず無難そうな[whowatch](https://github.com/nao20010128nao/ytdl-patched/blob/master/youtube_dl/extractor/whowatch.py)[^intname]にします。配信は適当にトップページから選択。

# ステップ1: Webサイトの観察
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/d02e2f3e-97f3-ca6a-5cc0-f37a6f9fdd45.png)

[適当な配信を持ってきました](https://whowatch.tv/viewer/18667149)。クリックすれば再生できそうです。

とりあえず再生してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/239953af-9b06-4429-998f-2b649490a2fd.png)

再生できたので大丈夫でしょう。[^playable]

# ステップ2: 開発者ツールを開く
開発者ツールを開き、どこに動画データがあるか見てみましょう。
Firefoxなら`Ctrl+Shift+I`で開きます。[^rightclk]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/85cf5d89-a6e8-fe38-cfea-41bb9bc8ed7c.png)

開いたウィンドウの`ネットワーク`(`Network`)タブを開くと、こうなります。
ところで、HTML5での生配信はどのような形式で行われるのでしょう? 実は大体はHLSで行われています。
HLSのプレイリスト[^hlspl]は`.m3u8`という拡張子なので、これで検索してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/a8c1db48-5b6a-6484-1275-04cadb533ade.png)

なんか意味深なファイル名がありました。この`playlist.m3u8`をyoutube-dlに投げ込んでみましょう。[^ytdl]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/d5a51e70-2f2d-0404-bbae-7f3d8f983763.png)

ffmpegが起動してダウンロードが始まりました。
ということは、このURLを生配信視聴ページから取り出す/導くことができれば勝ち、ということになります。

# ステップ3: 導出方法を検討する
でも、これを毎度毎度人手で取り出すのは本末転倒です。自動化しなければなりません。

Firefoxでは便利なことに[^searchchrome]、ダウンロードされたリソースの中からテキストを検索する機能があります。
先程の検索欄の右側の虫眼鏡をクリックするとこうなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/a2eb3888-40c0-a79a-d988-5fef5ccfbe1b.png)

左側に出現した検索欄に何かを入力すると、ダウンロードされたリソースから検索出来てしまうのです。便利。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/7951332f-b8d5-8d17-e07a-bc1a2322baec.png)

では、先程のm3u8のファイル名を入れてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/e14d0572-6e48-422d-2ee9-64592db25290.png)
それっぽいのが出てきました。
上から、

- Web AppのJavascriptファイル
- APIリクエスト
- playlist.m3u8

の順番で並んでいることが分かります。

APIリクエストが一番怪しいので、中身を見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/59b7f966-a68c-02dd-f81f-efdd0cc803a1.png)

当たりを引きました。これを取り出しましょう。

URLは`https://api.whowatch.tv/lives/18667149/play?referer=https%3A%2F%2Fwhowatch.tv%2F`でした。

# ステップ4: youtube-dlに実装する
forkやらcloneやらは全て飛ばします。本題ではないので。

まず、適当にファイル名を決めて、`youtube-dl/extractor/[ファイル名].py`に次の内容をとりあえずコピペします。

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

# 最低限のimport
from .common import InfoExtractor


# ClassNameは置き換えること
class ClassNameIE(InfoExtractor):
    # このextractorが受け付けるURLの正規表現
    _VALID_URL = r''

    # 実際の抽出コードを_real_extractに書く
    def _real_extract(self, url):
        # 正規表現のIDパートを取り出す。
        video_id = self._match_id(url)
        return {}
```

ファイル名は`whowatch.py`にして、`ClassNameIE`は`WhoWatchIE`と置き換えることとします。

まず、正規表現を考える必要があります。しかし、このタイプのURLは非常に単純なので簡単にできますが、次のような条件を満たす必要があります。

- 必ず`id`という名前のついたグループを作る
    - URLにあるIDをキャプチャさせます
- 不必要なグループは入れない
    - 複数文字の任意部分を指定したい時は、`(?:test/)?`のようにグループにならないようにします

とりあえずこんな感じで良さそうです。

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

from .common import InfoExtractor


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

    def _real_extract(self, url):
        video_id = self._match_id(url)
        return {}
```

正規表現も細かくはやりません。本題ではないので。

正しく`id`という名前のついたグループを作れば、`self._match_id(url)`はグループ`id`を持ってきてくれます。

## ステップ 4.1: 試してみる
このコードで検証してみましょう。
`youtube_dl/extractor/extractors.py`に先程の`WhoWatchIE`を参照できるようにimportを追加して、

```python:youtube_dl/extractor/extractors.py
from .whowatch import WhoWatchIE
```

youtube-dlに与えてみます。

```
$ python3 -m youtube_dl https://whowatch.tv/viewer/18667149
ERROR: Missing "id" field in extractor result; 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.
```

エラーが出てきましたが、genericとはなっていないので成功です。

## ステップ 4.2: IDとタイトルを返す
`InfoExtractor`(`extractor`)の`_real_extract`は`dict`で情報を返します。IDは先程`self._match_id`で取り出しましたが、タイトルはそうは行きません。

ステップ3のAPIリクエスト中にm3u8のURLの他にタイトルが入っていたのを覚えていますか? あったんです。
なので、このJSONをダウンロードして取得しましょう。
APIリクエストのURLは`https://api.whowatch.tv/lives/18667149/play?referer=https%3A%2F%2Fwhowatch.tv%2F`で、視聴URLのIDパートは`18667149`でした。共通部分に気づきましたか?

そうですね。`'https://api.whowatch.tv/lives/' + video_id + '/play?referer=https%3A%2F%2Fwhowatch.tv%2F'`で全く同じURLが再現できます。

また、youtube-dlにはJSONダウンロードとデシリアライズを一度にやってくれる便利メソッド`_download_json`があるので、これでダウンロードします。

```python
        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)
        return {
          'id': video_id,
          # 鉤括弧があったので同時に外してしまう
          'title': live_data['share_info']['live_title'][1:-1]
        }

```

# ステップ 4.3: m3u8から情報を取り出す
次に、m3u8から情報を取り出してダウンロードに必要な最低限の情報を揃えます。ここでも`_extract_m3u8_formats`というそのままの名前の便利メソッドが活躍します。

```python
        # HLSのURL
        hls_url = live_data['hls_url']
        # とりあえず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
        }
```

# ステップ 4.4: 生放送であることを宣言する

```python
        return {
          'id': video_id,
          'title': live_data['share_info']['live_title'][1:-1],
          'formats': formats,
          # これは生放送です
          'is_live': True,
        }
```

これで最低限まともに動きます。試してみましょう。

# ステップ 5: Let's Try
できました。ダウンロード出来ています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111609/1577fbe0-07dd-5da9-9eb4-b0b6b933d348.png)

# おわりに
今回は、youtube-dlのextractor書き方の超基本的な所を書きました。
次に、エラーハンドリングやユーザーに対してエラーや警告をどうやって表示するのかをやっていきたいと思います。

適当かつ大急ぎで書いたので結構荒削りになってしまったことをお許し下さい。

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

次: [第2巻](https://qiita.com/nao20010128nao/items/ef0af6e9a356013e69dd)

## 付録: 今回のコード

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

from .common import InfoExtractor


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)

        # HLSのURL
        hls_url = live_data['hls_url']
        # とりあえず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,
        }

```

今回のコードは[ここにも置いておきます。](https://github.com/nao20010128nao/youtube-dl/blob/qiita_explaination/youtube_dl/extractor/whowatch.py)

[^pp]: よく目にするのは映像+音声の結合ですが、これに限りません。例えば、`--exec`オプションはpostprocessorで扱います。
[^intname]: 正式な名前はふわっちです。なお以降内部名で書きます。
[^playable]: 必ず[再生できなければなりません。](https://github.com/ytdl-org/youtube-dl/blob/master/.github/ISSUE_TEMPLATE/4_bug_report.md)
[^rightclk]: 分からなければ右クリックメニューに`要素を検査`があるので、それでも構いません。
[^hlspl]: ここでは、細切れにした動画のファイル名が示されたファイルのことです。YouTubeなどのプレイリストは一旦忘れて下さい。
[^ytdl]: スクリーンショット内では`ytdl`とaliasを張っています。同名のコマンドを提供するytdl-pafyとは無関係です。
[^searchchrome]: Chromeにも同様の機能はあると思いますが、もう何年も触ってないので知りません。適当なので。