Pushing latest episodes #3

次にアプリケーションサーバとなるここに関して。

アプリケーションサーバ側でもFirebaseが提供する、Firebase Admin SDKと呼ばれるライブラリを利用できる。ただしサポートされるのはNodeJavaPythonC#Goであり、phpはない。そんなわけで非公式のphp向けライブラリがあるのでそれを使う。どうしてphp版を公式に出さないんですか…どうして…。それはそれとして、php向けライブラリはドキュメントも充実しているのでありがたい。

アプリケーションサーバでやることは、クライアントから渡されたトークン、番組のシリーズIDをデータベースに保存し、番組表の取り込み時に該当するシリーズの最新話を見つけたらPush通知を行う…といった感じである。やること自体は割と簡単。

このシリーズIDとは何かというと。まず番組表は番組枠が列挙してある。番組枠とは時間で区切られた区間のことだ。この枠に番組が収まっている。この仕様から見れば、番組枠の中に複数の番組を入れることも不可能ではないので、データ上も番組枠(slot)にぶら下がっている番組(programs)は配列になっている。

シリーズIDはこのprogramsの各要素に生えているseriesオブジェクトのidプロパティであり、定義がないので断言はできないが、これが配信される作品全体に対して振られたユニークなIDのようである。また同様にprogramsの各要素に生えているepisodeオブジェクトのsequenceプロパティが該当シリーズにおける放送順を示している、ようである。

したがって、データベースにシリーズIDとその最新シーケンスナンバーのセットを保持しておき、番組表を取り込んだ際に照合して追跡中のシリーズでありかつ新しいシーケンスナンバーを持っている番組枠があれば、それが最新話ということになる(実際には、さらにmarkフラグなども見る)。

ところがここでひとつ問題がある。上記のシリーズIDの正規形は

\d+-\d+
らしいのだが、ときおりこのフォーマットから外れたものがある。例えばシリーズIDが
175-1rthzhecdme
といったものになったり、シーケンスナンバーが20とか80とか突拍子もないものになったりする。これはどうも「仮のシリーズID」的なもので、未来の番組表に初めて現れた最新話とかがこの状態になったりするようだ。そして、実際の配信日になると本来のシリーズIDが割り振られる。

この仕様がどういう意味を持っているのか分からないが、とにかく不完全なシリーズIDは最新話を検知するための足掛かりにならない。そこで、シリーズIDを元にした検知と平行して、タイトルによるヒューリスティックな検知も行うことにする。これは、例えば検知を開始した番組のタイトルを覚えておいて、それと似た感じなら同一シリーズと見なすものだ。似た感じというのはリーベンシュタイン距離とかそんな感じのアレである。

ところでここで使用する番組のタイトルというのは、正確には番組枠のタイトルである。これは配信される作品のタイトルとは独立しているため、物によって【WEB最速・単独最速】だとか【地上波先行・先行配信】だとか、様々な枕詞が付いたりする。これはヒューリスティックな検知にはノイズなので困る。seriesオブジェクトに作品自体のタイトルを示すプロパティが格納されていると嬉しいのだけど。

というわけでAbema.tvの番組表データはいろいろと不思議な点が多い。

Pushing latest episodes #2

ということで、ブラウザ側としては拡張機能内でFirebase SDKを使用する構成にする。この組み合わせは、一応Firebaseのドキュメントでも公式に動作が保証されている。ただし、どういうわけか知らないが拡張機能内でFirebaseを使用する際のサンプルやガイダンスはドキュメントに一切ない。そのため備忘録として気付いたことをメモしておく。

