UNPKG

node-nicovideo-api

Version:

nicovideo api (video, live, etc..) wrapper package for node.js

955 lines (779 loc) 27.2 kB
// 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);