Submission

目次

  1. Max 8 でニコニコの動画を再生してみる (基礎編)
  2. Max 8 + Node.js でニコ生コメントを取得する (練習編)
  3. Max 8 + Node.js でニコ動の動画を再生する (実践編) comming soon

概要

Max 8上でニコニコ動画のコンテンツを利用する方法を提供します。具体的には、シリーズ全体で次のノウハウを提供することを目的とします。

本章では、MaxでNode.jsオブジェクトを動かしてニコニコAPIを利用するテストケースを通じて、Node.jsとhttpリクエストについて理解を深めていきます。ただし、このページを参考にしただけなので、リンク先の技術内容を理解できる人は、そちらを読んだ方が早いです。

本記事では、ニコニコ動画のAPI利用方法について述べますが、API利用を正当化するものではありません。ニコニコAPIはわりと非公開なので、利用は自己責任です。サーバーに負荷をかけすぎると、アカウントがBANされるかもしれません。私は開発中、とある動画を一切視聴できない状態になってしまいました。動画ごとの出禁システムがあることにたまげました。場合によってはもっと怖いことが起きるかもしれないので、なるべくテストしてから動かしてみることをお勧めします。

導入

本章の内容

一連の記事はニコニコのコンテンツをMax 8(以下、単にMax)上で利用したい人への情報提供が目的です。今回は、ニコ生のコメントをMaxの制御に使えるように取り出します。

本章で取り扱う範囲は、以下の通りです。

本章からは、いよいよNode.jsが登場します。ただし、私自身が書いたコードはほとんどなく、参考文献からの、スクラップブックコーディングにすぎません。でも似たようなことをしたい人の時短になるように共有させてください。

Node.jsを使う前の下準備

外部エディターの登録

今回はjavascriptコードを書いていきます。Maxに付属しているエディタは、非ASCII文字の取り扱い、正規表現(/.../)やコメント(/*...*/)の範囲の認識にガバがあるので使いづらいです。ぜひ外部テキストエディタを使用することをお勧めします。私は、Visual Studio Codeを使用しています。でも何が一番いいエディタかは知りません。Max ツールバーから、Options > Preference > Interface > External Text Editor で使い慣れたエディタを登録してください。

Node.jsとnpmのインストール

Node.jsとnpmはどちらもMaxに入っていますので、あえてインストールする必要はありません。でも、Maxの外でもNode.jsやnpmを使いたくなるかもしれません。そういう人のためにインストール手順を解説しているサイトをご紹介します。(参考サイト:Win Mac)無事インストールが完了したら、コンソールから使えるようになります。

Node.js (Node for Max/n4m)の利用

node.scriptオブジェクトの作成

新規パッチャーを作り、そのまま保存してください。そして、保存したパッチャーのあるフォルダーにnicotest.jsという名前のテキストファイルを作成してください。
次に、Maxでnode.script nicotest.jsというオブジェクトを作成し、パッチャーをロックしてから、ダブルクリックして下さい。先ほどの手順で外部エディタを登録した人は、外部エディタが起動すると思います。

メッセージをやり取りする仕組み (max-api)

Hello, World!

起動したエディタで、Hello, World!プログラムを書いていきましょう。せっかくなので、コンソールのほかに、パッチャー上のメッセージボックスにもHello World!というメッセージを送りたいですね。そんなプログラムは次のようなものです。(例1)

const maxApi = require('max-api');

function hello() {
maxApi.post('Hello, World!'); maxApi.outlet('Hello, World!'); }; maxApi.addHandler('bang',hello);

最初の行は、max-apiという名前のモジュールをmaxApiという名前で使いますよ、という宣言文です。このmax-apiモジュールはNode.jsで処理した結果をMaxに渡したり、Maxのリソースにアクセスするのに使います。要するにほぼ必須です。

次のfunctionから始まる文はhelloという名前の関数を宣言しております。()は何も引数をとらないことを示しています。関数の実行内容は{と}の間に書きます。上の例の実行内容は、maxApi.post('Hello, World!'); およびmaxApi.outlet('Hello, World!'); ということになります。
maxApi.post('Hello, World!'); はmax-apiモジュールのメソッドpostを呼び出します。postは引数をコンソールに出力するメソッドです。

maxApi.outlet('Hello, World!');は、node.scriptの第1アウトレットから、"Hello, World!"というメッセージを出力します。