ちなみに、上記の保証というのはSDKがchrome-extension:スキームを認識するということである。これは今のところSDKはFirefox上のWebExtensionsでは動作しないことを意味する。したがって、あべ☆アニの最新話の追跡機能もまた、Firefoxでは動作しない。

  • 拡張にSDKを組み込む。これは具体的には
    <script src="https://www.gstatic.com/firebasejs/8.2.1/firebase-app.js"></script>
    てな感じの要素をバックグラウンドページに仕込んでおくことである。この場合、外部サイトのファイルを直接指定しているので、manifest.jsに
    "content_security_policy": "script-src 'self' https://www.gstatic.com; object-src 'self'"
    といったCSPの記述も必要。また、この拡張をストアにアップする際にリモートコードを使用している旨の告知とその理由が必要になる。
  • Push通知を受けるためにどこかでサービスワーカーを登録しなければならないのだが、Firebase SDKを使う分には明示的にそれをする必要はない。firebase-messaging-sw.jsという名前のファイルをサイトのルート(つまり拡張のアーカイブのルート)に置き、必要な処理をそこに書いとけばいい。ワーカーの登録はFirebaseが勝手にやる。登録済みのregistrationを元にFirebaseアプリケーションオブジェクトを構築することもできるが、Firebase側でよしなにしてくれることをわざわざやることもないだろう。なお登録は不要だがサービスワーカーからのメッセージはリスンする必要があるので、
    navigator.serviceWorker.addEventListener('message', aFunctionDefinedSomewhereElse);
    とかはしておく。
  • Push通知を受けるためのトークンを
    firebase.messaging().getToken()
    によって取得することができる。これによって得られる文字列は永続的なものであり、一度取得したら永続的なストレージに入れて使い回していい。この手のトークンにはたいていrefreshイベントをリスンして期限切れに備えましょう的なノウハウがあったりするが、必要ない。onTokenRefreshイベントは用意されているがすでにdeprecateである。
  • このトークンは通知の宛先となるものなので、通知を出す側であるアプリケーションサーバへ送り付ける。さてこれが漏れると勝手に通知を出されるので厳重に暗号化した上で適切に送信しなければならない…はずなのだが、ドキュメントでは単に「トークンを取得したら、それをアプリサーバーに送信して、適切な方法で保管します。」としか記述されていない。送信のためのベストプラクティスとかはないのである。単にpostすればいいらしい。本当? とりあえずあべ☆アニextensionではblowfishで暗号化した上でここのサーバに送信している。まあ拡張のソース見ればすぐバレちゃうけど。
  • Push通知を受けるには、サービスワーカーにおいてもFirebase SDKのコードをimportScript()して、必要な情報を元にFirebaseアプリケーションオブジェクトを生成し、onBackgroundMessageイベントをリスンする…とドキュメントには書いてあり、それが正道なんだろうが、実は単に通知を受けるだけならワーカー自身のpushイベントをリスンするだけで事足りる。FirebaseはPush通知以外にも様々な機能が統合されているので、通知を受けるとともにそれらの機能を利用する場合はドキュメント通りのやりかたが正しいのだろう。あべ☆アニextensionでは通知を単に受けるだけで良いので、直接pushイベントを見ている。importScript()云々もしていない。
  • サービスワーカーで通知を受けたらデータペイロードを拡張のバックグラウンドページへ送信する。ここで困るのはバックグラウンドページがイベントページ、つまり用が済んだら勝手にアンロードされるモードの場合だ。この場合、サービスワーカーのClients#matchAll()ではバックグラウンドページを取得できないし、起こすこともできない。どうするか。実はサービスワーカーのグローバルオブジェクトにchrome.runtime.getBackgroundClient()というものが生えており、それを呼ぶと必要ならバックグラウンドページを起こした上でそれに対応するClientオブジェクトで解決されるPromiseを返すようになっている。このメソッドはChromeの拡張APIドキュメントのどこにも載ってない、この記事だけのかなりのオトク情報である。まあいずれ載ると思うけど。

こんなもんかな。

Pushing latest episodes

あべ☆アニに、任意の番組の最新話を自動的に通知予約する機能を追加している。

そのためにはまず最新話を検知する処理をどこに置くかということを考えないといけない。番組の通知予約機能自体に立ち返ってみると登場人物が3者いる: ここのサーバ、ブラウザの拡張機能(のバックグラウンドページ)、あべ☆アニのページである。

番組の開始時刻になったことを検知し、タブを開く処理を置くのにどこが相応しいかを考えると、あべ☆アニのページはそれを開いている時しか機能しない。サーバ側でやってPush通知をするとなると通知が番組開始1分前に確実に届くかは保証できないし、通知を受けるサービスワーカーから行えるタブ操作が微妙に痒いところに手が届かない。と言うわけで、ブラウザの拡張機能にアラームをしかける実装になっている。

