Source: search-nico.js

'use strict';

/**
 * @fileoverview search-nico-js aims to provide a complete, asynchronous client library for the api.search.nicovideo.jp.
 *   For API details and how to use promises, see the JavaScript Promises articles.
 * @author shoito
 */

if (typeof window === 'undefined') {
    var Promise = Promise || require('promise');
    var XMLHttpRequest = XMLHttpRequest || require('xmlhttprequest').XMLHttpRequest;
}

(function() {
    function httpStatus(apiStatus) {
        var statusMap = {
            200: {'status': 200, 'text': 'OK'},
            300: {'status': 400, 'text': 'Bad Request'},
            101: {'status': 503, 'text': 'Service Unavailable'},
            1001: {'status': 504, 'text': 'Gateway Timeout'}
        };
        return statusMap[apiStatus] || {'status': 500, 'text': 'Internal Server Error'};
    }

    var ApiRequest = (function() {
        var ApiRequest = {};
        ApiRequest.BASE_URL = 'http://api.search.nicovideo.jp';

        ApiRequest.post = function(url, params, parseFunc) {
            return new Promise(function(resolve, reject) {
                var xhr = new XMLHttpRequest();
                xhr.onload = function() {
                    if (xhr.status === 200) {
                        var response = parseFunc(xhr.responseText);
                        if (response.status === 200) {
                            resolve(response);
                        } else {
                            reject({'status': response.status, 'error': response.statusText, 'error_description': 'An error has occurred while requesting api'});
                        }
                    } else {
                        reject({'status': xhr.status, 'error': xhr.statusText, 'error_description': 'An error has occurred while requesting api'});
                    }
                };
                xhr.onerror = reject;

                xhr.open('POST', ApiRequest.BASE_URL + url);
                xhr.timeout = params.timeout;
                xhr.send(JSON.stringify(params));
            });
        };

        return ApiRequest;
    })();


    var ContentsSearchRequest = (function() {
        var query = {
              'query':'keyword',
              'service':['video'],
              'search':['title'],
              'join':['cmsid'],
              'filters':[],
              'from':0,
              'size':10,
              'sort_by':'view_counter'
            };

        /**
         * コンテンツ検索リクエストのコンストラクタ
         * @class ContentsSearchRequest
         * @classdesc コンテンツ検索リクエスト
         * @memberOf SearchNico
         */
        function ContentsSearchRequest(options) {
            options = options || {};
            if (typeof options.issuer === 'undefined' ||
                typeof options.reason === 'undefined') {
                throw new Error('issuer and reason parameters is required.');
            }

            query.timeout = options.timeout || 3000;
            query.issuer = options.issuer;
            query.reason = options.reason;
        }

        function parse(rawResponse) {
            var response = {};
            response.status = httpStatus(200).status;
            rawResponse.split('\n').forEach(function(rawChunk) {
                if (rawChunk === '') {
                    return;
                }

                var chunk = JSON.parse(rawChunk);
                if (chunk.type === 'stats' && chunk.values) {
                    response.hits = chunk.values[0].total;
                } else if (chunk.type === 'hits' && chunk.values) {
                    response.values = chunk.values.map(function(content) {
                        delete content._rowid;
                        return content;
                    });
                } else if (chunk.errid) {
                    var http = httpStatus(chunk.errid);
                    response.status = http.status;
                    response.statusText = http.text;
                }
            });

            if (typeof response.hits === 'undefined') {
                response.hits = 0;
                response.values = [];
            }
            return response;
        }

        /**
         * equalフィルタオブジェクトを生成する
         * @memberof SearchNico.ContentsSearchRequest
         * @method equalFilter
         * @instance
         * @param {String} field - フィルタ対象フィールド
         * @param {String/Number} value - フィルタ対象の値
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.equalFilter = function(field, value) {
            return {'type': 'equal', 'field': field, 'value': value};
        };

        /**
         * rangeフィルタオブジェクトを生成する
         * @memberof SearchNico.ContentsSearchRequest
         * @method rangeFilter
         * @instance
         * @param {String} field - フィルタ対象フィールド
         * @param {String/Number} from - 開始値
         * @param {String/Number} to - 終了値
         * @param {Boolean} includeUpper - 上限値を含むかどうか
         * @param {Boolean} includeLower - 下限値を含むかどうか
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.rangeFilter = function(field, from, to, includeUpper, includeLower) {
            var filter = {'type': 'range', 'field': field, 'include_upper': true, 'include_lower': true};
            if (typeof from !== 'undefined') {
                filter.from = from;
            }
            if (typeof to !== 'undefined') {
                filter.to = to;
            }
            if (typeof includeUpper !== 'undefined') {
                filter.include_upper = includeUpper;
            }
            if (typeof includeLower !== 'undefined') {
                filter.include_lower = includeLower;
            }
            return filter;
        };

        /**
         * 検索対象サービスを設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method service
         * @instance
         * @param {String} name - 検索対象サービス
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.service = function(name) {
            query.service = [name];
            return this;
        };

        /**
         * 検索キーワードを設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method keyword
         * @instance
         * @param {String} keyword - 検索キーワード
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.keyword = function(keyword) {
            query.query = keyword;
            return this;
        };

        /**
         * 検索対象となるフィールドを設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method target
         * @instance
         * @param {Array} names - 検索対象フィールドリスト
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.target = function(names) {
            query.search = names;
            return this;
        };

        /**
         * フィルタ条件を設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method filter
         * @instance
         * @param {Array} filters - フィルタ条件リスト
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.filter = function(filters) {
            query.filters = filters;
            return this;
        };

        /**
         * ソート条件を設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method sort
         * @instance
         * @param {String} name - ソート条件となるフィールド
         * @param {String} [order=desc] - asc/desc (昇順/降順)
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.sort = function(name, order) {
            query.sort_by = name;
            query.order = order || 'desc';
            return this;
        };

        /**
         * 検索結果に含めるフィールドを設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method select
         * @instance
         * @param {Array} names - 取得対象のフィールドリスト
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.select = function(names) {
            query.join = names;
            return this;
        };

        /**
         * 検索結果の取得開始位置を設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method from
         * @instance
         * @param {Number} from - 検索結果の取得開始位置
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.from = function(from) {
            query.from = from;
            return this;
        };

        /**
         * 検索結果の取得件数を設定する
         * @memberof SearchNico.ContentsSearchRequest
         * @method size
         * @instance
         * @param {Number} size - コンテンツ検索結果の取得件数
         * @return {ContentsSearchRequest} コンテンツ検索リクエストインスタンス
         */
        ContentsSearchRequest.prototype.size = function(size) {
            query.size = size;
            return this;
        };

        /**
         * 検索リクエストのプロミスオブジェクトを取得する
         * @memberof SearchNico.ContentsSearchRequest
         * @method fetch
         * @instance
         * @return {Promise} プロミスオブジェクト - resolve/reject関数がプロミスの成功または失敗に応じて呼ばれる
         */
        ContentsSearchRequest.prototype.fetch = function() {
            return ApiRequest.post('/api/', query, parse);
        };

        return ContentsSearchRequest;
    })();


    var TagsSearchRequest = (function() {
        var query = {
              'query':'keyword',
              'service':['video'],
              'from':0,
              'size':10
            };

        /**
         * 関連タグ検索リクエストのコンストラクタ
         * @class TagsSearchRequest
         * @classdesc 関連タグ検索リクエスト
         * @memberof SearchNico
         */
        function TagsSearchRequest(options) {
            options = options || {};
            if (typeof options.issuer === 'undefined' ||
                typeof options.reason === 'undefined') {
                throw new Error('issuer and reason parameters is required.');
            }

            query.timeout = options.timeout || 3000;
            query.issuer = options.issuer;
            query.reason = options.reason;
        }

        function parse(rawResponse) {
            var response = {};
            response.status = httpStatus(200).status;
            rawResponse.split('\n').forEach(function(rawChunk) {
                if (rawChunk === '') {
                    return;
                }

                var chunk = JSON.parse(rawChunk);
                if (chunk.type === 'tags' && chunk.values) {
                    response.values = chunk.values.map(function(value) {
                        return value.tag;
                    });
                } else if (chunk.errid) {
                    var http = httpStatus(chunk.errid);
                    response.status = http.status;
                    response.statusText = http.text;
                }
            });

            if (typeof response.values === 'undefined') {
                response.values = [];
            }
            return response;
        }

        /**
         * 検索対象サービスを設定する
         * @memberof SearchNico.TagsSearchRequest
         * @method service
         * @instance
         * @param {String} name - 関連タグの検索対象サービス
         * @return {TagsSearchRequest} 関連タグ検索リクエストインスタンス
         */
        TagsSearchRequest.prototype.service = function(name) {
            query.service = ['tag_' + name];
            return this;
        };

        /**
         * 検索キーワードを設定する
         * @memberof SearchNico.TagsSearchRequest
         * @method keyword
         * @instance
         * @param {String} keyword - 関連タグの検索キーワード
         * @return {TagsSearchRequest} 関連タグ検索リクエストインスタンス
         */
        TagsSearchRequest.prototype.keyword = function(keyword) {
            query.query = keyword;
            return this;
        };

        /**
         * 検索結果の取得開始位置を設定する
         * @memberof SearchNico.TagsSearchRequest
         * @method from
         * @instance
         * @param {Number} from - 検索結果の取得開始位置
         * @return {TagsSearchRequest} 関連タグ検索リクエストインスタンス
         */
        TagsSearchRequest.prototype.from = function(from) {
            query.from = from;
            return this;
        };

        /**
         * 検索結果の取得件数を設定する
         * @memberof SearchNico.TagsSearchRequest
         * @method size
         * @instance
         * @param {Number} size - 関連タグの検索結果の取得件数
         * @return {TagsSearchRequest} 関連タグ検索リクエストインスタンス
         */
        TagsSearchRequest.prototype.size = function(size) {
            query.size = size;
            return this;
        };

        /**
         * 検索リクエストのプロミスオブジェクトを取得する
         * @memberof SearchNico.TagsSearchRequest
         * @method fetch
         * @instance
         * @return {Promise} プロミスオブジェクト - resolve/reject関数がプロミスの成功または失敗に応じて呼ばれる
         */
        TagsSearchRequest.prototype.fetch = function() {
            return ApiRequest.post('/api/tag/', query, parse);
        };

        return TagsSearchRequest;
    })();


    /**
     * niconico検索APIクライアント
     * @namespace
     * @name SearchNico
     */
    var SearchNico = {};

    /**
     * コンテンツ検索リクエストのインスタンスを取得する
     * @memberof SearchNico
     * @method
     * @param {Object} [options] - APIパラメーター
     * @param {String} [options.issuer] - サービス/アプリケーション名
     * @param {String} [options.reason=html5jc] - コンテスト/イベント名
     * @param {Number} [options.tiemout=3000] - timeout(ms)
     * @see {@link http://search.nicovideo.jp/docs/api/html5jc.html}
     */
    SearchNico.contents = function(options) {
        return new ContentsSearchRequest(options);
    };

    /**
     * 関連タグ検索リクエストのインスタンスを取得する
     * @memberof SearchNico
     * @method
     * @param {Object} [options] - APIパラメーター
     * @param {String} [options.issuer] - サービス/アプリケーション名
     * @param {String} [options.reason=html5jc] - コンテスト/イベント名
     * @param {Number} [options.tiemout=3000] - timeout(ms)
     * @see {@link http://search.nicovideo.jp/docs/api/html5jc.html}
     */
    SearchNico.tags = function(options) {
        return new TagsSearchRequest(options);
    };


    if (typeof module !== 'undefined') {
        module.exports = SearchNico;
    } else {
        // <script src='https://www.promisejs.org/polyfills/promise-4.0.0.js'></script>
        this.SearchNico = SearchNico;
    }
}).call(this);