The TV program time table #4

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

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

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

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

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

The TV program time table #3

1 クール(12〜13話)分の一挙放送、といった番組は番組表においても縦方向に非常に長い面積を占める。その枠の先頭部分にタイトルとサムネイルを描画するわけなのだが残りの部分は全くの空白なので、始まって数時間もすると何を放送しているのか分からないただの枠が番組表に残るだけになる。これを何とかしたい。

これを何とかするのは、近年よく広告に使われる sticky positioning だ。sticky というのは日本語で言えばネバネバだ。ページをスクロールした時にできるだけビュー内にとどまろうと粘っこい動作をするアレである。現実には広告によく使われるが、本来 sticky な動作をして嬉しいのは上下に長い表のヘッダ部分などである。あべアニで言えばそれは正にチャンネル名の部分で、既にそういう動作をするように仕掛けてある。同じことを、それぞれの番組枠にも適用すればいい。

ところでこの sticky positioning というのは CSS3 の規格でまだ策定中の段階であり、現時点では Firefox を除くブラウザにはまだ実装されていない。なので javascript による Polyfill を使うことになる。そういうわけであべアニでは StickyFill というライブラリを使っている。ところが残念なことに、このライブラリに対してそれぞれの番組枠を sticky にするように指示しても上手くいかないのであった。上手くいかない上にスクロールもかなり重くなる。

Polyfill でやるべきことは

  • window の scroll イベントをリスンして
  • sticky 動作の対象となっている要素を走査し、その領域内にビューの上端が含まれていたら要素を position:fixed にする。このとき、その親要素の領域内に収まることを優先しつつ、要素の内容がビューに収まるように位置を微調整する。それから、領域の周辺の要素の位置を動かさないためにダミーの置換要素を生成したりも必要ならする

ということにつきる。しかしやることは単純だが、重い。まず scroll イベントは相当な頻度で発生する。それから要素を走査する際にその配置情報を逐一取得するのも重い。StickyFill は汎用的に使えるようにするためにその辺を素直にやっているので重い。

そういうわけで仕方ないので番組枠の sticky 動作についてはページに最適化しつつ自前で書いた。たとえばいったん計算したら使いまわせるもの(margin や border のサイズ)は積極的にキャッシュしたり、そもそも sticky 動作させる必要のない番組枠(内容の高さが枠の高さ以上あるもの)は積極的に除外したりだ。それでもブラウザがスムーズスクロールする設定にされていると若干描画がバタつく。早くブラウザでネイティブサポートされて欲しい。

ついでに言うとネイティブサポートの際は stuck 状態かどうかを取れる CSS の擬似クラスを定義してくれるととても助かるのだが。そうすると stuck したときに限り box-shadow をかけるなどの小細工ができる。

Abeanibot introduced #2

前の記事で、

検索結果をローカルに保持してある sqlite のデータベースと照合する。新しいものがあればそれをツイートする

と書いた。ここのやり方は sqlite ならではのものになっているので一応メモしておく。

普通に考えると、番組 A が既にデータベースにあるかを照合し、その結果によって処理を振り分けるには

  • [cci]select * from program where name=:A[/cci] する
  • レコードセットが行を返さなかったら [cci]insert into program (name) values (:A)[/cci] する。そしてツイートする

と、1 番組につき 2 回クエリを発行しなければならない。これは無駄な感じがするので、例の sqlite 側の関数の実体を php で実装できる機能を使う。


$db = new SQLite3("program.db");
$db->createFunction("php_name", __NAMESPACE__ . "\\phpName");

$insertedNames = [];

function phpName ($name) {
global $insertedNames;
$insertedNames[$name] = 1;
return $name;
}

こんな感じにしといて、[cci]insert or ignore into program (name) values (php_name(:A))[/cci] とすると、番組 A がテーブルに存在していなかった時のみ php_name 関数が呼ばれ、そして行が追加される。php_name 関数自体は何も副作用をもたらさないのだがこの時 PHP 側に制御がいったん移るので、そこで適当なものに覚えておく。こうすると 1 番組につき 1 クエリで済むようになる。

ところで createFunction の第 2 引数には PHP 側の関数を指定する。PHP で関数オブジェクトを指し示す方法としては、関数名の文字列、[cci]array([$this object], [method name string])[/cci] のような配列、クラス名を前置した静的メソッド名文字列などが段階的に追加されてきた。加えて最近の PHP では function リテラルそのものも使えるようになっている(これは Closure クラスのインスタンスが実体だ)。

この段階的というのがミソで、sqlite3::createFunction は最も初歩的な関数名文字列しか対応していないっぽいのである(かろうじて、名前空間は認識するが)。このへんの「ううn…」さがまあ PHP らしさなのだが。

Abeanibot introduced

あべ☆アニ関連でちょっとした Twitter 上のボットを作った:

あべアニ ぼっと
https://twitter.com/Abeanibot

これは AbemaTV 上で新しく配信されるらしいアニメをつぶやく。例えばこんなふうに:

新しくというのは、そのままの意味だ。例えば、すでにのんのんびよりは何度も配信されているので、もしまた再度第 1 話から配信されることになったとしてもそれはツイートされない。一方もし将来まじぽかの配信が決定した場合はそれはツイートされる。

どのように対象の番組を抽出しているか。情報源としては番組表データと、検索結果のデータが考えられるが、今回あべアニぼっとでは検索結果を使用している。というのは番組表データは当日から 1 週間先のものまでしか得られないが、検索であれば更にそれを飛び越えた期間の番組データを得ることができるからだ。