では、最新話の検知機能はどうか。それを行うには番組表の全ての枠を走査しなければならないが、まずあべ☆アニのページにしても拡張機能にしても個々のクライアントがやるべき処理ではない。一方で元々ここのサーバが番組表をキャッシュするために定期的に番組表を取り込んでいるのだから、その一環として最新話かどうかを判断する処理を入れるのが最も収まりがいいだろう。

そんなわけで検知機能はサーバ側に置き、見つけたらPush通知により個々のクライアントに送りつける構成にしよう。Push通知に関してはWeb PushのAPIをそのまま使うと妙にやることが多くてめんどくさいので、Firebase SDKを使うことにする。

ということでだいたいできて今dog-foodingしているところである。今月中くらいにはリリースしたい。

finding time shifted programs

abema.tvでへやキャン△をやっているのだけど、配信が木曜の3:55〜という謎の時間帯で困る。

なので1週間有効の見逃し配信で見るわけなのだが、見逃し配信が有効な過去の番組枠をあべ☆アニから検索するのがめんどくさい。

ということで検索時にそういうものも対象とするようにした。

The TV program time table #9

虹裏でアナウンスした際に Vivaldi で動かした時、お気に入りのチャンネルを定義する場合のドラッグ&ドロップが正常に動作しないという報告を受けた。

調べてみると、ドラッグ&ドロップのためにはいくつかのイベントを処理しないといけないわけだが、その中でドロップされた際の drop イベントと、ドラッグプロセスが完全に終了した際の dragend イベントの発行される順番の問題のようだ。

仕様では、常に drop -> dragend の順番であり、あべアニもそういう前提で組んであり、実際 Chrome、Opera、Firefox、Edge でテストした際もそういうふうに動作した。Vivaldi ではテストしなかったのだが、これは Vivaldi は結局 Chrome ファミリーであり、Chrome で動けばきっと多分 Vivaldi でも同様のはず…という判断なのだが、びっくりすることに Vivaldi では drop イベントが最後に発行するようなのである。

なんで…?

前述の通り drop -> dragend なのはそういう規格だし、Chrome 自体がそういうふうに動作するのに、何を意図して変更したんだろう? 分からないがとにかく、いずれの順番でも正しくドラッグ&ドロップが動作するように修正。

The TV program time table #8

Firefox 版は AMO に置くのを諦め、ここでホストすることにした。

Chrome 版のソースを Firefox の WebExtensions のシステムで動かす際、以下の点が Chrome と異なる。

  • chrome.runtime.onInstalled がまだない。したがって、
    'onInstalled' in chrome.runtime && chrome.runtime.onInstalled.addListener(function () { });

    みたいな書き方をする必要がある。

  • content script で生成した CustomEvent にオブジェクトを含んだ detail を与え、ディスパッチする。それをページスクリプトでリスンすると、detail が正しく引き渡されない。Firefox では権限の異なるスクリプト間ではプリミティブな型以外のものを渡せないようだ。そんなわけで、Firefox で動かす際は detail には JSON.stringify() したものを与え、受け取る際も JSON.parse() しないといけない
  • ソースはほぼ Chrome 版と共有できるが、パッケージングはかなり異なる。ストアに登録する際は Chrome 同様にソースディレクトリを単に zip するだけでもよいが、web-ext ツールを用いてビルドと署名をコマンドラインから行う方法も用意されている。
  • 自前でホストする場合、更新情報も自分で管理しないといけない。そのために、manifest.json に
    "applications": {
        "gecko": {
            "id": "abeani-extension@appsweets.net",
            "strict_min_version": "42.0",
            "update_url": "https://appsweets.net/abeani/updates.json"
        }
    },

    というものを含める必要がある。このエントリは、Chrome は無視するのでこの manifest を Chrome に与えてもエラーにはならない。updates.json の内容は

    {
        "addons": {
            "abeani-extension@appsweets.net": {
                "updates": [
                    {
                        "version": "1.0.8",
                        "update_link": "https://appsweets.net/abeani/extension/AbeaniExtension.xpi"
                    }
                ]
            }
        }
    }

    こんな感じ。

    調子に乗って Edge でも動かしてみたのだが、まず Edge は chrome.alarms がないので普通のタイマーで代替しないといけないのだが、それは代替手段があるのだから別にいい。だがなんと、content script から chrome.runtime.sendMessage() でバックグラウンドにメッセージを送る機能がまだ実装されていない。これがないとどうにもならない。ひどい。というわけで Edge 版はしばらくペンディングだ。MS さん真面目にやってくださいよ。

