API Docs for:
Show:

File: src/live/NicoLiveInfo.coffee

###
# ニコニコ生放送の配信情報
#
# Properties
#   stream:     -- 放送の基礎情報
#       liveId:         string -- 放送ID
#       title:          string -- 放送タイトル
#       description:    string -- 放送の説明
#
#       watchCount:     number -- 視聴数
#       commentCount:   number -- コメント数
#
#       baseTime:       Date -- 生放送の時間の関わる計算の"元になる時間"
#       startTime:      Date -- 放送の開始時刻
#       openTime:       Date -- 放送の開場時間
#       endTime:        Date -- 放送の終了時刻(放送中であれば終了予定時刻)
#
#       isOfficial:     boolean -- 公式配信か
#       isNsen:         boolean -- Nsenのチャンネルか
#       nsenType:       string -- Nsenのチャンネル種別("nsen/***"の***の部分)
#
#       contents:       Array<Object>
#           id:             string -- メイン画面かサブ画面か
#           startTime:      number -- 再生開始時間
#           disableAudio:   boolean -- 音声が無効にされているか
#           disableVideo:   boolean -- 映像が無効にされているか
#           duration:       number|null -- 再生されているコンテンツの長さ(秒数)
#           title:          string|null -- 再生されているコンテンツのタイトル
#           content:        string -- 再生されているコンテンツのアドレス(動画の場合は"smile:動画ID")
#
#   owner:      -- 配信者の情報
#       userId:         number -- ユーザーID
#       name:           string -- ユーザー名
#
#   user:       -- 自分自身の情報
#       id:             number -- ユーザーID
#       name:           string -- ユーザー名
#       isPremium:      boolean -- プレミアムアカウントか
#
#   rtmp:       -- 配信に関する情報。詳細不明
#       isFms:          boolean
#       port:           number
#       url:            string
#       ticket:         string
#
#   comment:    -- コメントサーバーの情報
#       addr:           string -- サーバーアドレス
#       port:           number -- サーバーポート
#       thread:         number -- この放送と対応するスレッドID
#
# @class NicoLiveInfo
###

_ = require "lodash"
__ = require "lodash-deep"
Cheerio = require "cheerio"
Request = require "request-promise"
{sprintf} = require "sprintf"

APIEndpoints = require "../APIEndpoints"
NicoURL = require "../NicoURL"
NicoException = "../NicoException"
Emitter = require "../Emitter"
CommentProvider = require "./CommentProvider"

