UNPKG

mediamonkeyserver

Version:
615 lines (488 loc) 15.6 kB
//@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;