The TV program time table #7

  • 表示できるチャンネルをアニメ以外にも拡大した: 各チャンネルを適当なカテゴリで分けたが、このカテゴリでいいの? というのは若干ある。REALITY SHOW は果たしてドキュメンタリーなのか? とか。あるいは、よくわからないチャンネルはすべてバラエティに突っ込んだがそれでいいのか? とか。これは文句が出たらその都度対応することにしよう
  • 各チャンネル群を従来は table 要素でマークアップしていたが、CSS flex を用いるようにした。これによりナウじゃないブラウザでは表示できなくなった可能性がある。しかし 21 世紀も 16 年も経って flex に対応していないブラウザが存在していたならば、それはそれが一方的に根本的に全て何もかも悪い。という方向でそろそろ行きたい。なにしろ flex 自体は Presto Opera ですら対応しているのだ(といいつつ、Presto Opera での表示確認は一切していないのだが)
  • 各番組の放映開始に併せて、虹裏では番組の実況スレというものが立つことが多い。このスレッドの本文を生成する助けになることを意図して、共有機能を実装した。共有機能というのはなんかよく意味がわからないが、生成したテキストをそのままツイートできるようにした上での命名だ。というのはこのツイート機能がないと「実況スレ本文生成」みたいな更によく分からない機能になってしまって、これを呼び出すためのボタンのラベルが冗長になってしまうからだ
  • ところで本文を生成する際、特定のマーカーは番組の情報に置換される。たとえば [start-at] が 2016/12/06(火) といった具合だ。ここで、2016年12月06日 の形式がいい! といった場合にはマーカーのオプションとして書式を付加することができる。[start-at{%m月%d日(%a)%p%l:%M}] などと書くと {〜} の中身を strftime(3) で評価した結果で置換される
  • 番組開始通知時のオプションとして、番組終了時に自動的にチャンネルのタブを閉じる機能を追加した
  • 特にチャンネルの拡大を虹裏 img でアナウンスした時なのだが、投げ銭してもいい的なレスをいくつか受けた。ありがたいことだが、しかし所詮は「」の言うことであって、9割方口だけである。が、人の心を失っていないまっとうな「」もいることを期待して、一応そのための窓口は設けた。はやくお酒飲みたいなあ
  • 通知機能のためのブラウザ拡張は、相変わらずその申請が降りるまでの時間が各ブラウザで違う。Chrome は機械的に 1 時間もすれば通る。Opera は人がレビューするが、大体 3 営業日くらいで通る(土日はお休みだ)。Firefox は、なんか以前 AMO 公式のブログや、あるいはニュースメールなどでいっぱい KAIZEN してレビューも早くなったよ!ほめて!的なアピールをやたら目にしたことがあったが、全然変化ないように思える。すなわち申請からだいたい 1 ヶ月はかかる見込みである。ぶっ飛ばされたいのか。wasavi と同様 Firefox 版だけはストアに置かない形式にするかもしれない

The TV program time table #6: regalized program name

あべアニで番組開始の通知をする際「のんのんびより 開始1分前です」的な読み上げを行わせることができる。PhoneticNews に引き続き、この読み上げは voicetext を利用している。ただまあ仕方ないことだとは思うが、たまに正しく読み上げないことがある。忍ペンまん丸をしのぶぺんまんまるなどと読み上げたりする。これをなんとかできないだろうか。

アニメ作品のデータベースとしては animedb というプロジェクトがあり、その中で作品名のふりがなも管理されている。これを利用できるかもしれない。

