放送大学のインターネット配信授業をダウンロードするCLIツールを作った

この記事は放送大学 Advent Calendar 2018 17日目の記事です。

過去記事で放送大学のインターネット配信授業のダウンロード、字幕変換&合成まで行いました。今回作ったのはそれらを大体自動化してくれるCLIツールです。

作ったもの

mtane0412/ouj-downloader

放送大学の学生アカウントを持っていればダウンロードしたい科目を選択してダウンロードすることができます。科目のチャプター選択や字幕オプションなどが選択可能です。なお、このモジュールの他にlibass付きでffmpegをビルドしておく必要があります。

Gyazo GIFの時間制限内で急いでキャプチャしたやつです。科目を選択するのはあらかじめAPIをローカルに保存しておくのでレスポンスは速いです。

ダウンロードや字幕作成はプログレス表示されます。

検索や字幕がある動画は字幕オプションもあります。(通常動画+字幕ファイル(.srt)+字幕合成動画)

保存先は実行ディレクトリで科目名のフォルダを作成して動画を保存します。字幕オプションを選択した場合は字幕ファイルと字幕合成した動画も保存します。

科目名
├{第1回タイトル}.mp4
├{第2回タイトル}.mp4
├ ...
├subtitles
|├{第1回タイトル}.srt
|└ ...
└subbed
├{第1回タイトル(字幕)}.mp4
└ ...

ダウンロードしたmp4ファイルにはiTunes等で表示されるメタデータが付与されています。科目がアルバムになっているので科目ごとに分類でき、各回の出演者がアーティスト欄に表示されます。(アルバムartistは放送大学です。)なお、アートワークが必要な場合は自分で入れてください。(下のロゴは自分で入れました。)

勉強になったところ

puppeteer

配信授業一覧については内部APIを叩いて取得できるので、requestなどのHTTP Clientでもいけるかな?と思ったのですが、リダイレクト中に付与されるセッションIDを取得するのが意外と難しかったのでPuppeteerで実装しました。

puppeteerで取得しているものは具体的には以下のものです。

  • ログインクッキー(下のアクセスに必要)
  • 内部API(初回、ouj-downloader update実行時)
  • ダウンロードする動画のauthTicket(1科目ごとなので通常最大15回。大学院で30回?)
  • オプション:字幕ファイル(SAMI)

puppeteerでうっかりループしてしまうとpossible EventEmitter memory leak detectedが発生してしまいます。イベントリスナの上限数を上げるという力技もありますが、ループごとにブラウザを立ち上げては消して…、というのを繰り返すのが正攻法っぽいです。今回はログインが必要なのでセッション情報があるクッキーを取得しておいて、ブラウザを立ち上げる都度にクッキーをセットすればログインセッションを維持することができました。

引数がスプレッド演算子だったことに気づかずに結構ハマりました。

// クッキーの取得
const cookies = page.cookies();
// クッキーをセット
page.setCookie(...cookies);

次にjsonの取得についてです。ログインだけpuppeteerでやってあとはrequestでできないかとも思ったんですが、ログインセッションのあるアクセスに対してまた別のクッキーを…みたいに割と複雑で取得が難儀したので素直にそのままpuppeteerで行うことにしました。puppeteerでのjsonの取得はstack overflowを参考にしました。jsonに直接飛んでbodyを取得するというやつですね。

このツールは依存パッケージとしてPuppeteerを含んでいるのでChromiumも当然ダウンロードすることになり、かなり容量があります。puppeteer-coreの方を使えばいいんですが、chromiumのpathをユーザーに通してもらうことを考える必要があり、色々考えたのですがどうせ誰も使わないしいいかということでそのままです。

Stream APIと再帰関数

これらを取得後はセッションを終了して、node-rtmpdumpでストリーミングをdumpして、fluent-ffmpegで動画を出力しています。ここらへんはNodeのStream APIを触るいい機会になりました。ffmpegも色々できそうで楽しいですね。

“「Stream を制するものは、Node.js を制す」と言われている”というのを各所で見かけましたが結局誰が言ったのか…。ともかく、PromiseとStreamにふれる機会になったのでJavaScriptの非同期処理の勉強にはなりました。

また、今回は授業回数分ダウンロードする必要があり、しかし普通にループを回してしまうと授業15回分のダウンロードが並列して行われるということになりサーバー負荷的にもよろしくなさそうです。(そもそもリスナ上限で不可能かな。)今回はStreamを一つずつ同期的に行うために再帰関数を使用しました。

NodeのCLI周り

今回は定番のcommander.jsに加えて、メインは対話モードにしようと決めていたのでinquirer.jsを使用しました。便利ですねこれ。

ダウンロード中のプログレス表示は以下を参考にしました。

rtmpdumpからffmpegにpipeした場合は進捗のパーセント表示が取得できなかったのでAPIから取得したdurationで計算してます。

反省点は最初は放送大学の内部APIをそのまま利用する形で実装したので、科目の特定が結構複雑になっちゃったところですかね。

正規性が疑わしいデータは最初に正規化をする

放送大学の内部APIは多分何か別のシステムをそのまま流用したっぽく、科目のカテゴリー情報のあるcategories.jsonとそれぞれの授業情報があるvod-contents.jsonの2つに分かれています。この時点でそもそも一つにすればよくね?という感じなのですが、当初は特に考えずにAPIをそのまま利用する形でコードを書いていました。

ところが、カテゴリーによって同名のフィールドに格納するデータがまるで違ったり、区切り文字に違うものが使用されていたり、果ては一部の動画は字幕が全部画像になってたりしていることが後から判明してしまい、利用しやすい形に正規化したものを対症療法的に作りましたが、すべてを書き換えてないので依然としてカオスな状況です。。

ともかく、内部的に利用されているAPIなど正規性が疑わしいものは多少面倒でも最初に正規性チェックして徹底して正規化しておくことが大事だと勉強になりました。。。つらい。

おわりに

ダウンロードは一科目単位で一つずつ順番に、などサーバー負荷にも一応配慮はしているつもりですが、Librahack氏のような件もあるので利用は自己責任でお願いします。(GPL-3.0で配布しているのでこのソフトウェアは無保証です。)

今回はダウンロードですが、CLIで科目申請や各種手続きができても面白いかなと思いました。ただ仕様が変わるたびに作り直さないといけないので大変そうです。