plex-api
Version:
Simple wrapper for querying against HTTP API on the Plex Media Server
332 lines (280 loc) • 10.9 kB
JavaScript
var os = require('os');
var uuid = require('uuid');
var url = require('url');
var request = require('request');
var xml2js = require('xml2js');
var headers = require('plex-api-headers');
var extend = require('util')._extend;
var uri = require('./uri');
var PLEX_SERVER_PORT = 32400;
function PlexAPI(options, deprecatedPort) {
var opts = options || {};
var hostname = typeof options === 'string' ? options : options.hostname;
this.hostname = hostname;
this.port = deprecatedPort || opts.port || PLEX_SERVER_PORT;
this.https = opts.https;
this.requestOptions = opts.requestOptions || {};
this.timeout = opts.timeout;
this.username = opts.username;
this.password = opts.password;
this.managedUser = opts.managedUser;
this.authToken = opts.token;
this.authenticator = opts.authenticator || this._credentialsAuthenticator();
this.responseParser = opts.responseParser || this._defaultResponseParser;
this.options = opts.options || {};
this.options.identifier = this.options.identifier || uuid.v4();
this.options.product = this.options.product || 'Node.js App';
this.options.version = this.options.version || '1.0';
this.options.device = this.options.device || os.platform();
this.options.deviceName = this.options.deviceName || 'Node.js App';
this.options.platform = this.options.platform || 'Node.js';
this.options.platformVersion = this.options.platformVersion || process.version;
if (typeof this.hostname !== 'string') {
throw new TypeError('Invalid Plex Server hostname');
}
if (typeof deprecatedPort !== 'undefined') {
console.warn('PlexAPI constuctor port argument is deprecated, use an options object instead.');
}
this.serverUrl = hostname + ':' + this.port;
this._initializeAuthenticator();
}
PlexAPI.prototype.getHostname = function getHostname() {
return this.hostname;
};
PlexAPI.prototype.getPort = function getPort() {
return this.port;
};
PlexAPI.prototype.getIdentifier = function getIdentifier() {
return this.options.identifier;
};
PlexAPI.prototype.query = function query(options) {
if (typeof options === 'string') {
// Support old method of only supplying a single `url` parameter
options = { uri: options };
}
if (options.uri === undefined) {
throw new TypeError('Requires uri parameter');
}
options.method = 'GET';
options.parseResponse = true;
return this._request(options).then(uri.attach(options.uri));
};
PlexAPI.prototype.postQuery = function postQuery(options) {
if (typeof options === 'string') {
// Support old method of only supplying a single `url` parameter
options = { uri: options };
}
if (options.uri === undefined) {
throw new TypeError('Requires uri parameter');
}
options.method = 'POST';
options.parseResponse = true;
return this._request(options).then(uri.attach(url));
};
PlexAPI.prototype.putQuery = function putQuery(options) {
if (typeof options === 'string') {
// Support old method of only supplying a single `url` parameter
options = { uri: options };
}
if (options.uri === undefined) {
throw new TypeError('Requires uri parameter');
}
options.method = 'PUT';
options.parseResponse = true;
return this._request(options).then(uri.attach(url));
};
PlexAPI.prototype.deleteQuery = function deleteQuery(options) {
if (typeof options === 'string') {
// Support old method of only supplying a single `url` parameter
options = { uri: options };
}
if (options.uri === undefined) {
throw new TypeError('Requires uri parameter');
}
options.method = 'DELETE';
options.parseResponse = false;
return this._request(options);
};
PlexAPI.prototype.perform = function perform(options) {
if (typeof options === 'string') {
// Support old method of only supplying a single `url` parameter
options = { uri: options };
}
if (options.uri === undefined) {
throw new TypeError('Requires uri parameter');
}
options.method = 'GET';
options.parseResponse = false;
return this._request(options);
};
PlexAPI.prototype.find = function find(options, criterias) {
if (typeof options === 'string') {
// Support old method of only supplying a single `url` parameter
options = { uri: options };
}
if (options.uri === undefined) {
throw new TypeError('Requires uri parameter');
}
return this.query(options).then(function (result) {
var children =
result._children ||
result.MediaContainer.Server ||
result.MediaContainer.Directory ||
result.MediaContainer.Provider ||
result.MediaContainer.Metadata;
return filterChildrenByCriterias(children, criterias);
});
};
PlexAPI.prototype._request = function _request(options) {
var reqUrl = this._generateRelativeUrl(options.uri);
var method = options.method;
var timeout = this.timeout;
var parseResponse = options.parseResponse;
var extraHeaders = options.extraHeaders || {};
var self = this;
var requestHeaders = headers(
this,
extend(
{
Accept: 'application/json',
'X-Plex-Token': this.authToken,
'X-Plex-Username': this.username,
},
extraHeaders
)
);
var reqOpts = {
uri: url.parse(reqUrl),
encoding: null,
method: method || 'GET',
timeout: timeout,
gzip: true,
headers: requestHeaders,
...this.requestOptions,
};
return new Promise((resolve, reject) => {
request(reqOpts, function onResponse(err, response, body) {
if (err) {
return reject(err);
}
// 403 forbidden when managed user does not have sufficient permission
if (response.statusCode === 403) {
return reject(
new Error(
'Plex Server denied request due to lack of managed user permissions! ' +
'In case of a delete request, delete content mustbe allowed in plex-media-server options.'
)
);
}
// 401 unauthorized when authentication is required against the requested URL
if (response.statusCode === 401) {
if (self.authenticator === undefined) {
return reject(
new Error(
'Plex Server denied request, you must provide a way to authenticate! ' +
'Read more about plex-api authenticators on https://www.npmjs.com/package/plex-api#authenticators'
)
);
}
return resolve(
self._authenticate().then(function () {
return self._request(options);
})
);
}
if (response.statusCode < 200 || response.statusCode > 299) {
return reject(
new Error(
'Plex Server didnt respond with a valid 2xx status code, response code: ' + response.statusCode
)
);
}
// prevent holding an open http agent connection by pretending to consume data,
// releasing socket back to the agent connection pool: http://nodejs.org/api/http.html#http_agent_maxsockets
response.on('data', function onData() {});
return parseResponse ? resolve(self.responseParser(response, body)) : resolve();
});
});
};
PlexAPI.prototype._authenticate = function _authenticate() {
return new Promise((resolve, reject) => {
if (this.authToken) {
return reject(
new Error(
'Permission denied even after attempted authentication :( Wrong username and/or password maybe?'
)
);
}
this.authenticator.authenticate(this, (err, token) => {
if (err) {
return reject(new Error('Authentication failed, reason: ' + err.message));
}
this.authToken = token;
resolve();
});
});
};
PlexAPI.prototype._credentialsAuthenticator = function _credentialsAuthenticator() {
var credentials;
if (this.username && this.password) {
credentials = require('plex-api-credentials');
return credentials({
username: this.username,
password: this.password,
managedUser: this.managedUser,
});
}
return undefined;
};
PlexAPI.prototype._initializeAuthenticator = function _initializeAuthenticator() {
if (this.authenticator && typeof this.authenticator.initialize === 'function') {
this.authenticator.initialize(this);
}
};
PlexAPI.prototype._generateRelativeUrl = function _generateRelativeUrl(relativeUrl) {
return this._serverScheme() + this.serverUrl + relativeUrl;
};
PlexAPI.prototype._serverScheme = function _serverScheme() {
if (typeof this.https !== 'undefined') {
// If https is supplied by the user, always do what it says
return this.https ? 'https://' : 'http://';
}
// Otherwise, use https if it's on port 443, the standard https port.
return this.port === 443 ? 'https://' : 'http://';
};
PlexAPI.prototype._defaultResponseParser = function _defaultResponseParser(response, body) {
if (response.headers['content-type'] === 'application/json') {
return Promise.resolve(body.toString('utf8')).then(JSON.parse);
} else if (!response.headers['content-type']) {
return Promise.resolve(body.toString('utf8'));
} else if (response.headers['content-type'].indexOf('xml') > -1) {
return xmlToJSON(body.toString('utf8'), {
attrkey: 'attributes',
});
}
return Promise.resolve(body);
};
function xmlToJSON(str, options) {
return new Promise((resolve, reject) => {
xml2js.parseString(str, options, (err, jsonObj) => {
if (err) {
return reject(err);
}
resolve(jsonObj);
});
});
}
function filterChildrenByCriterias(children, criterias) {
var context = {
criterias: criterias || {},
};
return children.filter(criteriasMatchChild, context);
}
function criteriasMatchChild(child) {
var criterias = this.criterias;
return Object.keys(criterias).reduce(function matchCriteria(hasFoundMatch, currentRule) {
var regexToMatch = new RegExp(criterias[currentRule]);
return regexToMatch.test(child[currentRule]);
}, true);
}
module.exports = PlexAPI;