最後の行で呼び出しているメソッドaddHandlerは、node.scriptに対して発生するイベント(メッセージの入力)に応じて、"関数を実行する機能"(イベントハンドラ)を作るメソッドです。どのイベントに反応させるか、はmaxApi.addHandlerの第一引数で指定します。上の例では、Maxでおなじみのbangに反応するようにしてます。実行したい関数は、addHandlerの第二引数に渡します。上の例では、helloを渡しているので、bangイベントに反応して、hello()を実行する("bangイベントで発火する"と呼びます)ようになります。

javascriptでは、hello()関数の宣言の代わりに、無名関数(function ())を作って変数helloに入れるように書きかえられます。(例2)

const maxApi = require('max-api');

const hello = function() {
    maxApi.post('Hello, World!');
    maxApi.outlet('Hello, World!');
}

maxApi.addHandler('bang',hello);

また、変数に入れるのを省略すると、(例3)

const maxApi = require('max-api');

maxApi.addHandler('bang',function() {
        maxApi.post('Hello, World!');
        maxApi.outlet('Hello, World!');
    }
);

のようにも記述可能です。javascriptに慣れていない人は面食らうかもしれませんが、この記述方法は、インターネットに公開されているたくさんのサンプルコードで頻出する形です。また、function(){...}の代わりに()=>{...}と書くこともできます。

※厳密に言えば、上記3つの例にも微妙に違いがあります。それは、hello()を呼べる場所です。最初の例では、宣言文の前であっても呼び出せます(巻き上げといいます)。2つ目の例では、変数helloが宣言される前に呼び出すことはできません。3つ目の例では外から呼び出せません。いろんなところで呼べる方が便利といえば便利ですが、意図しないところで呼べてしまうことが弊害になりえます。

オブジェクト配置

配置図

node.scriptに対して、script start, script stop, をインレットにつなぎます。第1アウトレットにメッセージボックス、第2アウトレットに node.debugをつなぎます。

動かしてみよう

準備は整いましたが、この状態でをクリックしても何も起こりません。bangを受け付けるためには、先にNode.jsを動かす必要があります。Node.jsの起動には、script startのメッセージを送信する必要があります。script start送信後、デバッグ画面の背景が緑色になれば正常に動作したことになります。しかし、まだ何も出力されません。javascriptは、node.scriptbangを入力されたら関数を実行するように、addHandlerというメソッドを呼び出したあと、処理が終わりました。プログラム処理は終わったNode.jsは何もすることがないのでスリープ状態にありますが、イベント発生を待受けています。この状態でをクリックすると、出力に"Hello, World!"が出てきます。Node.jsを止めるには、script stopのメッセージを入力します。script stopメッセージを受け取ったNode.jsはそれ以降のメッセージの入力を受け付けませんし、javascriptの実行も止まります。ある特定のイベントハンドラだけを止めたい場合は、removeHanlder()およびremoveHandlers()というメソッドで削除することができます。

上記のNode.jsオブジェクトは、大仰なメッセージボックスでしかなく、何の面白みもありません。そこで、Max側からの入力に応じて処理を行い、結果を出力するようにしていきます。

イベントハンドラに引数を渡す/好きなイベントを発火させて分岐を作る

それでは、Maxから二つの数字を受け取って合計値を出力するようにしてみましょう。

const maxApi = require('max-api');

maxApi.addHandler('list',function(x, y) {
        maxApi.outlet(x + y);
    }
);

'list'はメッセージ文ではなく、Maxのリストメッセージを入力された時の動作を指定します。メッセージが4 5であれば、9が出力されます。なお、2つ目の要素が文字列でも動いてしまいますので、入力されるリストは必ず2つの数字になるように設計する必要があるかもしれません。一方、先頭の要素が文字列である場合、そのメッセージをイベント名と解釈するため、対応するイベントハンドラが呼ばれます。('list'のイベントハンドラは呼ばれません)つまり、好きに決めた名前のイベントを発火させることで、望みの処理をするようにできます。なお、その場合、引数としてイベントハンドラに渡されるリストは先頭の要素(つまりイベント名)が削られます。

const maxApi = require('max-api');

maxApi.addHandler('add',function(x, y) {
        maxApi.outlet(x + y);
    }
);