ただ、

  • ドキュメントにも記載されているが若干表記の揺れが残っている。たとえば鷹の爪で grep すると
    $ grep "鷹の爪" google-ime-dict.txt
    ザフロッグマンショーヒミツケッシャタカノツメ  THE FROGMAN SHOW「秘密結社鷹の爪」 固有名詞
    ヒミツケッシャタカノツメザムービーソウトウハニドシヌ  秘密結社 鷹の爪 THE MOVIE ~総統は二度死ぬ~    固有名詞
    ヒミツケッシャタカノツメザムービーツーワタシヲアイシタクロウーロンチャ   秘密結社 鷹の爪 THE MOVIEⅡ ~私を愛した黒烏龍茶~   固有名詞
    ヒミツケッシャタカノツメカウントダウン   秘密結社鷹の爪カウントダウン  固有名詞
    ヒミツケッシャタカノツメ    秘密結社鷹の爪   固有名詞
    ヒミツケッシャタカノツメザムービースリータカノツメジェイピーハエイエンニ    秘密結社 鷹の爪 THE MOVIE 3 http://鷹の爪.jpは永遠に  固有名詞
    ヒミツケッシャタカノツメザムービーフォーカスベルスキーヲモツオトコ 秘密結社鷹の爪 THE MOVIE 4 カスベルスキーを持つ男 固有名詞
    タカノツメネオ   鷹の爪 NEO   固有名詞
    ヒミツケッシャタカノツメジェーピー 秘密結社鷹の爪.jp    固有名詞
    タカノツメマックス 鷹の爪 MAX   固有名詞
    タカノツメゴーウツクシキエリエールショウシュウプラス  鷹の爪GO 美しきエリエール消臭プラス 固有名詞
    ヒミツケッシャタカノツメドットジェイピーブルーレイボックスジョウカンカンゼンシンサクエイゾウ  秘密結社 鷹の爪.jp Blu-ray BOX上巻[完全新作映像] 固有名詞
    シネマトラベルタカノツメタカノツメダンシネマトラベルヘイクノマキ    シネマ・トラベル × 鷹の爪 鷹の爪団! シネマ・トラベルへ行くの巻!    固有名詞
    ヒミツケッシャタカノツメドゥー   秘密結社鷹の爪 DO    固有名詞

    などと「秘密結社」の有無、あるいは「秘密結社」に続いて空白が入っているかなどが揺れている。それともそれぞれの作品で正式名称の表記が揺れているのが正確な状態なんだろうか? よく知らない

  • 上記の例で Blu-ray BOX 上巻云々が含まれているものがあるがこれは作品名なのか? 製品名ではないのか?
  • タイトルに含まれる空白がよみがなでは省略されているが、これをそのまま読み上げさせると不自然なアクセントになってしまう。できればよみがなでも空白は維持してほしい。Google 日本語入力用の辞書ファイルなのであえてそうなっているのかと思ったら元データである animedb.yml でも同一なのでそういうわけでもないようだ

というわけで、作品名の正規化に用いるには若干難しいかもしれない。しかし膨大なデータなのは確かなので何かに利用したいなあ。

The TV program time table #5

設定機能及び番組開始時の通知機能を実装した。

番組枠のポップアップに「通知」というボタンが追加されており、それを押すと予約される。時間になると自動的に Abema.tv の該当チャンネルを開いたり、音声でガイドしたり、OS が持つ通知機能を通してメッセージを表示したりする。これらの組み合わせは設定パネルで好きにできる。

その他、

  • 予約したら、あべアニのページは閉じて良い
  • 該当チャンネルを開く際、すでに何らかの Abema.tv 上のチャンネルを開いていたらそのタブを再利用する

といった特徴がある。

このような機能を実装するとして、最も単純なのは当然、あべアニのページで setTimeout() することだ。これなら何も実装上で難易度の高いものはない。しかしやはり当然ながら、この方法だとあべアニのページをずっと開きっぱなしじゃないといけないのである。もしも実際のユーザの使用状況が、いったんあべアニを開いたらずっと開きっぱなしであるならこの方法で実装してもいいが、そうではないなら別の方法を考えないといけない。

