UNPKG

mediamonkeyserver

Version:
375 lines (296 loc) 10.2 kB
// ts-check 'use strict'; var Async = require('async'); var events = require('events'); var jstoxml = require('jstoxml'); var Uuid = require('uuid'); var Path = require('path'); var send = require('send'); var debugFactory = require('debug'); var debug = debugFactory('upnpserver:server'); var debugProfiling = debugFactory('upnpserver:profiling'); var debugRequest = debugFactory('upnpserver:request'); var logger = require('./logger'); var xmlFilters = require('./util/xmlFilters'); var Xmlns = require('./xmlns'); var ContentDirectoryService = require('./contentDirectoryService'); var ConnectionManagerService = require('./connectionManagerService'); var MediaReceiverRegistrarService = require('./mediaReceiverRegistrarService'); var DEFAULT_LANGUAGE = 'en'; class UpnpServer extends events.EventEmitter { constructor(port, _configuration, callback) { super(); var configuration = Object.assign({}, _configuration); this.configuration = configuration; var lang = configuration.lang || process.env.LANG || DEFAULT_LANGUAGE; var langPart = /^([a-z][a-z])/i.exec(lang); try { configuration.i18n = require('./i18n/' + langPart[1].toLowerCase()); } catch (e) { // if localization is not supported, trying to use english by default configuration.i18n = require('./i18n/' + DEFAULT_LANGUAGE); } if (configuration.logActivity) { var logDate = Date.now(); var ws = ' '.repeat(80); var stdout = process.stdout; this.logActivity = (message) => { var now = Date.now(); if (logDate + 250 > now) { return; } logDate = now; message = message.slice(0, 80); stdout.write(message); stdout.write(ws.substring(message.length)); stdout.write('\r'); }; } this.dlnaSupport = (configuration.dlnaSupport !== false); this.microsoftSupport = (configuration.microsoftSupport !== false); this.packageDescription = require('../package.json'); this.name = configuration.name || 'Node UPNP Server'; this.uuid = configuration.uuid || Uuid.v4(); if (this.uuid.indexOf('uuid:') !== 0) { this.uuid = 'uuid:' + this.uuid; } this.serverName = configuration.serverName; if (!this.serverName) { var ns = ['Node/' + process.versions.node, 'UPnP/1.0', 'UPnPServer/' + this.packageDescription.version]; if (this.dlnaSupport) { ns.push('DLNADOC/1.50'); } this.serverName = ns.join(' '); } this.port = port; // this.externalIp = this.GetIp(); // The machine can have multiple IPs ! (IPv4/IPv6/ ...) this.services = {}; this.type = 'urn:schemas-upnp-org:device:MediaServer:1'; if (!configuration.services) { configuration.services = [new ConnectionManagerService(configuration), new ContentDirectoryService(configuration)]; if (this.microsoftSupport && this.dlnaSupport) { configuration.services.push(new MediaReceiverRegistrarService( configuration)); } } Async.each(configuration.services, (service, callback) => { this.addService(service, callback); }, (error) => { if (error) { return callback(error); } callback(null, this); }); } /** * * @param {Repository[]} * repositories * @param {Function} * callback * @deprecated */ setRepositories(repositories, callback) { this.addRepositories(repositories, callback); } addRepositories(repositories, callback) { this.services['cds'].addRepositories(repositories, callback); } addService(service, callback) { service.initialize(this, (error) => { if (error) { return callback(error); } this.services[service.route] = service; this.emit('newService', service); callback(null, service); }); } toJXML(request, callback) { var localhost = request.myHostname; var localport = request.socket.localPort; var serviceList = []; for (var route in this.services) { var service = this.services[route]; serviceList.push(service.serviceToJXml()); } var xml = { _name: 'root', _attrs: { xmlns: Xmlns.UPNP_DEVICE, // attempt to make windows media player to "recognise this device" }, _content: { specVersion: { major: 1, minor: 0 }, device: { deviceType: 'urn:schemas-upnp-org:device:MediaServer:1', friendlyName: this.name, manufacturer: 'Ventis Media, Inc.', //this.packageDescription.author, // LS: author contained HTML tag <name> which caused that this server was not seen by WMP manufacturerURL: 'https://mediamonkey.com/', modelDescription: 'UPnP sync capable server written in nodejs', modelName: 'MediaMonkey sync capable server', // LS: modelName is used for detecting "sync capability" in MM5 modelURL: 'https://mediamonkey.com/syncserver', modelNumber: this.packageDescription.version, serialNumber: '1.0', UDN: this.uuid, presentationURL: 'http://' + localhost + ':' + localport + '/index.html', iconList: [{ _name: 'icon', _content: { mimetype: 'image/png', width: 48, height: 48, depth: 24, url: '/icons/icon_48.png' } }, { _name: 'icon', _content: { mimetype: 'image/png', width: 120, height: 120, depth: 24, url: '/icons/icon_120.png' } }], serviceList: serviceList } } }; if (this.microsoftSupport) { // attempt to make windows media player to "recognise this device" /* // LS: commented out, not needed for the WMP support, the reason for not recognizing was that <manufacturer> had <name> as subnode xml._attrs["xmlns:pnpx"] = Xmlns.MICROSOFT_WINDOWS_PNPX; xml._attrs["xmlns:df"] = Xmlns.MICROSFT_DEVICE_FOUNDATION; xml._content.device["pnpx:X_deviceCategory"] = "MediaDevices"; xml._content.device["df:X_deviceCategory"] = "Multimedia"; xml._content.device.modelName = "Windows Media Connect compatible (" + xml._content.device.modelName + ")"; */ } if (this.dlnaSupport) { xml._attrs['xmlns:dlna'] = Xmlns.DLNA_DEVICE; xml._content.device['dlna:X_DLNACAP'] = ''; xml._content.device['dlna:X_DLNADOC'] = 'DMS-1.50'; // ??? xml._content.device["dlna:X_DLNADOC"] = "M-DMS-1.50"; } if (this.secDlnaSupport) { // see https://github.com/nmaier/simpleDLNA/blob/master/server/Resources/description.xml xml._attrs['xmlns:sec'] = Xmlns.SEC_DLNA; xml._content.device['sec:ProductCap'] = 'smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec'; xml._content.device['sec:X_ProductCap'] = 'smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec'; } return callback(null, xml); } processRequest(request, response, path, callback) { var now; if (debugProfiling.enabled) { now = Date.now(); } var localhost = request.socket.localAddress; if (localhost === '::1') { // We transform IPv6 local host to IPv4 local host localhost = '127.0.0.1'; } else { var ip6 = /::ffff:(.*)+/.exec(localhost); if (ip6) { localhost = ip6[1]; // Transform IPv6 IP address to IPv4 } } request.myHostname = localhost; response.setHeader('Server', this.serverName); // Replace any // by /, split and remove first empty segment var reg = /\/?([^/]+)?(\/.*)?/.exec(path); if (!reg) { return callback('Invalid path (' + path + ')'); } var segment = reg[1]; var action = reg[2] && reg[2].slice(1); if (debugRequest.enabled) { debugRequest('Request=', path, 'from=', request.connection.remoteAddress, 'segment=', segment, 'action=', action); } if (!segment) segment = ''; switch (segment) { case '': response.redirect('/web'); return; case 'index.html': debugRequest('Index request'); response.writeHead(200, { 'Content-Type': 'text/html' }); var body = '<html><head><title>' + this.name + '</title></head><body><h1>' + this.name + '</h1></body></html>'; response.end(body, 'utf-8', (error) => { if (error) { logger.error(error); } }); return; // Server Web UI case 'web': if (!action) action = 'index.html'; response.sendFile(Path.join(__dirname, '..', 'build-webui', action)); return; case 'description.xml': debugRequest('Description request'); this.toJXML(request, (error, xmlObject) => { if (error) { logger.error(error); return callback(error); } var xml = jstoxml.toXML(xmlObject, { header: true, indent: ' ', filter: xmlFilters }); debug('Descript Path request: returns:', xml); // logger.verbose("Request description path: " + xml); response.writeHead(200, { 'Content-Type': 'text/xml; charset="utf-8"' }); response.end(xml, 'UTF-8'); callback(null, true); }); return; case 'icons': var iconPath = action.replace(/\.\./g, '').replace(/\\/g, '').replace( /\//g, ''); debugRequest('Icons request path=', iconPath); var dir = __dirname; dir = dir.substring(0, dir.lastIndexOf(Path.sep)); iconPath = dir + ('/icon/' + iconPath).replace(/\//g, Path.sep); debug('Send icon', iconPath); send(request, iconPath).pipe(response); return callback(null, true); } if (this.dlnaSupport) { // Thanks to smolleyes for theses lines response.setHeader('transferMode.dlna.org', 'Streaming'); response.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000'); } var service = this.services[segment]; if (service) { service.processRequest(request, response, action, (error, found) => { if (error) { return callback(error); } if (debugProfiling.enabled) { debugProfiling('Profiling ' + (Date.now() - now) + 'ms'); } callback(null, found); }); return; } callback(null, false); } } module.exports = UpnpServer;