maxApi.addHandler('multiply', (x, y) => {
        maxApi.outlet(x * y);
    }
);

上記のようにすると、add 4 5メッセージの入力によりaddイベントが発火して、9が出力されます。multiply 4 5とした場合はmultiplyイベントが発火し、20が出力されます。この場合も、addmultiplyに続いて、2つの数字を持つように設計する必要があるかもしれません。"スプレッド構文"を使うことで、要素の数がいくつであっても対応できるイベントハンドラを作れます。必要になった時に調べてみてください。また、例示のためfunction(x,y){}の書き方と(x,y)=>{}の書き方を混在させていますが、使い分ける意味は何もないのでマネしないほうがいいと思います。

うまくいきましたか?私の解説は拙いので、詰まってしまったらjavascriptの解説サイトを参考にしてください。

ニコニコと関係のない練習が長くなってしまいました。次からはいよいよニコニコAPIの利用を試すことになります。まずは、簡単なAPIから利用して練習します。ニコ生コメント取得に至るまではまだまだ先が長いですが、頑張りましょう。

ニコニコAPIでユーザー名取得: http GET (axios)

ユーザー名取得用API

ログインせずに使えるニコニコAPIのうち、ユーザー名取得用APIを試します。APIリストによると、ユーザーの名前を取得するには、http://seiga.nicovideo.jp/api/user/info?id={user_id}というURLにアクセスすればよいようです。まずは、ブラウザを使って、{user_id}を自分のユーザーIDなどで置き換えたURLにアクセスしてみてください。すると、XMLの木構造が表示されます。上に書いてあるメッセージはブラウザ側で追加されているものです。<nickname>あなたのユーザー名</nickname>のような表示が確認できます。

それでは、ユーザーIDをMaxから入力したら、node.scriptがAPIをたたいて、Maxにユーザー名が出てくる、という仕組みを作っていきましょう。

axiosのインストール

Node.jsにnpmとaxiosをインストールします

Node.jsでAPIにアクセスするためには、httpクライアントが必要です。今回、httpクライアントとしてaxiosモジュールを使用します。axiosは管理ツールnpmでインストールしますので、まず、npmをインストールする必要があります。script npm installメッセージをnode.scriptに送って、npmをインストールしてください。デバッグ画面で、インストール完了報告が出たら、同様にscript npm install axiosメッセージでaxiosをインストールしてください。

npmとaxiosのインストールがすんだら、コードを書いていきましょう。getnameで発火するイベントハンドラを作ります。同時に、数値を入力してユーザーIDとしてイベントハンドラに渡します。

const maxApi = require('max-api');
const axios = require('axios');
const usernameURL = 'https://seiga.nicovideo.jp/api/user/info?id=';

maxApi.addHandler('getname', async (id) => {
        const response = await axios(usernameURL + id).catch(() => { return 'ERRORNAME'; } )
        maxApi.outlet(response.data);
    }
);

axiosモジュールも、使うために宣言が必要です。通例としてモジュールは同名変数に入れますが、別の名前でも問題はありません。axios()というメソッドは、文字列だけを渡すとそれをURLと解釈してサーバーにGETリクエストを送ります。返り値は、サーバーからのレスポンスです。axiosのreadmeによれば、レスポンスの構成は以下のようなオブジェクトです。

{
    data: {},
    status: 200,
    statusText: 'OK',
    headers: {},
    config: {},
    request: {},
}

本文であるXMLデータは上記オブジェクトのメンバのうち、dataに入りますので、response.datamaxApi.outlet()で出力しましょう。

これまでのメソッドは、まず失敗しないものばかりでしたが、axios()は失敗する可能性があります。URLが古いとか、ネットワーク回線の調子が悪いとか、いろんなことが原因になります。例外処理が必要なので、axios()の後ろにcatch()というものをつけます。catchの中には、axios()が失敗したときだけ、代わりに実行する処理を書きます。

