/**
* Bangumi API client library for node.js.
* @module bangumi
* @version 1.2.0
*/
const VERSION = '1.2.0';
const protocols = {
http: require('http'),
https: require('https'),
};
const querystring = require('querystring');
const utils = require('./utils');
const PromiseProvider = require('./promise_provider');
const nodeBangumiAppId = 'bgm2305af4de0f06a38'; //for debug
/**
* Represents the Bangumi API client.
* @class Bangumi
* @param {Object} [options] - Configuration options.
* @param {string} [options.app_id] - App ID used to identify the source (deprecated).
* @param {string} [options.access_token] - Access token used to identify the user.
* @param {string} [options.protocol] - HTTP protocol used for the API (e.g., 'http' or 'https').
* @param {string} [options.user_agent] - Custom user agent. See {@link https://github.com/bangumi/api/blob/master/docs-raw/user%20agent.md} for more information.
* @example
* const Bangumi = require('bangumi');
* const bgm = new Bangumi({
* access_token: "123456"
* });
*/
function Bangumi(options){
if (!(this instanceof Bangumi)) return new Bangumi(options);
const defaults = {
headers: {
'Accept': '*/*',
'Content-Type': 'application/x-www-form-urlencoded',
'Connection': 'close',
'User-Agent': 'markni/node-bangumi/' + VERSION + ' (https://github.com/markni/node-bangumi)'
},
callback_url: null,
rest_base: 'api.bgm.tv',
cookie_options: {},
cookie_secret: null
};
this.options = utils.merge(defaults, options);
if (this.options.user_agent) {
this.options.headers['User-Agent'] = this.options.user_agent;
}
}
Bangumi.VERSION = VERSION;
module.exports = Bangumi;
/**
* Sets a custom promise provider. By default, the Node.js ES6 promise provider is used.
* @param {function} provider - Custom promise provider.
* @example
* // Use Bluebird as the promise provider, so you can use features like .finally()
* bgm.setPromiseProvider(require('bluebird'));
*/
Bangumi.prototype.setPromiseProvider = function(provider) {
PromiseProvider.set(provider);
};
/**
* Updates the access token in case it's refreshed.
* @param {string} access_token - The new access token.
* @example
* bgm.setAccessToken('123456');
*/
Bangumi.prototype.setAccessToken = function(access_token) {
this.options['access_token'] = access_token;
};
Bangumi.prototype._get = function(url, params, callback) {
if (typeof params === 'function') {
callback = params;
params = {};
}
if ( typeof callback !== 'function' ) {
throw 'FAIL: INVALID CALLBACK.';
}
if (url.charAt(0) !== '/') {
throw 'FAIL: INVALID URL.';
}
if (Object.prototype.hasOwnProperty.call(this.options, 'app_id')) params = utils.merge({source:this.options.app_id}, params);
const protocolName = this.options.protocol || 'https';
const options = {
host: this.options.rest_base,
path: encodeURI(url + (querystring.stringify(params) ? '?' : '') + querystring.stringify(params)),
method: 'GET',
headers: this.options.headers
};
if (Object.prototype.hasOwnProperty.call(this.options, 'access_token')) options.headers['Authorization'] = 'Bearer ' + this.options.access_token;
const req = protocols[protocolName].request(options, function (res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function (chunk) {
data += chunk;
});
res.on('end', function () {
try {
const json = JSON.parse(data);
if (Object.prototype.hasOwnProperty.call(json, 'error')) callback(json, {});
else callback(null, json);
} catch (err) {
callback(err, {});
}
});
res.on('error', function (err) {
callback(err, {});
});
});
req.end();
};
/**
* A general GET request method for Bangumi API
* @param {string} url - Base GET request path
* @param {object} params - Parameters used for GET query string
* @param {function} [callback] - Optional callback function
* @returns {Promise|undefined} - Returns a promise if no callback is provided, otherwise undefined
*/
Bangumi.prototype.get = function(url, params, callback) {
const self = this;
return utils.promiseOrCallback(callback, function(cb){
self._get(url, params, cb);
});
};
Bangumi.prototype._post = function(url, params, callback) {
if (typeof params === 'function') {
callback = params;
params = {};
}
if ( typeof callback !== 'function' ) {
throw 'FAIL: INVALID CALLBACK.';
}
if (url.charAt(0) !== '/') {
throw 'FAIL: INVALID URL.';
}
const protocolName = this.options.protocol || 'https';
const post_data = querystring.stringify(params);
const headers = utils.merge({'Content-Length': post_data.length}, this.options.headers);
const options = {
host: this.options.rest_base,
path: encodeURI(url),
method: 'POST',
headers: headers
};
if (Object.prototype.hasOwnProperty.call(this.options, 'access_token')) options.headers['Authorization'] = 'Bearer ' + this.options.access_token;
if (Object.prototype.hasOwnProperty.call(this.options, 'app_id')) options.path += '?' + querystring.stringify({source: this.options.app_id});
const req = protocols[protocolName].request(options, function (res) {
let data = '';
res.on('data', function (chunk) {
data += chunk;
});
res.on('end', function () {
try {
const json = JSON.parse(data);
if (Object.prototype.hasOwnProperty.call(json, 'error')) callback(json, {});
else callback(null, json);
} catch (err) {
callback(err, {});
}
});
res.on('error', function (err) {
callback(err, {});
});
});
req.write(post_data);
req.end();
};
/**
* A general POST request method for Bangumi API
* @param {string} url - Base POST request path
* @param {object} params - Parameters used for POST body
* @param {function} [callback] - Optional callback function
* @returns {Promise|undefined} - Returns a promise if no callback is provided
*/
Bangumi.prototype.post = function(url, params, callback) {
const self = this;
return utils.promiseOrCallback(callback, function(cb){
self._post(url, params, cb);
});
};
/**
* A GET method that gets current weekly shows' seclude
* @param {function} [callback] - Optional callback function
* @returns {Promise|undefined} - Returns a promise if no callback is provided
*/
Bangumi.prototype.fetchCalendar = function(callback) {
const url = '/calendar';
return this.get(url, callback);
};
/**
* A GET method that gets target user's profile
* @param {string|number} username - Username or UID of the target user
* @param {function} [callback] - Optional callback function
* @returns {Promise|undefined} - Returns a promise if no callback is provided
*/
Bangumi.prototype.fetchUser = function(username, callback){
const url = '/user/' + username;
return this.get(url, callback);
};
/**
* @typedef {Object} SubjectEp
* @property {number} id - Episode ID
* @property {string} url - Episode URL
* @property {number} type - Episode type
* @property {number} sort - Episode sort order
* @property {string} name - Episode name
* @property {string} name_cn - Episode Chinese name
* @property {string} duration - Episode duration
* @property {string} airdate - Episode air date
* @property {number} comment - Number of comments
* @property {string} desc - Episode description
* @property {string} status - Episode status
*/
/**
* @typedef {Object} SubjectCrt
* @property {number} id - Character ID
* @property {string} url - Character URL
* @property {string} name - Character name
* @property {string} name_cn - Character Chinese name
* @property {string} role_name - Character role name
* @property {Object} images - Character images
* @property {number} comment - Number of comments
* @property {number} collects - Number of collections
* @property {Object} info - Additional character information
* @property {Array<Object>} actors - List of voice actors
*/
/**
* @typedef {Object} SubjectStaff
* @property {number} id - Staff ID
* @property {string} url - Staff URL
* @property {string} name - Staff name
* @property {string} name_cn - Staff Chinese name
* @property {string} role_name - Staff role name
* @property {Object} images - Staff images
* @property {number} comment - Number of comments
* @property {number} collects - Number of collections
* @property {Object} info - Additional staff information
* @property {Array<string>} jobs - List of jobs
*/
/**
* @typedef {Object} SubjectTopic
* @property {number} id - Topic ID
* @property {string} url - Topic URL
* @property {string} title - Topic title
* @property {number} main_id - Main subject ID
* @property {number} timestamp - Topic creation timestamp
* @property {number} lastpost - Last post timestamp
* @property {number} replies - Number of replies
* @property {Object} user - User who created the topic
*/
/**
* @typedef {Object} SubjectBlog
* @property {number} id - Blog ID
* @property {string} url - Blog URL
* @property {string} title - Blog title
* @property {string} summary - Blog summary
* @property {string} image - Blog image URL
* @property {number} replies - Number of replies
* @property {number} timestamp - Blog creation timestamp
* @property {string} dateline - Formatted date
* @property {Object} user - User who created the blog
*/
/**
* @typedef {Object} Subject
* @property {number} id - ID of the subject
* @property {string} url - URL of the subject page
* @property {number} type - Type of the subject
* @property {string} name - Name of the subject
* @property {string} name_cn - Chinese name of the subject
* @property {string} summary - Summary or description of the subject
* @property {number} eps - Number of episodes
* @property {number} eps_count - Count of episodes
* @property {string} air_date - Air date of the subject
* @property {number} air_weekday - Air weekday of the subject
* @property {Object} rating - Rating information
* @property {number} rating.total - Total number of ratings
* @property {Object} rating.count - Count of ratings for each score
* @property {number} rating.score - Average score
* @property {number} rank - Rank of the subject
* @property {Object} images - Image URLs for different sizes
* @property {string} images.large - URL for large image
* @property {string} images.common - URL for common image
* @property {string} images.medium - URL for medium image
* @property {string} images.small - URL for small image
* @property {string} images.grid - URL for grid image
* @property {Object} collection - Collection statistics
* @property {number} collection.wish - Number of users who wish to watch
* @property {number} collection.collect - Number of users who have collected
* @property {number} collection.doing - Number of users currently watching
* @property {number} collection.on_hold - Number of users who have put on hold
* @property {number} collection.dropped - Number of users who have dropped
* @property {Array<SubjectEp>} [eps] - Array of episode objects
* @property {Array<SubjectCrt>} [crt] - Array of character objects
* @property {Array<SubjectStaff>} [staff] - Array of staff objects
* @property {Array<SubjectTopic>} [topic] - Array of topic objects
* @property {Array<SubjectBlog>} [blog] - Array of blog objects
*/
/**
* A GET method that gets details of a subject by id
* @param {number} subject_id - ID of the target subject
* @param {Object} [params] - Optional parameters for the request
* @param {string} [params.responseGroup] - Accepts [small|medium|large]
* @param {function} [callback] - Optional callback function
* @returns {Promise<Subject>} - Returns a promise that resolves to the subject details
*/
Bangumi.prototype.fetchSubject = function(subject_id, params, callback) {
const url = '/subject/' + subject_id;
return this.get(url, params, callback);
};
/**
* A GET method that gets a list of episodes of a subject by subject id
* @param {number} subject_id - ID of the target subject
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @deprecated use fetchSubjectEps
*/
Bangumi.prototype.fetchEps = function(subject_id, callback){
return this.fetchSubjectEps(subject_id, callback);
};
/**
* A GET method that gets a list of episodes of a subject by subject id
* @param {number} subject_id - ID of the target subject
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.fetchSubjectEps(211567);
*/
Bangumi.prototype.fetchSubjectEps = function(subject_id, callback){
const url = '/subject/' + subject_id + '/ep';
return this.get(url, callback);
};
/**
* A GET method that gets details of user's collection by user id
* @param {string|number} username - Username or UID of the target user
* @param {object} params - Parameters for the request
* @param {string} params.cat - Type of the collections, accepts [watching|all_watching]
* @param {string} [params.responseGroup] - Type of the collections, accepts [medium|small]
* @param {string} [params.ids] - IDs of subjects the user wants to fetch, this is very poorly implemented at the moment
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.fetchUserCollection(1, {cat:'all_watching', responseGroup: 'small'}).then(console.log)
*/
Bangumi.prototype.fetchUserCollection = function(username, params, callback){
const url = '/user/' + username + '/collection';
return this.get(url, params, callback);
};
/**
* A GET method that gets overview of user's collection by username and subject type
* @param {string|number} username - Username or UID of the target user
* @param {string} subject_type - Type of subject
* @param {object} [params] - Optional parameters for the request
* @param {number} [params.max_results] - Upper limit of the amount of subjects the client can fetch (max 25)
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.fetchUserCollections('sai', 'real', {max_results: 1}).then(console.log).catch(console.error)
*/
Bangumi.prototype.fetchUserCollections = function(username, subject_type, params, callback){
//note here using our appid due to an api bug: https://github.com/bangumi/api/issues/24
if(typeof params === 'object' && !Object.prototype.hasOwnProperty.call(params, 'app_id')) params.app_id = nodeBangumiAppId;
if(typeof params === 'undefined') params = {app_id : nodeBangumiAppId};
const url = '/user/' + username + '/collections' + (subject_type ? '/' : '') + subject_type;
return this.get(url, params, callback);
};
/**
* A GET method that gets overview of user's collection statics by username
* @param {string|number} username - Username or UID of the target user
* @param {object} [params] - Optional parameters for the request
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.fetchUserCollectionsStatus('sai').then(console.log).catch(console.error)
*/
Bangumi.prototype.fetchUserCollectionsStatus = function(username, params, callback){
//note here using our appid due to an api bug: https://github.com/bangumi/api/issues/24
if(typeof params === 'object' && !Object.prototype.hasOwnProperty.call(params, 'app_id')) params.app_id = nodeBangumiAppId;
if(typeof params === 'undefined') params = {app_id : nodeBangumiAppId};
const url = '/user/' + username + '/collections/status';
return this.get(url, params, callback);
};
/**
* A GET method that searches and returns a list of subjects by keywords
* @param {string} keywords - Query string for search
* @param {object} [params] - Optional parameters for the request
* @param {string} [params.responseGroup] - Accepts [small|medium|large]
* @param {number} [params.type] - Accepts [1|2|3|4|6] (1.book 2.anime 3.music 4.game 5.live action)
* @param {number} [params.start] - Result start index, used for paging
* @param {number} [params.max_results] - The number of entries to return, max 25
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.search('天元突破');
*/
Bangumi.prototype.search = function(keywords, params, callback){
const url = '/search/subject/' + keywords;
return this.get(url, params, callback);
};
/**
* A GET method that get lists of episodes that user already watched, sorted by subject id
* @param {string|number} username - Username or UID of the target user
* @param {object} [params] - Optional parameters for the request
* @param {number} [params.subject_id] - The ID of the subject the user wants to fetch progress for
* @param {string} [params.auth] - Authentication string used for validation of user identity, deprecated
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.fetchProgress(1, {subject_id:899}).then(console.log);
*/
Bangumi.prototype.fetchProgress = function(username, params, callback){
const url = '/user/' + username + '/progress';
return this.get(url, params, callback);
};
/**
* A POST method that login user with username and password, and returns auth string
* @param {object} params - Parameters for the request
* @param {string} params.username - Username or UID of the target user
* @param {string} params.password - Password for the target user
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @deprecated since version 1.0.0, newer version of api will use oauth.
*/
Bangumi.prototype.auth = function(params, callback){
console.warn('auth() will be deprecated in the near future');
const url = '/auth';
return this.post(url, params, callback);
};
/**
* A GET method that get current user's collection details on specific subject by subject id
* @param {number} subject_id - ID of the target subject
* @param {object} [params] - Optional parameters for the request
* @param {string} [params.auth] - Authentication string used for validation of user identity, deprecated
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.fetchCollection(1);
*/
Bangumi.prototype.fetchCollection = function(subject_id, params, callback){
const url = '/collection/' + subject_id;
return this.get(url, params, callback);
};
/**
* A POST method that updates user's status on specific subject by subject id
* @param {number} subject_id - ID of the target subject
* @param {object} [params] - Optional parameters for the request
* @param {string} [params.status=wish] - Accepts [wish|collect|do|on_hold|dropped]
* @param {string} [params.comment] - Comment on the target subject status update
* @param {string} [params.tags] - Tags on the target subject status update, separated by comma
* @param {number} [params.rating] - Star rating on the target subject, accepts [0~10]
* @param {number} [params.privacy] - Privacy control of this action, accepts [0|1] (0.open, 1.private)
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.createCollection(211567, {status: 'do', rating: 10});
*/
Bangumi.prototype.createCollection = function(subject_id, params, callback){
const url = '/collection/' + subject_id + '/update';
return this.post(url, params, callback);
};
/**
* A POST method that updates user's status on specific episode by episode id
* @param {number} ep_id - ID of the target episode
* @param {string} status - Accepts [watched|queue|drop|remove]
* @param {object} [params] - Optional parameters for the request
* @param {string} [params.ep_id] - A list of episode IDs for batch processing, separated by comma
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.updateEpStatus(7036,'drop');
*/
Bangumi.prototype.updateEpStatus = function(ep_id, status, params, callback){
const url = '/ep/' + ep_id + '/status/' + status;
return this.post(url, params, callback);
};
/**
* A POST method that marks episode 1 to target number as watched
* @param {number} subject_id - ID of the target subject
* @param {object} params - Parameters for the request
* @param {string} params.watched_eps - The number of episodes the user currently has watched
* @param {string} [params.watched_vols] - The number of volumes the user currently has watched
* @param {function} [callback] - Optional callback function
* @returns {Promise} - Returns a promise if no callback is provided
* @example
* bgm.updateWatchedEps(18462, {watched_eps: 10, watched_vols: 2});
*/
Bangumi.prototype.updateWatchedEps = function(subject_id, params, callback){
const url = '/subject/' + subject_id + '/update/watched_eps';
return this.post(url, params, callback);
};