そういうわけでバックエンドでは 1 時間ごとに PHP スクリプトを起動し、AbemaTV の検索 API に対して 2 つのクエリを投げる。なぜ 2 つかというとこの API には若干の癖があるようで、どうも検索結果が 100 件を超えないように調整されるような感じがするのである。AbemaTV のアニメ系チャンネルの場合、タイトルに含まれる話数は必ず [cci]#\d+(〜\d+)?[/cci] の形式になっているようなので、第 1 話から新しく配信されていそうな番組を検索するには [cci]#1[/cci] で検索する。しかしこれには #10 とかも含まれて、直近の番組で 100 件近くに達してしまう。なのでこのクエリだと 1 週間先、2 週間先の新番組を抽出できない可能性がある。

そこで次に、[cci]#1〜[/cci] でも検索する。これなら確実に第 1 話の番組だけを抽出できる。ただしこれの場合複数話連続している配信形態であることが前提になるので、第 1 話を単体で配信するパターンに対応できない。それをカバーするのは最初に投げたクエリである。

この 2 つの検索結果をマージすることで最終的な結果が得られる。ここまで読めば分かるように、この方法だと 1 週間先、2 週間先で、かつ第 1 話を単体で配信するパターンの番組は検索から漏れてしまう可能性があるのだが、まあ仕方がない。クエリに単語区切りを指定するような特別な演算子を含めることができれば解決するのだが、クエリがどのような文法になっているのか分からない。

いずれにしても得た検索結果をローカルに保持してある sqlite のデータベースと照合する。新しいものがあればそれをツイートするという流れだ。

ところで Twitter ボットを作ったのは自由帳ボットに続いて 2 つめなのだが、どちらも純粋にツイートするだけの機能しかない。巷の高機能なボットはフォロー返しや、メンションに対するある程度の反応を行うものもある。これらのボットにもそういう機能が必要だろうか? 要調査。

The TV program time table #2

CloudFlare 絡みでもうひとつ。オリジンサーバから送出される静的なコンテンツについて、CloudFlare は CDN として振る舞う。つまり CloudFlare 側でキャッシュし、クライアントのアクセスの際はそのキャッシュを利用すると同時にオリジンサーバの負荷を低減する。

この静的の定義とはなんぞやというと、このへんに詳しい。特定の拡張子を持つもの、Cache-Control ヘッダで public と指定されかつ max-age が 0 より大きいもの、PageRule で特別に指定されたもの…という感じ。意外なことに html ファイル自体はキャッシュされない(PageRule の設定でキャッシュさせるようにすることはできる)。

で。あべアニのうち最もファイルサイズがでかいのは、番組表のデータである json ファイルなのであるが(gzip して 200KB 弱くらい)、アクセス解析を見てみたらなんとこれが CloudFlare 側にキャッシュされていないのであった。これはいけません。

とりあえず適切に Cache-Control ヘッダを付加するようにしてみたところ、効果なし。そこで mod_rewrite で擬似的な js ファイルとして扱うようにして、かつクエリ文字列を使わないようにしてみたところ、キャッシュに乗るようになった。間抜けなことにそれらを同時に変更したので、乗るようになった要因が拡張子なのかクエリ文字列なのかはっきりしていないのだが、まあ結果オーライなのだ。

ちなみに PageRule で何とかする方法もあるのだが、フリープランだと何と登録できる PageRule は 3 つまでなのである。ダイヤモンドより貴重なのでホイホイ使うわけにはいかない。

CloudFlare のキャッシュに乗っているかどうかは、レスポンスヘッダに [cci]cf-cache-status: HIT[/cci] が含まれているかどうかで判断できる。また、CloudFlare に限らずコンテンツが CDN によって適切に分散されているかどうかは https://www.webpagetest.org/ でテストすることができる。

The TV program time table

Abema.tv というサービスがある。これはつまりインターネット上の無料 TV サービスである。様々なチャンネルを持っているのだが、その中に 5 つくらいアニメ専門チャンネルがあり、そのせいで(あるいはそのおかげで)虹裏 img はここのところ半分 Abema 実況板と化している状態だ。

というわけで見てみたところ、Abema のサービス自体は非常によく出来ているのだが、ただひとつ番組表が使いにくいと感じた。全体的に重いのと、1 ページに 5 チャンネル分の番組表が表示されるのだがこれが単純にチャンネル番号順で、カテゴリ分けされるわけではない。そのため例えばアニメだけで一覧、ということができない。また、ページを非アクティブ状態からアクティブにした瞬間、どうも現在時刻に合わせた番組枠を表示しようとスクロール位置を変えるっぽいのだが、それがバグってるっぽい上になかなか直らない。……などなど、いろいろと不満があるのである。

そういうわけで、代替物を作った:

あべ☆アニ
https://appsweets.net/abeani/

これを作る過程での技術的なポイントはなくもないのだが、本質的にはただの html と javascript だけのシンプルなページであり、特に書くほどのものではない。

しかしこの番組表自体には直接関係ない分野から強いて 1 つ書いておくとすると、CloudFlare のキャッシュとの連携が挙げられる。知ってのとおり CloudFlare は静的ファイルをキャッシュする。従って web ページを開発して頻繁にサーバにアップロードして動作確認をする際はそのキャッシュ機構が邪魔になる。そんなわけで CloudFlare のサイトにログインすると現れるダッシュボードには個別のファイルのキャッシュをパージする機能が用意されているのだが、もちろん手作業になるのでめんどくさい。

と、だれでもそう思うわけで、API が用意されている。で結局、rsync した結果を元に API を呼び出すスクリプトを書いて解決。