イベントハンドラの前にasyncというキーワード、axios()の前に awaitというキーワードが登場してきました。asyncに指定した関数内で、await指定したメソッドや関数は、その処理が完了するまで、次の処理に進まなくなります。逆に言えば、axios()はそのままだと完了を待たずに次の処理に進んでしまいます(非同期処理といいます)。どうしてそうなっているかは、本記事では解説できませんが、ともかく、awaitがない場合、const responseaxios()の返り値を受ける前にmaxApi.outlet()が呼ばれてしまいます。つまり、何も出力されずに処理が先に進みます。axios()によるリクエストに対するレスポンスは”いつかは”帰ってくるのですが、そのころには、もうすでにmaxApi.outlet()の処理が終わっているので、後の祭りです。asyncawaitを書くことで、const responseに返り値が入ってから、maxApi.outlet() が呼ばれることを保証することができます。同様のことをするための方法は他にもありますが、は”他の方法より簡単に書くこと”を目的にasyncawaitが近年追加された経緯があるように、断然書きやすいし、読みやすいです。axiosのほかにもたくさんの非同期処理があるので、これが書けるようになると楽しいです。

では、動かしてみましょう。なお、ユーザーID:2の人は、戀塚さんです。

名前は出てきたけど……なんか変?

response.data全体を出力しているため余計なものまでメッセージボックスに出力されています。文字が欠けているように見えますが、メッセージボックスの内容を編集しようとすると正常な表示になりますので、ちゃんとデータは出力できたようです。名前だけを抜き出すため、response.data(文字列)から特定の文字列を検索します。

const maxApi = require('max-api');
const axios = require('axios');
const usernameURL = 'https://seiga.nicovideo.jp/api/user/info?id=';

maxApi.addHandler('getname', async (id) => {
        const response = await axios(usernameURL + id);
        const matched = response.data.match(/<nickname>(.+)<\/nickname>/);
        maxApi.outlet(matched[1]);
    }
);

文字列にmatch()メソッドを実行すると、正規表現(/<nickname>(.+)<\/nickname>/)にヒットした文字列が格納された配列が返ります。このとき、正規表現に()で囲まれた部分が1か所以上ある場合、その部分を抜粋した文字列が、順番に配列に追加されていきます。正規表現において"."はすべての文字、"+"は1回以上の繰返しです。"\/"の二つの文字は"/"という文字そのものを意味します。("/"は正規表現の符号なので、そのまま書けません)

上のコードを実行すると、matched[0]に"<nickname>hoge</nickname>"が、matched[1]に"hoge"が入ります。matched[1]だけ出力すれば、ユーザー名だけをMaxの処理に渡すことができます。

次は、サーバーに情報を渡してレスポンスを受け取るために、POSTプロトコルで通信してみます。段々面白くなってきましたね!

ニコニコAPIでログイン: http POST (axios)

ログイン用アカウント情報の保存

Node.jsでニコニコにログインします。ログインにはIDとパスワードが要りますが、IDとパスワードをjavascriptコード本体に書いてしまうのは、かなりよろしくないので、Maxのdictオブジェクトに一時保存することにしましょう(これも、セキュリティ的によろしくないですけど)。dictの名前は、accountinfoとし、mail_tel, password, user_sessionというキーと値のペアを持つようにします。mail_telはログインID、passwordはログインパスワードです。user_sessionは後で使いますので、空の文字列を入れておいてください。dictオブジェクトの中身は以下のようになります。末尾のカンマを忘れないようにしてください。ただし、最後のメンバにはカンマをつけないでください。ちょうどこんな感じです:

{
    "mail_tel": "example@example.com",
    "password": "_!_pass1234$",
    "user_session": ""
}

次は、ログインするためのコードを書いていきます。ログイン用のAPIのURLは、https://account.nicovideo.jp/api/v1/loginです。このURLに、ログインに必要なデータを送信します。先ほどの例同様、axiosを使います。

const maxApi = require('max-api');
const axios = require('axios');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';

maxApi.addHandler('login', async () => {
        const accountinfo = await maxApi.getDict('accountinfo');

        const response = await axios({
            url: loginURL,
            method: 'post',
            data: {
                mail_tel: accountinfo.mail_tel,
                password: accountinfo.password
            },
        });
maxApi.outlet(response.headers);
}
);

maxApi.getDict()は、パッチャー上にあるdictオブジェクトをとってきて、中身をjavascriptオブジェクトとして返すメソッドです。先ほど、IDとパスワードを、それぞれmail_telとpasswordとしてdictに書いておきましたので、上の例では、それぞれaccountinfo.mail_tel, accountinfo.passwordで得ることができます。