module.exports =
class NicoLiveInfo extends Emitter

    ###*
    # @propery {Object}
    ###
    @defaults :
        stream      :
            liveId      : null
            title       : null
            description : null

            watchCount  : -1
            commentCount    : -1

            baseTime    : null
            openTime    : null
            startTime   : null
            endTime     : null

            isOfficial  : false
            isNsen      : false
            nsenType    : null

            contents    : {
                #  id:string,
                #  startTime:number,
                #  disableAudio:boolean,
                #  disableVideo:boolean,
                #  duration:number|null,
                #  title:string|null,
                #  content:string
            }

        owner       :
            userId      : -1
            name        : null

        user        :
            id          : -1
            name        : null
            isPremium   : null

        rtmp        :
            isFms       : null
            port        : null
            url         : null
            ticket      : null

        comment     :
            addr        : null
            port        : -1
            thread      : null

        _hasError   : true

    ###*
    # @static
    # @return {Promise}
    ###
    @instanceFor : (liveId, session) ->
        if typeof liveId isnt "string" or liveId is ""
            throw new TypeError("liveId must bea string")

        live = new NicoLiveInfo(liveId, session)
        live.fetch().then -> Promise.resolve(live)


    ###*
    # マイリストが最新の内容に更新された時に発火します
    # @event MyList#did-refresh
    # @property {NicoLiveInfo}  live
    ###

    ###*
    # @private
    # @property {CommentProvider}   _commentProvider
    ###
    _commentProvider : null

    ###*
    # @private
    # @property {NicoSession}   _session
    ###
    _session : null

    ###*
    # @private
    # @property {Object}    _attr
    ###
    _attr : null

    ###*
    # @property {String}    id
    ###


    ###*
    # @param {NicoSession}  session 認証チケット
    # @param {string}       liveId  放送ID
    ###
    constructor     : (liveId, @_session) ->
        super

        Object.defineProperties @,
            id :
                value : liveId


    ###*
    # 公式放送か調べます。
    # @return {boolean}
    ###
    isOfficialLive : ->
        !!@get("stream").isOfficial


    ###*
    # Nsenのチャンネルか調べます。
    # @return {boolean}
    ###
    isNsenLive : ->
        !!@get("stream").isNsen


    ###*
    # 放送が終了しているか調べます。
    # @return {boolean}
    ###
    isEnded         : ->
        @get("isEnded") is true


    ###*
    # @param {String}   path
    ###
    get : (path) ->
        __.deepGet @_attr, path


    ###*
    # この放送に対応するCommentProviderオブジェクトを取得します。
    # @param {Object} options 接続設定
    # @param {Number} [options.firstGetComments] 接続時に取得するコメント数
    # @param {Number} [options.timeoutMs] タイムアウトまでのミリ秒
    # @param {Boolean} [options.connect=true] trueを指定するとコネクション確立後にresolveします
    # @return {Promise}
    ###
    commentProvider  : (options = {
        connect: true
    }) ->
        unless @_commentProvider?
            return CommentProvider.instanceFor(@, options).then (provider) =>
                @_commentProvider = provider
                provider.onDidEndLive @_didEndLive.bind(@)

                if options.connect
                    provider.connect(options)
                else
                    Promise.resolve provider

        Promise.resolve @_commentProvider


    ###*
    # APIから取得した情報をパースします。
    # @private
    # @param {String}   res     API受信結果
    ###
    parse           : (res) ->
        $res    = Cheerio.load res
        $root   = $res ":root"
        $stream = $res "stream"
        $user   = $res "user"
        $rtmp   = $res "rtmp"
        $ms     = $res "ms"
        props     = null

        if $root.attr("status") isnt "ok"
            errorCode = $res("error code").text()
            throw new NicoException
                message: "Failed to parse live info (#{errorCode})"
                code : errorCode
                response : res

        props =
            stream  :
                liveId      : $stream.find("id").text()
                title       : $stream.find("title").text()
                description : $stream.find("description").text()

                watchCount  : $stream.find("watch_count").text()|0
                commentCount: $stream.find("comment_count")|0

                baseTime    : new Date(($stream.find("base_time").text()|0) * 1000)
                openTime    : new Date(($stream.find("open_time").text()|0) * 1000)
                startTime   : new Date(($stream.find("start_time").text()|0) * 1000)
                endTime     : new Date(($stream.find("end_time")|0) * 1000)

                isOfficial  : $stream.find("provider_type").text() is "official"
                isNsen      : $res("ns").length > 0
                nsenType    : $res("ns nstype").text() or null

                contents    : _.map $stream.find("contents_list contents"), (el) ->
                    $content = Cheerio el
                    {
                        id              : $content.attr("id")
                        startTime       : new Date(($content.attr("start_time")|0) * 1000)
                        disableAudio    : ($content.attr("disableAudio")|0) is 1
                        disableVideo    : ($content.attr("disableVideo")|0) is 1
                        duration        : $content.attr("duration")|0 ? null # ついてない時がある
                        title           : $content.attr("title") ? null      # ついてない時がある
                        content         : $content.text()
                    }

            # 放送者情報
            owner   :
                userId      : $stream.find("owner_id").text()|0
                name        : $stream.find("owner_name").text()

            # ユーザー情報
            user    :
                id          : $user.find("user_id").text()|0
                name        : $user.find("nickname").text()
                isPremium   : $user.find("is_premium").text() is "1"

            # RTMP情報
            rtmp    :
                isFms       : $rtmp.attr("is_fms") is "1"
                port        : $rtmp.attr("rtmpt_port")|0
                url         : $rtmp.find("url").text()
                ticket      : $rtmp.find("ticket").text()

            # コメントサーバー情報
            comment :
                addr        : $ms.find("addr").text()
                port        : $ms.find("port").text()|0
                thread      : $ms.find("thread").text()|0

            _hasError: $res("getplayerstatus").attr("status") isnt "ok"

        props


    ###*
    # 番組情報を最新の状態に同期します。
    # @return {Promise}
    ###
    fetch :  ->
        APIEndpoints.live.getPlayerStatus(@_session, {liveId : @id})
        .then (res) =>
            # check errors
            if res.statusCode is 503
                return Promise.reject new Error(sprintf("Live[%s]: Nicovideo has in maintenance.", @id))

            @_attr = @parse(res.body)
            @emit "did-refresh", @

            Promise.resolve()

    ###*
    # 現在のインスタンスおよび、関連するオブジェクトを破棄し、利用不能にします。
    ###
    dispose : ->
        @_commentProvider?.dispose()
        @_commentProvider = null
        delete NicoLiveInfo._cache[@id]
        super


    #
    # Event Listeners
    #

    _didEndLive : ->
        @_attr.isEnded = true
        return


    #
    # Event Handlers
    #
    onDidRefresh : (listener) ->
        @on "did-refresh", listener