🔙 Back to Top

Fri May 6 16:24:13 JST 2016

ケータイがあること前提の社会と
クレジットカードがあること前提の社会だけは許さない

アニメの実況の場は

既に2ちゃんからは失われ、Twitter へ移ったと言って過言ではあるまい. 法的にはアウトであるが、テレビ映像のキャプチャ画像はTwitter で流れることが多く、 実際、キャプチャ画像を貼りながら実況するようなアカウントというのがいくつかあって (e.g. @youtonjoe, @Story_terror) そのようなアカウントに対する需要はあるものと見受けられる. 実際に実況してる時にはテレビ映像を見てるのだから、そんなキャプチャ画像は必要ないようにも思えるが、 ちょうど良いシーンのキャプチャに成功すれば、そのシーンについて言及するのに、その画像が役に立つ.

ところで

ちょうど先週、 ぱちおさん (@patioglass) が作者である pretty-watch.net というものを知り (だって本人が宣伝してたからね) 自分が欲しかったものが、良い感じのデザインでそこにあったので、大変に感心した.

  1. ソースがTwitterであること
  2. 画像があればそれを用いること
  3. 明らかにニコニコ動画をモチーフとしていること

特別に技術的にすごいところは無いが、2つめのアイデアは上手いと思った. 最近に得られた画像の1枚を、そこに表示し続けることで、 擬似的に動画を見てるような体験がある. (しかもそれはどこかのユーザーが勝手に投稿したものである.)

ただ画像が流れるだけの掲示板とも違った. 私は2ちゃんをソースにして含まれてる画像をただ流すだけのものを作って満足していたが、 固定観念に囚われていたことを恥じた.

確かに、画像を蒐集する目的でなく、実況を円滑にするための画像だと考えるならば、こちらのほうが良い.

しかしながら

8:45頃にもなるとtweetの流速に耐え切れずにFirefox が死ぬので実用に耐えられない. patioglass/precure_watch にソースがあるが、ちょっと私にはいじれない (複雑すぎませんかね). 作者に直接問い合わせると、調整中だそうで、それに熱心に開発してるようなので、あと数週間もすれば良い物になってると期待しています.

cympfh/precure-watch

これだけアイデアのオリジナルについて言及すれば、堂々とパクっても良いだろう.
同じことを、もっと軽い環境で、実現する. シェルスクリプト (bash) で. 完成品は cympfh/precure-watch に置いてある.

0. requirements

1. twurlstatuses/filter API を叩く

twurl -t -d track=#precure -A 'Accept-Encoding: text' \
    -H userstream.twitter.com \
    /1.1/statuses/filter.json 2>/dev/null

-t オプションでバッファに貯めずに出力させる. streaming 系統には必須. 1tweet が1行JSONとして出力される.

filter の仕様なのか、APIの叩き方が悪いのか、偶にただ空の行が来ることがある. 後の処理で気にする必要があるかもしれない.

2. JSON パース、整形

statuses/filter から築次的に受け取るレスポンスを jq でパースしていく.

レスポンスから欲しいのは次の3つ.

Bashでパイプで繋いで一行で書きたいので、次の書式のファイルをログとして延々吐き続けることにする:

T screen_name text
M media_url

その行の頭が T ならその行はツイート本文を表現してて、半角空白区切りでアレとアレが並んでる. 頭が M なら、それに続く文字列は画像のurlである.

例えば、

{}
{"text":"body","media":[{"media_url":"http://a.jpg"},{"media_url":"http://b.jpg"}]}
{"text":"body2"}

こんな標準入力を

T body
M http://a.jpg
M http://b.jpg
T body2

に変換したい.

N.B. status/filter のレスポンスはストリーム. 一行ずつリアリタイムにやってくるとして、一行ずつ表示するのが理想です. しかしながら実際に処理する場合にはある程度データが溜まってから処理が行われます. つまり勝手にバッファを取ります. grep の場合、 --line-buffered オプションを附けることで、一行データが来たらバッファを解放してくれます. jq はよくわからないです. jq の後ろにパイプで grep --line-buffered をつなげるとどっかにバッファが大きくトラれます.