POSTプロトコルでは、axios()にはURLのほか、プロトコル指定用文字列('post')と、送信データであるオブジェクトを一緒に渡す必要があります。これらはそれぞれ、url, method, dataというキーの値としてもつオブジェクトで渡します。

うまくいくと、サーバーからのレスポンスが、const responseに格納されます。ログイン情報は、response.headersに入っているので、最後に、response.headersをnode.scriptから出力します。なお、axios()によって得られるレスポンスのヘッダはjavascriptオブジェクトなので、そのままMaxのdictオブジェクトに入力できます。

ログイン情報の格納



出力先にdictオブジェクトを置きます。

ちょうどこんな風にすると、ヘッダーをheadersという名前のdictに保存することができます。maxApi.getDict()はパッチャーにあるdict全てにアクセスできるので、配線する必要はありません。先ほどのコードの最後をmaxApi.outlet()ではなくmaxApi.setDict()を用いて、maxApi.setDict("headers",response.headers);と書き換えても同じです。maxApi.setDict()は既存のdictオブジェクトの中身を置き換えるメソッドで、第一引数はdictの名前、第2引数はjavascriptオブジェクトです。maxApi.setDict()を用いる場合も、node.scriptとdictを配線する必要はありません。

ログインしてみる

script startで起動してから、login一度だけ押してみてください。そのあと、ログイン情報のページにアクセスして、ログイン出来たかどうかを確認してください。"有効なアクセス"欄に"ブラウザ"によるログインがあれば成功しています。失敗した方は、addHandler()のイベント名とイベントメッセージの組合せ、ログインAPIのURL、dictの名前(accountinfo;)、メールアドレスとパスワードの組合せ、キー名(mail_tel, password)、axios()に渡すオブジェクトと、その中のdataオブジェクトの内容が全部正しいかをチェックしてください。

ニコニコのサーバーは、アクセスしてきたユーザーがログインしているかどうかを、クライアントから送信されるクッキーの中の、"user_session"の値を見て判定しています。この値は、ログイン時のレスポンスヘッダーの、set-cookieに書かれていて、ブラウザはこれを記録することで、ログイン状態を保っています。

上記コードでログインが成功した後、dict headersをのぞいてみると、サーバーからのレスポンスのヘッダーが格納されています。しかしながら、set-cookieに"user_session"の値は見つかりません。ログインは成功しているのに、どういうことなの……。

より細かいオプションをつけてログインする

ニコニコのログインAPIは、ログイン成功時のステータスコード(404 Not Foundとか500 Internal Server Errorとかの数字です)として、302を返します。これはリダイレクトを求めるレスポンスです。axiosはデフォルトでは、5回までのリダイレクトに従いリダイレクトが終わった後のページのレスポンスを返します。つまり、ログイン直後の302を返してきたレスポンスのヘッダーは捨てられています。またデフォルトでは、最終的にレスポンスのステータスが200番台でないと通信が失敗したとしてエラーを吐きます。これを回避するには、リダイレクトを禁止し、ステータスコード302でも通信成功とする必要があります。axios()にこんな注文を付けましょう。

const maxApi = require('max-api');
const axios = require('axios');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';

maxApi.addHandler('login', async () => {
const accountinfo = await maxApi.getDict('accountinfo');

const response = await axios({
url: loginURL,
method: 'post',
maxRedirects: 0,
validateStatus: (status) =>{ return status == 302;},
data: {
mail_tel: accountinfo.mail_tel,
password: accountinfo.password
},
});
maxApi.outlet(response.headers);
}
);

axiosに渡すオプションオブジェクトに、maxRedirectsvalidateStatusが増えています。maxRedirectsはその名の通り、リダイレクトに従う回数です。validateStatusはサーバーからレスポンスを受け取るたびに評価される関数で、サーバーから受け取ったステータスコードを関数に渡して、trueが帰ってきたら通信成功と判定します。ここでは、ステータスコード302ならtrue、それ以外はfalseとするため、return status == 302としました。しかし、通信成功と判定されても、リダイレクトには従ってしまうので、結局 maxRedirects: 0として、リダイレクトを禁止する必要があります。

user_sessionの保存

コードを書き換えたのち、もう一度ログインしてみてください。今度はset-cookieに"user_session=deleted; ..." や "user_session=user_session...." という値が確認できると思います。このうち、後者が有効なuser_sessionですので、これを保存します。