というわけでアクセス解析を見てみたところ、だいたいあべアニを開いて閉じるまでが5分以内のユーザが62.37%、1時間以内なのが17.8%とかそんな感じだった。つまり、setTimeout 作戦ではダメなのである。

そこで、今流行りの Push API という規格を検討してみた。これは setTimeout 作戦との比較で言えば、ここの Web サーバが setTimeout の役目を肩代わりする。そして通知をすべき時間になったら、Push サーバというものを通してブラウザに通知を送る…という仕組みだ。かつてはブラウザへの Push といえばブラウザ側から接続をかけて long polling という手法がとられた。この規格を実現している Google と Mozilla の Push サーバが未だそういう手法なのかは知らないが、いずれにしても単純な http ではない特殊な接続方法でブラウザと通信する必要があるようなので、Push サーバが必要になる。

しかし検討した結果、これも実装したい機能にはちょっと力不足だった。

  • 新しい規格なので、また仕様が固まっておらず、各種言語向けのライブラリなども揃いきってない。ペイロードを含んだ通知などではかなりめんどくさい暗号化を施さないといけないので一から作るのはちょっと大変。Node.js だと楽そうなのでビルドを試してみたがここのサーバの glibc のバージョンが異様に古くて動かない
  • ここの Web サーバが通知を送るタイミングを管理するということは、一定期間ごとに適当なプログラムを自動起動させないといけない。実はここのサーバでも一応 crontab は編集できるのだが、それは1時間に1回までという限度があるのだった。もっと細かい時間単位で起動させるには別のサーバを使わないといけないので面倒
  • ブラウザが通知を受け取ったあと、チャンネルを開くとして既存のタブを再利用するという芸当が多分現状の仕様ではできない。将来、API が拡充されればできるようになるかもしれないが、今はできない

というわけで、現実的な解としてブラウザの拡張機能との組み合わせで実現することにしてちょちょっと作った。

ナウいブラウザの拡張機能といえば、なんと言っても Firefox の WebExtensions である。この際なので WebExtensions で作ってみた。実際は作ってみたというほどのことではなくて、単純に Chrome 版のソースディレクトリを与えたらだいたい動いた、すごい! という程度のものだが。

それから当然ながら Chrome 版は Opera でもだいたい動くので Opera 版も作った。

だいたい動くというのは、例えば chrome.storage.syncchrome.runtime.onInstalled など Firefox や Opera では微妙に実装されていないものがあるのでそういうものを使うのは避けないといけないということだ。このせいで拡張機能をインストールしたあといったんあべアニをリロードしないといけない。

ちなみに Chrome の拡張をパク…大いに参考にしたという意味では Edge でも動くはずだが、なんと Edge の場合 chrome.alarms すら実装されていないそうな。もうちょっとがんばってくださいよ MS さん。

The TV program time table #4

abeani-gearいくつか、ユーザごとにカスタマイズできる項目を作りたい。そのためにとりあえず歯車アイコンを配置した。

ところで最近はこの手のページ全体に対する設定やらアクションやらを担当するリンクとしていわゆるハンバーガーアイコンがよく用いられるのだが、あれすごくダサいと思う。ダサいしそれを押すことで何が起きるのか連想できないという機能上の問題も抱えている。早くこの世からなくなって欲しい。

それはさておき、設定可能な項目をつらつらと挙げてみると:

  1. 各チャンネルのロゴ、番組枠及びその詳細リンクそれぞれをクリックした際のウィンドウ名: これを _blank にすればクリックするごとに新しいタブが開く。特定の名前にすればクリックした時に既にそれが存在すれば再利用される(もちろんユーザにはウィンドウ名自体は重要ではないので、設定の際は単に「クリックした時タブを再利用するか否か」と簡略化される)
  2. 翌日の番組表へのリンクの背景として何時間分をチラ見せするか
  3. 番組枠上の詳細ポップアップの可否
  4. 表示するチャンネル、及びその並び
  5. 事前通知する場合の、通知方法

このうち 3. と 4. はどういう仕様にしたものかいろいろと考えることが必要だ。