3. filter.sh

#!/bin/bash

Q="#precure"
twurl -t -d track=$Q -A 'Accept-Encoding: text' -H userstream.twitter.com \
    /1.1/statuses/filter.json 2>/dev/null |
jq --unbuffered -r \
    '(if has("text") then "T " + .text else "" end) + "\n" + (if has ("media") then (.media | map(.media_url) | map("M " + .) | join("\n") ) else "" end)'

もちろん jq じゃなくて ruby -n などでもいいが、バッファさせたらいけない. つまり、statuses/filter からのレスポンスをがんがん画面に出力させたい. jq の場合は --unbuffered でそれが実現できる. ruby の場合どうするかは調べたが不明.

ファイル構成の構想

  1. filter.sh: filter API からリアルタイム検索して、ツイート本文と画像を保存 /tmp/tw
  2. server.sh: 鯖です
  3. index.html: クライアント

4. server.sh

Bash で鯖書くのは、一度要領を覚えてしまえば簡単. 調べるのが大変だったけれど、人は man に立ち帰るのだった.

There is no -c or -e option in this netcat, but you still can execute a command after connection being
established by redirecting file descriptors. Be cautious here because opening a port and let anyone con‐
nected execute arbitrary command on your site is DANGEROUS. If you really need to do this, here is an
example:

On ‘server’ side:

      $ rm -f /tmp/f; mkfifo /tmp/f
      $ cat /tmp/f | /bin/sh -i 2>&1 | nc -l 127.0.0.1 1234 > /tmp/f

On ‘client’ side:

      $ nc host.example.com 1234
      $ (shell prompt from host.example.com)

例えば /media とそれ以外とを識別するだけの鯖は次のように

#!/bin/bash

PORT=8080

rm -f /tmp/f; mkfifo /tmp/f
trap "rm -f /tmp/f; exit" INT

run() {
  head -1 > /tmp/req
  if ( grep "^GET /media" /tmp/req >/dev/null ); then
    echo media
  elif ( grep "^GET /tw" /tmp/req >/dev/null ); then
    echo tw
  else
    echo "HTTP/1.1 200 OK"; echo
    cat index.html
  fi
}

echo http://127.0.0.1:$PORT/
while :; do
  cat /tmp/f | run | nc -l 127.0.0.1 $PORT > /tmp/f
done

これで不取敢、鯖っぽいものの骨格ができた.

完成版はgithubに置いておくので.

run はパイプで初め存在しない /tmp/f を読んでるので、cat などですべてを読もうとするとブロックしてしまう. head -1 みたいな必要最小限だけ読む.

index.html

すべからくすべし

  <script>
var xml = new XMLHttpRequest();
setInterval(function () {

  xml.open("GET", "media", false);
  xml.send();
  I.src = xml.responseText;
  xml.open("GET", "tw", false);
  xml.send();
  U.innerHTML = xml.responseText.split("\n").map(line => `<li>${line}</li>`).join("");

}, 100);
  </script>

みたいな感じ.

run.sh

バックグラウンドで filter.shserver.sh を実行してやるだけのスクリプト. running on http://127.0.0.1:8000 なんて言ってやれば気が利いてると思われる.

鯖なので停められるまで勝手に動き続ける. C-c で終わるようにしたい. run.sh じゃなくてバックのもちゃんと止まらないといけない.

次を run.sh の最後に仕込んでおく. こうすることで自分に属する (つまりバックで実行した子供) プロセスにSIGINTを送れる.

trap "kill -TERM -$$" SIGINT
wait
bash filter.sh | tee /tmp/tw

したらバッファとるし

bash filter.sh >> /tmp/tw

でもそうだしもう全然ダメ!

と思ったら、そうだ (参考; unix - Force line-buffering of stdout when piping to tee - Stack Overflow)

stdbuf -o L filter.sh | tee /tmp/tw

だ!!!

#!/bin/sh

trap "kill -TERM -$$" SIGINT

: >/tmp/tw
bash server.sh &
stdbuf -o L bash filter.sh | tee /tmp/tw
# firefox http://127.0.0.1:8080/
wait

こんな感じです。

無駄に疲れたわ