文字列の抜粋

const maxApi = require('max-api');
const axios = require('axios');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';

maxApi.addHandler('login', async () => {
const accountinfo = await maxApi.getDict('accountinfo');

const response = await axios({
url: loginURL,
method: 'post',
maxRedirects: 0,
validateStatus: (status) =>{ return status == 302;},
data: {
mail_tel: accountinfo.mail_tel,
password: accountinfo.password
},
});
const cookies = response.headers['set-cookie'].join();
const user_session = cookies.match(/user_session=user_session.*?;/)[0];
maxApi.updateDict('accountinfo', 'user_session', user_session);
}
);

response.headers['set-cookie']は文字列をを要素としてもつ配列であり、このままでは文字列を検索しにくいため、join()で全要素をひとつながりの文字列にしてから、正規表現でマッチングしています。続いて、accountinfoの"user_session"の値をマッチした文字列(user_session)で上書きするため、maxApi.updateDict()を呼んでいます。

ニコニコ生放送のサーバーに接続 (net)

netモジュールのインストール

axiosをインストールしたときと同じ要領で、netと、cheerioというモジュールをインストールします。script npm install net メッセージおよびscript npm install cheerio メッセージを node.script に入力して下さい。

コード

ニコ生コメントをNode.jsから取得する方法は、このページがお手本となります。また、新配信システムに対応する方法は、こちらのページでとても詳しく解説されています。後者のページの方法は筆者がまだ試していないため、今回は、旧配信サーバーにアクセスしてコメントを取得することにします。async と await を使って参考サイトと同じ動作をするように目指したコードは以下の通りです。node.scriptオブジェクトに対し、'login'メッセージでログインして、'getlv lv****'で生放送ID:lv****のコメントを受信できるようになります。※例外処理はエラーを特定するために便利ですので、適宜追加してください。

const maxApi = require('max-api');
const axios = require('axios');
const net = require('net');
const cheerio = require('cheerio');
const loginURL = 'https://account.nicovideo.jp/api/v1/login';
const getplayerstatusURL = 'http://live.nicovideo.jp/api/getplayerstatus/';
const viewer = new net.Socket();

maxApi.addHandler('login', async () => {
    const user_session = await nicolive.getsession();
    if(user_session){return;}
    await nicolive.login();
});
maxApi.addHandler('getlv', async (lvid) => {
    const user_session = await nicolive.getsession();
    if(!user_session){return;}
    const thread = await nicolive.fetchThread(lvid, user_session);
    nicolive.view(thread);
});

const nicolive = {
    login: async() => {
        const accountinfo = await maxApi.getDict('accountinfo');
        const response = await axios({
            url: loginURL,
            method: 'post',
            maxRedirects: 0,
            validateStatus: (status) => { return status == 302; },
            data: {
                mail_tel: accountinfo.mail_tel,
                password: accountinfo.password
            },
        });
        const cookies = response.headers['set-cookie'].join();
        const user_session = cookies.match(/user_session=user_session.*?;/)[0];
        maxApi.updateDict('accountinfo', 'user_session', user_session);
    },
    fetchThread: async(lvid, user_session) => {
        const response = await axios({
            url: getplayerstatusURL + lvid,
            headers: {Cookie: user_session}
        });
        const $ = cheerio.load(response.data);
        const port = $('getplayerstatus ms port').first().text();
        const addr = $('getplayerstatus ms addr').first().text();
        const thread = $('getplayerstatus ms thread').first().text();
        return {port: port, addr: addr, thread: thread};
    },
    view: async(thread) => {
        viewer.removeAllListeners();
        viewer.destroy();
        process.nextTick(() => {
            viewer.connect(thread.port, thread.addr, () => {
                viewer.setEncoding('utf-8');
                viewer.write('<thread thread="'+thread.thread+'" res_from="-5" version="20061206" />\0');
            });
            viewer.on('data', (data) => {
                const $ = cheerio.load(data);
                $('chat').each();

                maxApi.outlet(texts);
            });
        });
    },
    getsession: async() => {
        const dict =  await maxApi.getDict('accountinfo');
        return dict.user_session;
    },
};

login

ログイン処理は、nicoliveのメソッドにそっくり移動しました。ただし、user_sessiondictに存在する場合は、ログインをスキップします。

getlv

