###*
# @class NsenChannel
###
_ = require "lodash"
Emitter = require "../Emitter"
Cheerio = require "cheerio"
deepFreeze = require "deep-freeze"
Request = require "request-promise"
{sprintf} = require("sprintf")
{CompositeDisposable} = require "event-kit"
QueryString = require "querystring"
APIEndpoints = require "../APIEndpoints"
NicoException = require "../NicoException"
NicoLiveInfo = require "./NicoLiveInfo"
NsenChannels = require "./NsenChannels"
module.exports =
class NsenChannel extends Emitter
###*
# Nsenリクエスト時のエラーコード
# @const {Object.<String, String>
###
@RequestError : deepFreeze
NO_LOGIN : "not_login"
CLOSED : "nsen_close"
REQUIRED_TAG : "nsen_tag"
TOO_LONG : "nsen_long"
REQUESTED : "nsen_requested"
# "ログインしていません。"
# "現在リクエストを受け付けていません。"
# "リクエストに必要なタグが登録されていません。"
# "動画が長過ぎます。"
# "リクエストされたばかりです。"
@Gage : deepFreeze
BLUE : 0
GREEN : 1
YELLOW : 2
ORANGE : 3
RED : 4
@Channels : NsenChannels
###*
# @return Promise
###
@instanceFor : (live, session) ->
nsen = new NsenChannel(live, session)
nsen._attachLive(live)
Promise.resolve nsen
###*
# @private
# @property {NicoLiveInfo} _live
###
_live : null
###*
# @private
# @property {CommentProvider} _commentProvider
###
_commentProvider : null
###*
# @private
# @property {NicoSession} _session
###
_session : null
###*
# 再生中の動画情報
# @private
# @property {NicoLiveInfo} _playingMovie
###
_playingMovie : null
###*
# 最後にリクエストした動画情報
# @private
# @property {NicoVideoInfo} _requestedMovie
###
_requestedMovie : null
###*
# 最後にスキップした動画のID。
# 比較用なので動画IDだけ。
# @private
# @property {String} _lastSkippedMovieId
###
_lastSkippedMovieId : null
###*
# (午前4時遷移時の)移動先の配信のID
# @property {String} _nextLiveId
###
_nextLiveId : null
###*
# @param {NicoLiveInfo} liveInfo
# @param {NicoSession} _session
###
constructor : (liveInfo, @_session) ->
if liveInfo not instanceof NicoLiveInfo
throw new TypeError "Passed object not instance of NicoLiveInfo."
if liveInfo.isNsenLive() is false
throw new TypeError "This live is not Nsen live streaming."
super
Object.defineProperties @,
id :
get : -> @getChannelType()
@onDidChangeMovie @_didChangeMovie
@onWillClose @_willClose
###*
# @return {Promise}
###
_attachLive : (liveInfo) ->
@_channelSubscriptions?.dispose()
@_live = null
@_commentProvider = null
@_channelSubscriptions = sub = new CompositeDisposable
@_live = liveInfo
sub.add liveInfo.onDidRefresh =>
@_didLiveInfoUpdated()
liveInfo.commentProvider({connect: true})
.then (provider) =>
@_commentProvider = provider
# if provider.isFirstResponseProsessed is no
# sub.add provider.onDidProcessFirstResponse (comments) =>
# comments.forEach (comment) =>
# @_didCommentReceived comment, {ignoreVideoChanged: true}
#
# sub.add provider.onDidReceiveComment (comment) =>
# @_didCommentReceived(comment)
#
# else
sub.add provider.onDidReceiveComment (comment) =>
@_didCommentReceived(comment)
sub.add provider.onDidEndLive => @_onLiveClosed()
@_didLiveInfoUpdated()
@fetch()
###*
# チャンネルの種類を取得します。
# @return {String} "vocaloid", "toho"など
###
getChannelType : ->
@_live.get("stream.nsenType")
###*
# 現在接続中の放送のNicoLiveInfoオブジェクトを取得します。
# @return {NicoLiveInfo}
###
getLiveInfo : ->
@_live
###*
# 現在利用しているCommentProviderインスタンスを取得します。
# @return {CommentProvider?}
###
commentProvider : ->
@_commentProvider
###*
# 現在再生中の動画情報を取得します。
# @return {NicoVideoInfo?}
###
getCurrentVideo : ->
return @_playingMovie
###*
# @return {NicoVideoInfo?}
###
getRequestedMovie : ->
@_requestedMovie
###*
# スキップリクエストを送信可能か確認します。
# 基本的には、sendSkipイベント、skipAvailableイベントで
# 状態の変更を確認するようにします。
# @return {boolean
###
isSkipRequestable : ->
video = @getCurrentVideo()
return (video isnt null) and (@_lastSkippedMovieId isnt video.id)
###*
# @private
# @param {String} command NicoLive command with "/" prefix
# @param {Array.<String>} params command params
###
_processLiveCommands : (command, params = [], options = {}) ->
switch command
when "/prepare"
@emit "will-change-movie"
when "/play"
break if options.ignoreVideoChanged
[source, view, title] = params
videoId = /smile:((?:sm|nm)[1-9][0-9]*)/.exec(source)
if videoId?[1]?
@_didDetectMovieChange videoId[1]
# @emit "did-change-movie", videoId[1]
when "/reset"
[nextLiveId] = params
@_nextLiveId = nextLiveId
@emit "will-close", nextLiveId
when "/nspanel"
[operation, entity] = params
@_processNspanelCommand(operation, entity)
when "/nsenrequest"
[state] = params # "on", "lot"
@emit "did-receive-request-state", state
###*
# Processing /nspanel command
# @private
# @param {String} op
# @param {String} entity
###
_processNspanelCommand : (op, entity) ->
return if op isnt "show"
panelState = QueryString.parse(entity)
switch true
when panelState.goodClick?
@emit "did-receive-good"
return
when panelState.mylistClick?
@emit "did-receive-add-mylist"
return
if panelState.dj?
@emit "did-receive-tvchan-message", panelState.dj
return
@emit "did-change-panel-state", {
goodBtn : panelState.goodBtn is "1"
mylistBtn : panelState.mylistBtn is "1"
skipBtn : panelState.skipBtn is "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 isnt "0"
gage : panelState.gage | 0
tv : panelState.tv | 0
}
return
###*
# サーバー側の情報とインスタンスの情報を同期します。
# @return {Promise}
###
fetch : ->
return unless @_live?
Promise.reject new NicoException
message: "LiveInfo not attached."
# リクエストした動画の情報を取得
liveId = @_live.get("stream").liveId
@_live.fetch()
.then =>
APIEndpoints.nsen.syncRequest(@_session, {liveId})
.catch (e) =>
Promise.reject new NicoException
message : "Failed to fetch Nsen request status. (#{e.message})"
previous : e
.then (res) =>
$res = Cheerio.load(res.body)(":root")
status = $res.attr("status")
errorCode = $res.find("error code").text()
return if status isnt "ok" and errorCode isnt "unknown"
Promise.reject new NicoException
message : "Failed to fetch Nsen request status. (#{errorCode})"
code : errorCode
response : res.body
return if errorCode is "unknown" and @_requestedMovie?
@_requestedMovie = null
@emit "did-cancel-request"
Promise.resolve()
# リクエストの取得に成功したら動画情報を同期
videoId = $res.find("id").text()
# 直前にリクエストした動画と内容が異なれば
# 新しい動画に更新
if not @_requestedMovie? or @_requestedMovie.id isnt videoId
@_session.video.getVideoInfo(videoId)
.then (movie) =>
return unless movie?
@_requestedMovie = movie
@emit "did-send-request", movie
Promise.resolve()
###*
# コメントサーバーに再接続します。
###
reconnect : ->
@_commentProvider.reconnect()
dispose : ->
@emit "will-dispose"
@_live = null
@_commentProvider = null
@_channelSubscriptions.dispose()
super
#
# Nsen control methods
#
###*
# リクエストを送信します。
# @param {NicoVideoInfo|String} movie リクエストする動画の動画IDかNicoVideoInfoオブジェクト
# @return {Promise}
###
pushRequest : (movie) ->
promise = if typeof movie is "string"
@_session.video.getVideoInfo(movie)
else
Promise.resolve(movie)
promise.then (movie) =>
movieId = movie.id
liveId = @_live.get("stream.liveId")
Promise.all([APIEndpoints.nsen.request(@_session, {liveId, movieId}), movie])
.then ([res, movie]) =>
$res = Cheerio.load(res.body)(":root")
return unless $res.attr("status") is "ok"
Promise.reject new NicoException
message : "Failed to push request"
code : $res.find("error code").text()
response : res
@_requestedMovie = movie
@emit "did-send-request", movie
Promise.resolve()
###*
# リクエストをキャンセルします
# @return {Promise}
# キャンセルに成功すればresolveされます。
# (事前にリクエストが送信されていない場合もresolveされます。)
# リクエストに失敗した時、エラーメッセージつきでrejectされます。
###
cancelRequest : ->
if not @_requestedMovie
return Promise.resolve()
liveId = @_live.get("stream").liveId
APIEndpoints.nsen.cancelRequest(@_session, {liveId})
.then (res) =>
$res = Cheerio.load(res.body)(":root")
return if $res.attr("status") isnt "ok"
Promise.reject new NicoException
message : "Failed to cancel request"
code : $res.find("error code").text()
response : res.body
@emit "did-cancel-request", @_requestedMovie
@_requestedMovie = null
Promise.resolve()
###*
# Goodを送信します。
# @return {Promise}
# 成功したらresolveされます。
# 失敗した時、エラーメッセージつきでrejectされます。
###
pushGood : ->
liveId = @_live.get("stream").liveId
APIEndpoints.nsen.sendGood(@_session, {liveId})
.then (res) =>
$res = Cheerio.load(res.body)(":root")
return if $res.attr("status") isnt "ok"
Promise.reject new NicoException
message : "Failed to push good"
code : $res.find("error code").text()
response : res.body
@emit "did-push-good"
Promise.resolve()
###*
# SkipRequestを送信します。
# @return {Promise}
# 成功したらresolveされます。
# 失敗した時、エラーメッセージつきでrejectされます。
###
pushSkip : ->
liveId = @_live.get("stream").liveId
movieId = @getCurrentVideo()?.id?
if ! @isSkipRequestable()
return Promise.reject "Skip request already sended."
APIEndpoints.nsen.sendSkip(@_session, {liveId})
.then (res) =>
$res = Cheerio.load(res.body).find(":root")
# 通信に失敗
return if $res.attr("status") isnt "ok"
Promise.reject new NicoException
message : "Failed to push skip"
code : $res.find("error code").text()
response : res.body
@_lastSkippedMovieId = movieId
@emit "did-push-skip"
Promise.resolve()
###*
# コメントを投稿します。
# @param {String} msg 投稿するコメント
# @param {String|Array.<String>} [command] コマンド(184, bigなど)
# @param {Number} [timeoutMs]
# @return {Promise} 投稿に成功すればresolveされ、
# 失敗すればエラーメッセージとともにrejectされます。
###
postComment : (msg, command = "", timeoutMs = 3000) ->
@_commentProvider?.postComment(msg, command, timeoutMs)
###*
# 次のチャンネル情報を受信していれば、その配信へ移動します。
# @return {Promise}
# 移動に成功すればresolveされ、それ以外の時にはrejectされます。
###
moveToNextLive : ->
return Promise.reject() unless @_nextLiveId?
liveId = @_nextLiveId
return unless liveId?
Promise.reject new NicoException
message : "Next liveId is unknown."
# 放送情報を取得
@_session.live.getLiveInfo(liveId)
.then (liveInfo) =>
@_attachLive(liveInfo)
@emit "did-change-stream", liveInfo
@fetch()
#
# Event Listeners
#
###*
# コメントを受信した時のイベントリスナ。
#
# 制御コメントの中からNsen内イベントを通知するコメントを取得して
# 関係するイベントを発火させます。
# @param {LiveComment} comment
###
_didCommentReceived : (comment, options = {ignoreVideoChanged : false}) ->
if comment.isControlComment() or comment.isPostByDistributor()
[command, params...] = comment.comment.split(" ")
@_processLiveCommands command, params, options
@emit "did-receive-comment", comment
return
###*
# 配信情報が更新された時に実行される
# 再生中の動画などのデータを取得する
# @param {NicoLiveInfo} live
###
_didLiveInfoUpdated : ->
content = @_live.get("stream").contents[0]?.content
videoId = content and content.match(/^smile:((?:sm|nm)[1-9][0-9]*)/)
unless videoId?[1]?
@_didDetectMovieChange null
return
if (not @_playingMovie?) or @_playingMovie.id isnt videoId
@_didDetectMovieChange videoId[1]
###*
# 再生中の動画の変更を検知した時に呼ばれるメソッド
# @private
# @param {String} videoId 次に再生される動画のID
###
_didDetectMovieChange : (videoId) ->
return if @_videoInfoFetcher?
beforeVideo = @_playingMovie
if videoId is null
@emit "did-change-movie", null, beforeVideo
@_playingMovie = null
return
return if beforeVideo?.id is videoId
@_videoInfoFetcher = @_session.video.getVideoInfo(videoId).then (video) =>
@_playingMovie = video
@_videoInfoFetcher = null
@emit "did-change-movie", video, beforeVideo
return
return
###*
# チャンネルの内部放送IDの変更を検知するリスナ
# @param {String} nextLiveId
###
_willClose : (nextLiveId) ->
@_nextLiveId = nextLiveId
###*
# 放送が終了した時のイベントリスナ
###
_onLiveClosed : ->
@emit "ended"
# 放送情報を差し替え
@moveToNextLive()
###*
# 再生中の動画が変わった時のイベントリスナ
###
_didChangeMovie : ->
@_lastSkippedMovieId = null
@emit "did-available-skip"
#
# Event Handlers
#
###*
# @event NsenChannel#did-receive-comment
# @param {NicoLiveComment} comment
###
onDidReceiveComment : (listener) ->
@on "did-receive-comment", listener
###*
# @event NsenChannel#did-receive-good
###
onDidReceiveGood : (listener) ->
@on "did-receive-good", listener
###*
# @event NsenChannel#did-receive-add-mylist
###
onDidReceiveAddMylist : (listener) ->
@on "did-receive-add-mylist", listener
###*
# @event NsenChannel#did-push-good
###
onDidPushGood : (listener) ->
@on "did-push-good", listener
###*
# @event NsenChannel#did-push-skip
###
onDidPushSkip : (listener) ->
@on "did-push-skip", listener
###*
# @event NsenChannel#did-send-request
# @param {NicoVideoInfo} movie
###
onDidSendRequest : (listener) ->
@on "did-send-request", listener
###*
# @event NsenChannel#did-cancel-request
# @param {NicoVideoInfo}
###
onDidCancelRequest : (listener) ->
@on "did-cancel-request", listener
###*
# @event NsenChannel#did-change-movie
# @param {NicoVideoInfo} nextMovie
# @param {NicoVideoInfo} beforeMovie
###
onDidChangeMovie : (listener) ->
@on "did-change-movie", listener
###*
# @event NsenChannel#did-available-skip
###
onDidAvailableSkip : (listener) ->
@on "did-available-skip", listener
###*
# @event NsenChannel#will-close
# @param {String} nextLiveId
###
onWillClose : (listener) ->
@on "will-close", listener
###*
# @event NsenChannel#did-receive-request-state
# @param {String} newState
###
onDidReceiveRequestState : (listener) ->
@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
###
onDidChangePanelState : (listener) ->
@on "did-change-panel-state", listener
###*
# @event NsenChannel#did-receive-tvchan-message
# @param {String} message
###
onDidReceiveTvchanMessage : (listener) ->
@on "did-receive-tvchan-message", listener
###*
# @event NsenChannel#will-dispose
###
onWillDispose : (listener) ->
@on "will-dispose", listener