mediamonkeyserver
Version:
MediaMonkey Server
615 lines (488 loc) • 15.6 kB
JavaScript
//@ts-check
'use strict';
const assert = require('assert');
const events = require('events');
const http = require('http');
const SSDP = require('node-ssdp');
const url = require('url');
const util = require('util');
const express = require('express');
const bodyParser = require('body-parser');
const restRouter = require('./lib/api/rest');
const clients = require('./lib/clients');
const os = require('os');
const debug = require('debug')('upnpserver:api');
const logger = require('./lib/logger');
const UPNPServer = require('./lib/upnpServer');
const Repository = require('./lib/repositories/repository');
const Configuration = require('./lib/configuration');
class API extends events.EventEmitter {
/**
* upnpserver API.
*
* @param {object} configuration
* @param {array} paths
*
* @constructor
*/
constructor(configuration, paths) {
super();
this.configuration = Object.assign({}, this.defaultConfiguration, configuration);
this.repositories = [];
this._upnpClasses = {};
this._contentHandlers = [];
this._contentProviders = {};
this._contentHandlersKey = 0;
if (typeof (paths) === 'string') {
this.addDirectory('/', paths);
} else if (util.isArray(paths)) {
paths.forEach((path) => this.initPaths(path));
}
if (this.configuration.noDefaultConfig !== true) {
// @ts-ignore
this.loadConfiguration(require('./default-config.json'));
}
var cf = this.configuration.configurationFiles;
if (typeof (cf) === 'string') {
var toks = cf.split(',');
toks.forEach((tok) => this.loadConfiguration(require(tok)));
} else if (util.isArray(cf)) {
cf.forEach((c) => this.loadConfiguration(require(c)));
}
}
/**
* Default server configuration.
*
* @type {object}
*/
get defaultConfiguration() {
return {
'dlnaSupport': true,
'httpPort': 10222,
'name': 'Node Server',
// @ts-ignore
'version': require('./package.json').version
};
}
/**
* Initialize paths.
*
* @param {string|object} path
* @returns {Repository} the created repository
*/
initPaths(path) {
if (typeof (path) === 'string') {
return this.addDirectory('/', path);
}
if (typeof (path) === 'object') {
if (path.type === 'video') {
path.type = 'movie';
}
return this.declareRepository(path);
}
throw new Error('Invalid path \'' + util.inspect(path) + '\'');
}
/**
* Declare a repository
*
* @param {object} configuration
* @returns {Repository} the new repository
*/
declareRepository(configuration) {
var config = Object.assign({}, configuration);
var mountPath = config.mountPoint || config.mountPath || '/';
var type = config.type;
if (!type) {
logger.error('Type is not specified, assume it is a \'directory\' type');
type = 'directory';
}
var requirePath = configuration.require;
if (!requirePath) {
requirePath = './lib/repositories/' + type;
}
debug('declareRepository', 'requirePath=', requirePath, 'mountPath=', mountPath, 'config=', config);
var clazz = require(requirePath);
if (!clazz) {
logger.error('Class of repository must be specified');
return;
}
var repository = new clazz(mountPath, config);
return this.addRepository(repository);
}
/**
* Add a repository.
*
* @param {Repository} repository
*
* @returns {Repository} a Repository object
*/
addRepository(repository) {
assert(repository instanceof Repository, 'Invalid repository parameter \'' + repository + '\'');
this.repositories.push(repository);
return repository;
}
/**
* Add simple directory.
*
* @param {string} mountPath
* @param {string} path
* @param {Object} [configuration]
* @returns {Repository} a Repository object
*/
addDirectory(mountPath, path, configuration) {
assert.equal(typeof (mountPath), 'string', 'Invalid mountPoint parameter \'' +
mountPath + '\'');
assert.equal(typeof (path), 'string', 'Invalid path parameter \'' + path + '\'');
configuration = Object.assign({}, configuration, { mountPath, path, type: 'directory' });
return this.declareRepository(configuration);
}
/**
* Add music directory.
*
* @param {string} mountPath
* @param {string} path
*
* @returns {Repository} a Repository object
*/
addMusicDirectory(mountPath, path, configuration) {
assert.equal(typeof mountPath, 'string', 'Invalid mountPath parameter \'' +
mountPath + '\'');
assert.equal(typeof path, 'string', 'Invalid path parameter \'' + mountPath + '\'');
configuration = Object.assign({}, configuration, { mountPath, path, type: 'music' });
return this.declareRepository(configuration);
}
/**
* Add video directory.
*
* @param {string} mountPath
* @param {string} path
*
* @returns {Repository} a Repository object
*/
addVideoDirectory(mountPath, path, configuration) {
assert.equal(typeof mountPath, 'string', 'Invalid mountPoint parameter \'' + mountPath + '\'');
assert.equal(typeof path, 'string', 'Invalid path parameter \'' + path + '\'');
configuration = Object.assign({}, configuration, { mountPath, path, type: 'movie' });
return this.declareRepository(configuration);
}
/**
* Add history directory.
*
* @param {string} mountPath
*
* @returns {Repository} a Repository object
*/
addHistoryDirectory(mountPath, configuration) {
assert.equal(typeof mountPath, 'string', 'Invalid mountPoint parameter \'' + mountPath + '\'');
configuration = Object.assign({}, configuration, { mountPath, type: 'history' });
return this.declareRepository(configuration);
}
/**
* Add iceCast.
*
* @param {string} mountPath
* @param {object} configuration
*
* @returns {Repository} a Repository object
*/
addIceCast(mountPath, configuration) {
assert.equal(typeof mountPath, 'string', 'Invalid mountPoint parameter \'' +
mountPath + '\'');
configuration = Object.assign({}, configuration, { mountPath, type: 'iceCast' });
return this.declareRepository(configuration);
}
/**
* Load a JSON configuration
*
* @param {object} config - JSON read from file
*/
loadConfiguration(config) {
var upnpClasses = config.upnpClasses;
if (upnpClasses) {
for (var upnpClassName in upnpClasses) {
// var p = upnpClasses[upnpClassName];
// var clazz = require(p);
var clazz = require(`./lib/class/${upnpClassName}`);
this._upnpClasses[upnpClassName] = new clazz();
}
}
var contentHandlers = config.contentHandlers;
if (contentHandlers instanceof Array) {
contentHandlers.forEach((contentHandler) => {
var mimeTypes = contentHandler.mimeTypes || [];
if (contentHandler.mimeType) {
mimeTypes = mimeTypes.slice(0);
mimeTypes.push(contentHandler.mimeType);
}
/*var requirePath = contentHandler.require;
if (!requirePath) {
requirePath = './lib/contentHandlers/' + contentHandler.type;
}
if (!requirePath) {
logger.error('Require path is not defined !');
return;
}
var clazz = require(requirePath);*/
var clazz = require(`./lib/contentHandlers/${contentHandler.type}`);
if (!clazz) {
logger.error('Class of contentHandler must be specified');
return;
}
var configuration = contentHandler.configuration || {};
var ch = new clazz(configuration, mimeTypes);
ch.priority = contentHandler.priority || 0;
ch.mimeTypes = mimeTypes;
this._contentHandlers.push(ch);
});
}
var contentProviders = config.contentProviders;
if (contentProviders instanceof Array) {
contentProviders.forEach((contentProvider) => {
var protocol = contentProvider.protocol;
if (!protocol) {
logger.error('Protocol property must be defined for contentProvider ' + contentProvider.id + '\'.');
return;
}
if (protocol in this._contentProviders) {
logger.error('Protocol \'' + protocol + '\' is already known');
return;
}
var name = contentProvider.name || protocol;
/*var requirePath = contentProvider.require;
if (!requirePath) {
var type = contentProvider.type || protocol;
requirePath = './lib/contentProviders/' + type;
}
if (!requirePath) {
logger.error('Require path is not defined !');
return;
}
var clazz = require(requirePath);*/
var clazz = require(`./lib/contentProviders/${contentProvider.type || protocol}`);
if (!clazz) {
logger.error('Class of contentHandler must be specified');
return;
}
var configuration = Object.assign({}, contentProvider);
var ch = new clazz(configuration, protocol);
ch.protocol = protocol;
ch.name = name;
this._contentProviders[protocol] = ch;
});
}
var repositories = config.repositories;
if (repositories) {
repositories.forEach((configuration) => this.declareRepository(configuration));
}
}
/**
* Start server.
*/
start() {
this.stop(() => {
this.startServer();
});
}
/**
* Start server callback.
*
* @return {UPNPServer}
*/
startServer(callback) {
callback = callback || (() => {
});
debug('startServer', 'Start the server');
var configuration = this.configuration;
configuration.repositories = this.repositories;
configuration.upnpClasses = this._upnpClasses;
configuration.contentHandlers = this._contentHandlers;
configuration.contentProviders = this._contentProviders;
if (!callback) {
callback = (error) => {
if (error) {
logger.error(error);
}
};
}
var upnpServer = new UPNPServer(configuration.httpPort, configuration, (error, upnpServer) => {
if (error) {
logger.error('Can not start UPNPServer', error);
return callback(error);
}
debug('startServer', 'Server started ...');
this._upnpServerStarted(upnpServer, callback);
});
return upnpServer;
}
/**
* After the server start.
*
* @param {object} upnpServer
*/
_upnpServerStarted(upnpServer, callback) {
this.emit('starting');
this.upnpServer = upnpServer;
var config = {
udn: this.upnpServer.uuid,
location: { port: this.configuration.httpPort, path: '/description.xml' },
sourcePort: 1900, // is needed for SSDP multicast to work correctly (issue #75 of node-ssdp)
explicitSocketBind: true, // might be needed for multiple NICs (issue #34 of node-ssdp)
ssdpSig: 'Node/' + process.versions.node + ' UPnP/1.0 ' + 'UPnPServer/' +
// @ts-ignore
require('./package.json').version
};
debug('_upnpServerStarted', 'New SSDP server config=', config);
// @ts-ignore
var ssdpServer = new SSDP.Server(config);
this.ssdpServer = ssdpServer;
ssdpServer.addUSN('upnp:rootdevice');
ssdpServer.addUSN(upnpServer.type);
var services = upnpServer.services;
if (services) {
for (var route in services) {
ssdpServer.addUSN(services[route].type);
}
}
debug('_upnpServerStarted', 'New Http server port=', upnpServer.port);
var app = express();
var httpServer = http.createServer(app);
clients.initialize(httpServer);
this.httpServer = httpServer;
app.use('/api', bodyParser.urlencoded({ extended: true }));
app.use('/api', bodyParser.json());
app.use('/', (req, res, next) => {
logger.verbose('HTTP ' + req.method + ' ' + req.url);
next();
});
app.use('/api', restRouter);
app.use((req, res) => {
this._processRequest(req, res);
// Don't call next(), as we don't expect any further processing
// TODO: Rewrite our _processRequest() to be fully handled by express server?
});
httpServer.on('error', (err) => {
// @ts-ignore
if (err.code == 'EADDRINUSE') {
// @ts-ignore
logger.error('Could not start server, port ' + err.port + ' is already in use !!!');
logger.error('Probably another instance of this server is already running?');
} else
logger.error('httpServer error: ' + err);
});
var getLocalIP = function () {
var ifaces = os.networkInterfaces();
var res = null;
Object.keys(ifaces).forEach(function (ifname) {
ifaces[ifname].forEach(function (iface) {
if ('IPv4' !== iface.family || iface.internal !== false) {
// skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
return;
}
if (!res || ifname === 'Ethernet' || ifname === 'eth0') // Empty, or preferred interfaces
res = iface.address;
});
});
return res;
};
httpServer.listen(upnpServer.port, (error) => {
if (error) {
return callback(error);
}
this.ssdpServer.start();
this.emit('waiting');
var address = httpServer.address();
debug('_upnpServerStarted', 'Http server is listening on address=', address);
logger.info('==================================================');
logger.info(`Running at http://${getLocalIP()}:${address.port} (or http://localhost:${address.port})`);
logger.info('Connect using a web browser or using MediaMonkey 5.');
logger.info('==================================================');
this._initUIConfigObserver(callback);
});
}
_initUIConfigObserver(callback) {
var config = Configuration.getBasicConfig();
this.upnpServer.name = config.serverName;
var observer = Configuration.getConfigObserver();
observer.on('change', () => {
this.upnpServer.name = config.serverName;
});
callback();
}
/**
* Process request
*
* @param {object} request
* @param {object} response
*/
_processRequest(request, response) {
var path = url.parse(request.url).pathname;
// logger.debug("Uri=" + request.url);
var now = Date.now();
try {
this.upnpServer.processRequest(request, response, path, (error, processed) => {
var stats = {
request: request,
response: response,
path: path,
processTime: Date.now() - now,
};
if (error) {
response.writeHead(500, 'Server error: ' + error);
response.end();
this.emit('code:500', error, stats);
return;
}
if (!processed) {
response.writeHead(404, 'Resource not found: ' + path);
response.end();
this.emit('code:404', stats);
return;
}
this.emit('code:200', stats);
});
} catch (error) {
logger.error('Process request exception', error);
this.emit('error', error);
}
}
/**
* Stop server.
*
* @param {function|undefined} callback
*/
stop(callback) {
debug('stop', 'Stopping ...');
callback = callback || (() => {
return false;
});
var httpServer = this.httpServer;
var ssdpServer = this.ssdpServer;
var stopped = false;
if (this.ssdpServer) {
this.ssdpServer = undefined;
stopped = true;
try {
debug('stop', 'Stop ssdp server ...');
ssdpServer.stop();
} catch (error) {
logger.error(error);
}
}
if (httpServer) {
this.httpServer = undefined;
stopped = true;
try {
debug('stop', 'Stop http server ...');
httpServer.close();
} catch (error) {
logger.error(error);
}
}
debug('stop', 'Stopped');
if (stopped) {
this.emit('stopped');
}
callback(null, stopped);
}
}
module.exports = API;