まず、user_sessiondictから読めない場合は処理を中断します。つまり、何も起こりません。そうでない場合、fetchThread()というメソッドを呼び出して、完了後、次いでview()というメソッドを呼び出して、終わりです。

fetchThread

生放送IDとuser_sessionを引数にとります。参考サイトで解説されているように、getplayerstatus APIにuser_sessionを渡して、ポート、アドレス、スレッドなどの情報をもらう必要があります。レスポンスから情報を切り出しているのですが、何をしているかは次節で説明します。

view

プログラム冒頭部のconst viewer = new net.Socket();では2つのマシン間でソケット通信をするためのnet.Socketというクラスのインスタンスを作って(new)います。net.Socketに発生するいろいろなイベントに対して、on()でそのときのイベントハンドラ(イベントリスナー)を設定できます。'connect':接続したときに発生するイベント。エンコードをutf-8に設定し、サーバーに指定した文字列を送信(write)します。内容は、スレッドを開いて、最新コメント5つを送るようにサーバーに催促するものです。'data':データを受信すると発生するイベント。ハンドラー渡す引数dataは、受信したデータそのものであり、これをMaxの制御に使えるように、次節で説明するcheerioというモジュールによってコメント本文だけを抽出し、maxApi.outlet()で出力します。

cheerioによる文字列パース

コメント

サーバはコメントをXMLデータとして送信します。HTMLやXMLによるレスポンスは、その構造を解釈することにより、コメントの種類、ユーザー情報、時間情報などのたくさんの情報を読み取ることができます。具体的には、下のような構造のデータが送られてきます。各パラメータの意味合いは、新コメントシステムと同じだと思います。

<chat thread="0000000000" no="810" vpos="143000" date="1597328767" date_usec="114514" mail="184" user_id="Th1siSJu5tAnExaMp1eId" anonymity="1">オッツオッツ</chat>

chat要素しかないので、無茶苦茶単純ですね。<chat>~</chat>の中身を指定していきます。

cheerioモジュールは、操作する対象となる要素をかなり細かく指定できるので大変便利です。今回のプログラムでは、正規表現を使った文字列抽出だけでも出来ますが、cheerioを使う方が楽だと思います。cheerio.load()は文書の構造を解釈します。$はデータをロードしたcheerioオブジェクトが入ります。$にメソッドを実行することで、抽出する条件を設定します。$('xxx')とすると、xxxという名前の要素を(全部)指定し、それに対してtext()を実行すると文字列化します(複数あっても、連結した文字列となります)。また、first()は見つかった最初の要素だけ指定します。また、each()は、要素一つ一つについて、引数として受け取った関数を評価します。上のコードでは、iは要素のインデックス番号(0から始まる)、elは要素そのもの(オブジェクト)であり、$(el)はその要素の指定となります。今回の関数では、要素を一個ずつをテキスト化して出力するために、each()を用いて、それぞれの要素をテキスト化しています。'connect'イベント発火時は一度に5つ送られ、その後の'data'イベント発火時は1つずつデータが送られてきますが、上記のようにすることで要素がいくつあっても対応できます。

おわりに

おさらい

参考文献

Node For Max(英語)
Maxの開発による公式文書
Max8が出たのでNode for Maxを試してみた
MaxでAPIをたたくための情報
ニコニコAPIリストwiki
ニコニコが提供してくれているAPIの情報
Nodejsでニコ生のコメビュを作る
ログイン、ニコ生のサーバーと通信する方法
ニコニコ動画の新仕様動画を再生する方法
動画URLの取得方法、ハートビートについて
ZenzaWatchのソースコード
たぶんほとんどの疑問についての答えが書いてあると思います
ニコニコ動画のコメントをJSONで取得したり投稿したり
新配信システムについて

補足

Maxで特別な意味を持つイベント名

なお、"all"のイベントハンドラは他のイベントハンドラが実行されるかどうかに関わらず、毎回実行される。そのとき、また、イベント名を含む入力リストの先頭に、0か1が追加されたリストがイベントハンドラに渡される。先頭が0の時は他のハンドラが実行されないことを示し、先頭が1の時は、しかるべきハンドラが実行されることを示す。

愚痴

コードを読みやすく書こうと思うとスタイル指定を根性で入力する必要があり、しんどいです。一般ブロマガ投稿者にもCSSは解禁して欲しいですね。