node-nicovideo-api
Version:
nicovideo api (video, live, etc..) wrapper package for node.js
955 lines (779 loc) • 27.2 kB
JavaScript
// Generated by CoffeeScript 1.10.0
/**
* @class NsenChannel
*/
(function() {
var APIEndpoints, Cheerio, CompositeDisposable, Disposable, Emitter, NicoException, NicoLiveInfo, NsenChannel, NsenChannels, QueryString, Request, _, deepFreeze, ref, sprintf,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty,
slice = [].slice;
_ = require("lodash");
Emitter = require("disposable-emitter");
Cheerio = require("cheerio");
deepFreeze = require("deep-freeze");
Request = require("request-promise");
sprintf = require("sprintf").sprintf;
ref = require("event-kit"), CompositeDisposable = ref.CompositeDisposable, Disposable = ref.Disposable;
QueryString = require("querystring");
APIEndpoints = require("../APIEndpoints");
NicoException = require("../NicoException");
NicoLiveInfo = require("./NicoLiveInfo");
NsenChannels = require("./NsenChannels");
module.exports = NsenChannel = (function(superClass) {
extend(NsenChannel, superClass);
/**
* Nsenリクエスト時のエラーコード
* @const {Object.<String, String>
*/
NsenChannel.RequestError = deepFreeze({
NO_LOGIN: "not_login",
CLOSED: "nsen_close",
REQUIRED_TAG: "nsen_tag",
TOO_LONG: "nsen_long",
REQUESTED: "nsen_requested"
});
NsenChannel.Gage = deepFreeze({
BLUE: 0,
GREEN: 1,
YELLOW: 2,
ORANGE: 3,
RED: 4
});
NsenChannel.Channels = NsenChannels;
/**
* @param {Object} [options]
* @param {Boolean} [options.connect=false] NsenChannel生成時にコメントサーバーへ自動接続するか指定します。
* @param {Number} [options.firstGetComments] 接続時に取得するコメント数
* @param {Number} [options.timeoutMs] タイムアウトまでのミリ秒
* @param {NicoSession}
* @return {Promise}
*/
NsenChannel.instanceFor = function(live, options, session) {
var nsen;
if (options == null) {
options = {};
}
_.defaults(options, {
connect: false
});
nsen = new NsenChannel(live, session);
return nsen._attachLive(live).then(function() {
if (!options.connect) {
return Promise.resolve(nsen);
}
return nsen.connect(options).then(function() {
return Promise.resolve(nsen);
});
});
};
/**
* @private
* @property {NicoLiveInfo} _live
*/
NsenChannel.prototype._live = null;
/**
* @private
* @property {CommentProvider} _commentProvider
*/
NsenChannel.prototype._commentProvider = null;
/**
* @private
* @property {NicoSession} _session
*/
NsenChannel.prototype._session = null;
/**
* 再生中の動画情報
* @private
* @property {NicoLiveInfo} _playingMovie
*/
NsenChannel.prototype._playingMovie = null;
/**
* @private
* @property {Number}
*/
NsenChannel.prototype._movieChangeDetectionTimer = null;
/**
* 最後にリクエストした動画情報
* @private
* @property {NicoVideoInfo} _requestedMovie
*/
NsenChannel.prototype._requestedMovie = null;
/**
* 最後にスキップした動画のID。
* 比較用なので動画IDだけ。
* @private
* @property {String} _lastSkippedMovieId
*/
NsenChannel.prototype._lastSkippedMovieId = null;
/**
* (午前4時遷移時の)移動先の配信のID
* @property {String} _nextLiveId
*/
NsenChannel.prototype._nextLiveId = null;
/**
* @param {NicoLiveInfo} liveInfo
* @param {NicoSession} _session
*/
function NsenChannel(liveInfo, _session) {
this._session = _session;
if (!(liveInfo instanceof NicoLiveInfo)) {
throw new TypeError("Passed object not instance of NicoLiveInfo.");
}
if (liveInfo.isNsenLive() === false) {
throw new TypeError("This live is not Nsen live streaming.");
}
NsenChannel.__super__.constructor.apply(this, arguments);
Object.defineProperties(this, {
id: {
get: function() {
return this.getChannelType();
}
}
});
this.onDidChangeMovie(this._didChangeMovie);
this.onWillClose(this._willClose);
}
/**
* @return {Promise}
*/
NsenChannel.prototype._attachLive = function(liveInfo) {
var ref1, sub;
if ((ref1 = this._channelSubscriptions) != null) {
ref1.dispose();
}
this._live = null;
this._commentProvider = null;
sub = this._channelSubscriptions = new CompositeDisposable;
this._live = liveInfo;
sub.add(liveInfo.onDidRefresh((function(_this) {
return function() {
return _this._didLiveInfoUpdated();
};
})(this)));
return this.fetch();
};
/**
* チャンネルの種類を取得します。
* @return {String} "vocaloid", "toho"など
*/
NsenChannel.prototype.getChannelType = function() {
return this._live.get("stream.nsenType");
};
/**
* 現在接続中の放送のNicoLiveInfoオブジェクトを取得します。
* @return {NicoLiveInfo}
*/
NsenChannel.prototype.getLiveInfo = function() {
return this._live;
};
/**
* 現在利用しているCommentProviderインスタンスを取得します。
* @return {CommentProvider?}
*/
NsenChannel.prototype.commentProvider = function() {
return this._commentProvider;
};
/**
* 現在再生中の動画情報を取得します。
* @return {NicoVideoInfo?}
*/
NsenChannel.prototype.getCurrentVideo = function() {
return this._playingMovie;
};
/**
* @return {NicoVideoInfo?}
*/
NsenChannel.prototype.getRequestedMovie = function() {
return this._requestedMovie;
};
/**
* スキップリクエストを送信可能か確認します。
* 基本的には、sendSkipイベント、skipAvailableイベントで
* 状態の変更を確認するようにします。
* @return {boolean
*/
NsenChannel.prototype.isSkipRequestable = function() {
var video;
video = this.getCurrentVideo();
return (video !== null) && (this._lastSkippedMovieId !== video.id);
};
/**
* @private
* @param {String} command NicoLive command with "/" prefix
* @param {Array.<String>} params command params
*/
NsenChannel.prototype._processLiveCommands = function(command, params, options) {
var entity, nextLiveId, operation, source, state, title, videoId, view;
if (params == null) {
params = [];
}
if (options == null) {
options = {};
}
switch (command) {
case "/prepare":
videoId = params[0];
return this._willMovieChange(videoId);
case "/play":
if (options.ignoreVideoChanged) {
break;
}
source = params[0], view = params[1], title = params[2];
videoId = /smile:((?:sm|nm)[1-9][0-9]*)/.exec(source);
if ((videoId != null ? videoId[1] : void 0) != null) {
return this._didDetectMovieChange(videoId[1]);
}
break;
case "/reset":
nextLiveId = params[0];
this._nextLiveId = nextLiveId;
return this.emit("will-close", nextLiveId);
case "/nspanel":
operation = params[0], entity = params[1];
return this._processNspanelCommand(operation, entity);
case "/nsenrequest":
state = params[0];
return this.emit("did-receive-request-state", state);
}
};
/**
* Processing /nspanel command
* @private
* @param {String} op
* @param {String} entity
*/
NsenChannel.prototype._processNspanelCommand = function(op, entity) {
var panelState;
if (op !== "show") {
return;
}
panelState = QueryString.parse(entity);
switch (true) {
case panelState.goodClick != null:
this.emit("did-receive-good");
return;
case panelState.mylistClick != null:
this.emit("did-receive-add-mylist");
return;
}
if (panelState.dj != null) {
this.emit("did-receive-tvchan-message", panelState.dj);
return;
}
this.emit("did-change-panel-state", {
goodBtn: panelState.goodBtn === "1",
mylistBtn: panelState.mylistBtn === "1",
skipBtn: panelState.skipBtn === "1",
title: panelState.title,
view: panelState.view | 0,
comment: panelState.comment | 0,
mylist: panelState.mylist | 0,
uploadDate: new Date(panelState.date),
playlistLen: panelState.playlistLen | 0,
corner: panelState.corner !== "0",
gage: panelState.gage | 0,
tv: panelState.tv | 0
});
};
/**
* サーバー側の情報とインスタンスの情報を同期します。
* @return {Promise}
*/
NsenChannel.prototype.fetch = function() {
var liveId;
if (this._live == null) {
return Promise.reject(new NicoException({
message: "LiveInfo not attached."
}));
}
liveId = this._live.get("stream").liveId;
return this._live.fetch().then((function(_this) {
return function() {
return APIEndpoints.nsen.syncRequest(_this._session, {
liveId: liveId
});
};
})(this))["catch"]((function(_this) {
return function(e) {
return Promise.reject(new NicoException({
message: "Failed to fetch Nsen request status. (" + e.message + ")",
previous: e
}));
};
})(this)).then((function(_this) {
return function(res) {
var $res, errorCode, status, videoId;
$res = Cheerio.load(res.body)(":root");
status = $res.attr("status");
errorCode = $res.find("error code").text();
if (status !== "ok" && errorCode !== "unknown") {
return Promise.reject(new NicoException({
message: "Failed to fetch Nsen request status. (" + errorCode + ")",
code: errorCode,
response: res.body
}));
}
if (errorCode === "unknown" && (_this._requestedMovie != null)) {
_this._requestedMovie = null;
_this.emit("did-cancel-request");
return Promise.resolve();
}
videoId = $res.find("id").text();
if (videoId.length === 0) {
return Promise.resolve();
}
if ((_this._requestedMovie != null) && _this._requestedMovie.id === videoId) {
return;
}
return _this._session.video.getVideoInfo(videoId);
};
})(this)).then((function(_this) {
return function(movie) {
if (movie == null) {
return;
}
_this._requestedMovie = movie;
_this.emit("did-send-request", movie);
return Promise.resolve();
};
})(this));
};
/**
* コメントサーバーへ接続します。
*
* @param {Object} [options]
* @param {Number} [options.firstGetComments] 接続時に取得するコメント数
* @param {Number} [options.timeoutMs] タイムアウトまでのミリ秒
* @return {Promise}
*/
NsenChannel.prototype.connect = function(options) {
if (options == null) {
options = {};
}
_.assign(options, {
connect: false
});
return this._live.commentProvider(options).then((function(_this) {
return function(provider) {
var sub;
_this._commentProvider = provider;
sub = _this._channelSubscriptions;
sub.add(provider.onDidProcessFirstResponse(function(comments) {
_this.lockAutoEmit("did-process-first-response", comments);
comments.forEach(function(comment) {
return _this._didCommentReceived(comment);
});
sub.add(new Disposable(function() {
return _this.unlockAutoEmit("did-process-first-response");
}));
return sub.add(provider.onDidReceiveComment(function(comment) {
return _this._didCommentReceived(comment);
}));
}));
sub.add(provider.onDidEndLive(function() {
return _this._onLiveClosed();
}));
return provider.connect(options);
};
})(this));
};
/**
* コメントサーバーに再接続します。
*/
NsenChannel.prototype.reconnect = function() {
return this._commentProvider.reconnect();
};
NsenChannel.prototype.dispose = function() {
this.emit("will-dispose");
this._live = null;
this._commentProvider = null;
this._channelSubscriptions.dispose();
return NsenChannel.__super__.dispose.apply(this, arguments);
};
/**
* リクエストを送信します。
* @param {NicoVideoInfo|String} movie リクエストする動画の動画IDかNicoVideoInfoオブジェクト
* @return {Promise}
*/
NsenChannel.prototype.pushRequest = function(movie) {
var promise;
promise = typeof movie === "string" ? this._session.video.getVideoInfo(movie) : Promise.resolve(movie);
return promise.then((function(_this) {
return function(movie) {
var liveId, movieId;
movieId = movie.id;
liveId = _this._live.get("stream.liveId");
return Promise.all([
APIEndpoints.nsen.request(_this._session, {
liveId: liveId,
movieId: movieId
}), movie
]);
};
})(this)).then((function(_this) {
return function(arg) {
var $res, movie, res;
res = arg[0], movie = arg[1];
$res = Cheerio.load(res.body)(":root");
if ($res.attr("status") !== "ok") {
return Promise.reject(new NicoException({
message: "Failed to push request",
code: $res.find("error code").text(),
response: res
}));
}
_this._requestedMovie = movie;
_this.emit("did-send-request", movie);
return Promise.resolve();
};
})(this));
};
/**
* リクエストをキャンセルします
* @return {Promise}
* キャンセルに成功すればresolveされます。
* (事前にリクエストが送信されていない場合もresolveされます。)
* リクエストに失敗した時、エラーメッセージつきでrejectされます。
*/
NsenChannel.prototype.cancelRequest = function() {
var liveId;
if (!this._requestedMovie) {
return Promise.resolve();
}
liveId = this._live.get("stream").liveId;
return APIEndpoints.nsen.cancelRequest(this._session, {
liveId: liveId
}).then((function(_this) {
return function(res) {
var $res;
$res = Cheerio.load(res.body)(":root");
if ($res.attr("status") !== "ok") {
return Promise.reject(new NicoException({
message: "Failed to cancel request",
code: $res.find("error code").text(),
response: res.body
}));
}
_this.emit("did-cancel-request", _this._requestedMovie);
_this._requestedMovie = null;
return Promise.resolve();
};
})(this));
};
/**
* Goodを送信します。
* @return {Promise}
* 成功したらresolveされます。
* 失敗した時、エラーメッセージつきでrejectされます。
*/
NsenChannel.prototype.pushGood = function() {
var liveId;
liveId = this._live.get("stream").liveId;
return APIEndpoints.nsen.sendGood(this._session, {
liveId: liveId
}).then((function(_this) {
return function(res) {
var $res;
$res = Cheerio.load(res.body)(":root");
if ($res.attr("status") !== "ok") {
return Promise.reject(new NicoException({
message: "Failed to push good",
code: $res.find("error code").text(),
response: res.body
}));
}
_this.emit("did-push-good");
return Promise.resolve();
};
})(this));
};
/**
* SkipRequestを送信します。
* @return {Promise}
* 成功したらresolveされます。
* 失敗した時、エラーメッセージつきでrejectされます。
*/
NsenChannel.prototype.pushSkip = function() {
var liveId, movieId, ref1;
liveId = this._live.get("stream").liveId;
movieId = ((ref1 = this.getCurrentVideo()) != null ? ref1.id : void 0) != null;
if (!this.isSkipRequestable()) {
return Promise.reject("Skip request already sended.");
}
return APIEndpoints.nsen.sendSkip(this._session, {
liveId: liveId
}).then((function(_this) {
return function(res) {
var $res;
$res = Cheerio.load(res.body).find(":root");
if ($res.attr("status") !== "ok") {
return Promise.reject(new NicoException({
message: "Failed to push skip",
code: $res.find("error code").text(),
response: res.body
}));
}
_this._lastSkippedMovieId = movieId;
_this.emit("did-push-skip");
return Promise.resolve();
};
})(this));
};
/**
* コメントを投稿します。
* @param {String} msg 投稿するコメント
* @param {String|Array.<String>} [command] コマンド(184, bigなど)
* @param {Number} [timeoutMs]
* @return {Promise} 投稿に成功すればresolveされ、
* 失敗すればエラーメッセージとともにrejectされます。
*/
NsenChannel.prototype.postComment = function(msg, command, timeoutMs) {
var ref1;
if (command == null) {
command = "";
}
if (timeoutMs == null) {
timeoutMs = 3000;
}
return (ref1 = this._commentProvider) != null ? ref1.postComment(msg, command, timeoutMs) : void 0;
};
/**
* 次のチャンネル情報を受信していれば、その配信へ移動します。
* @param {Object} [options]
* @param {Boolean} [options.connect=true] コメントサーバーへ自動接続するか指定します。
* @return {Promise}
* 移動に成功すればresolveされ、それ以外の時にはrejectされます。
*/
NsenChannel.prototype.moveToNextLive = function(options) {
if (options == null) {
options = {};
}
if (this._nextLiveId == null) {
return Promise.reject();
}
_.defaults(options, {
connect: true
});
return this._session.live.getLiveInfo(this._nextLiveId).then((function(_this) {
return function(liveInfo) {
_this._attachLive(liveInfo, options);
_this.emit("did-change-stream", liveInfo);
return _this.fetch();
};
})(this));
};
/**
* コメントを受信した時のイベントリスナ。
*
* 制御コメントの中からNsen内イベントを通知するコメントを取得して
* 関係するイベントを発火させます。
* @param {LiveComment} comment
*/
NsenChannel.prototype._didCommentReceived = function(comment, options) {
var command, params, ref1;
if (options == null) {
options = {
ignoreVideoChanged: false
};
}
if (comment.isControlComment() || comment.isPostByDistributor()) {
ref1 = comment.comment.split(" "), command = ref1[0], params = 2 <= ref1.length ? slice.call(ref1, 1) : [];
this._processLiveCommands(command, params, options);
}
this.emit("did-receive-comment", comment);
};
/**
* 配信情報が更新された時に実行される
* 再生中の動画などのデータを取得する
* @param {NicoLiveInfo} live
*/
NsenChannel.prototype._didLiveInfoUpdated = function() {
var content, ref1, videoId;
content = (ref1 = this._live.get("stream").contents[0]) != null ? ref1.content : void 0;
videoId = content && content.match(/^smile:((?:sm|nm)[1-9][0-9]*)/);
if ((videoId != null ? videoId[1] : void 0) == null) {
this._didDetectMovieChange(null);
return;
}
if ((this._playingMovie == null) || this._playingMovie.id !== videoId) {
return this._didDetectMovieChange(videoId[1]);
}
};
NsenChannel.prototype._willMovieChange = function(videoId) {
return this._session.video.getVideoInfo(videoId).then((function(_this) {
return function(video) {
_this.emit("will-change-movie", video);
};
})(this));
};
/**
* 再生中の動画の変更を検知した時に呼ばれるメソッド
* @private
* @param {String} videoId 次に再生される動画のID
*/
NsenChannel.prototype._didDetectMovieChange = function(videoId) {
if (this._movieChangeDetectionTimer != null) {
clearTimeout(this._movieChangeDetectionTimer);
this._movieChangeDetectionTimer = null;
}
this._movieChangeDetectionTimer = setTimeout((function(_this) {
return function() {
var beforeVideo;
beforeVideo = _this._playingMovie;
if (videoId == null) {
_this.emit("did-change-movie", null, beforeVideo);
_this._playingMovie = null;
return;
}
if ((beforeVideo != null ? beforeVideo.id : void 0) === videoId) {
return;
}
return _this._session.video.getVideoInfo(videoId).then(function(video) {
_this._playingMovie = video;
_this.emit("did-change-movie", video, beforeVideo);
});
};
})(this), 1000);
};
/**
* チャンネルの内部放送IDの変更を検知するリスナ
* @param {String} nextLiveId
*/
NsenChannel.prototype._willClose = function(nextLiveId) {
return this._nextLiveId = nextLiveId;
};
/**
* 放送が終了した時のイベントリスナ
*/
NsenChannel.prototype._onLiveClosed = function() {
this.emit("ended");
return this.moveToNextLive();
};
/**
* 再生中の動画が変わった時のイベントリスナ
*/
NsenChannel.prototype._didChangeMovie = function() {
this._lastSkippedMovieId = null;
return this.emit("did-available-skip");
};
/**
* @event NsenChannel#did-process-first-response
* @param {Array.<NicoLiveComment>}
*/
NsenChannel.prototype.onDidProcessFirstResponse = function(listener) {
return this.on("did-process-first-response", listener);
};
/**
* @event NsenChannel#did-receive-comment
* @param {NicoLiveComment} comment
*/
NsenChannel.prototype.onDidReceiveComment = function(listener) {
return this.on("did-receive-comment", listener);
};
/**
* @event NsenChannel#did-receive-good
*/
NsenChannel.prototype.onDidReceiveGood = function(listener) {
return this.on("did-receive-good", listener);
};
/**
* @event NsenChannel#did-receive-add-mylist
*/
NsenChannel.prototype.onDidReceiveAddMylist = function(listener) {
return this.on("did-receive-add-mylist", listener);
};
/**
* @event NsenChannel#did-push-good
*/
NsenChannel.prototype.onDidPushGood = function(listener) {
return this.on("did-push-good", listener);
};
/**
* @event NsenChannel#did-push-skip
*/
NsenChannel.prototype.onDidPushSkip = function(listener) {
return this.on("did-push-skip", listener);
};
/**
* @event NsenChannel#did-send-request
* @param {NicoVideoInfo} movie
*/
NsenChannel.prototype.onDidSendRequest = function(listener) {
return this.on("did-send-request", listener);
};
/**
* @event NsenChannel#did-cancel-request
* @param {NicoVideoInfo}
*/
NsenChannel.prototype.onDidCancelRequest = function(listener) {
return this.on("did-cancel-request", listener);
};
/**
* @event NsenChannel#will-change-movie
* @param {NicoVideoInfo} movie
*/
NsenChannel.prototype.onWillChangeMovie = function(listener) {
return this.on("will-change-movie", listener);
};
/**
* @event NsenChannel#did-change-movie
* @param {NicoVideoInfo} nextMovie
* @param {NicoVideoInfo} beforeMovie
*/
NsenChannel.prototype.onDidChangeMovie = function(listener) {
return this.on("did-change-movie", listener);
};
/**
* @event NsenChannel#did-available-skip
*/
NsenChannel.prototype.onDidAvailableSkip = function(listener) {
return this.on("did-available-skip", listener);
};
/**
* @event NsenChannel#will-close
* @param {String} nextLiveId
*/
NsenChannel.prototype.onWillClose = function(listener) {
return this.on("will-close", listener);
};
/**
* @event NsenChannel#did-receive-request-state
* @param {String} newState
*/
NsenChannel.prototype.onDidReceiveRequestState = function(listener) {
return this.on("did-receive-request-state", listener);
};
/**
* @event NsenChannel#did-change-panel-state
* @property {Boolean} goodBtn
* @property {Boolean} mylistBtn
* @property {Boolean} skipBtn
* @property {String} title
* @property {Number} view
* @property {Number} comment
* @property {Number} mylist
* @property {Date} uploadDate
* @property {Number} playlistLen
* @property {Boolean} corner
* @property {Number} gage
* @property {Number} tv
*/
NsenChannel.prototype.onDidChangePanelState = function(listener) {
return this.on("did-change-panel-state", listener);
};
/**
* @event NsenChannel#did-receive-tvchan-message
* @param {String} message
*/
NsenChannel.prototype.onDidReceiveTvchanMessage = function(listener) {
return this.on("did-receive-tvchan-message", listener);
};
/**
* @event NsenChannel#will-dispose
*/
NsenChannel.prototype.onWillDispose = function(listener) {
return this.on("will-dispose", listener);
};
return NsenChannel;
})(Emitter);
}).call(this);