mediamonkeyserver
Version:
MediaMonkey Server
1,785 lines (1,462 loc) • 63.4 kB
JavaScript
// ts-check
'use strict';
const Util = require('util');
const assert = require('assert');
const rangeParser = require('range-parser');
const os = require('os');
const fs = require('fs');
const Configuration = require('./configuration');
const MediaProvider = require('./mediaProvider');
const debugFactory = require('debug');
const debug = debugFactory('upnpserver:contentDirectoryService');
const debugDIDL = debugFactory('upnpserver:contentDirectoryService:didl');
const debugStack = debugFactory('upnpserver:stack');
const debugMetas = debugFactory('upnpserver:contentDirectoryService:metas');
const Async = require('async');
const send = require('send');
const logger = require('./logger');
const Service = require('./service');
const Xmlns = require('./xmlns');
var Node;
const jstoxml = require('./util/jstoxml');
const NodeWeakHashmap = require('./util/nodeWeakHashmap');
const URL = require('./util/url');
const xmlFilters = require('./util/xmlFilters');
const UpnpItem = require('./class/object.item');
const UpnpContainer = require('./class/object.container');
const FilterSearchEngine = require('./filterSearchEngine');
const AlphaNormalizer = require('./util/alphaNormalizer');
const PathNormalizer = require('./util/pathNormalizer');
const PROTOCOL_SPLITTER = /^([A-Z0-9_-]+):(.*)/i;
class ContentDirectoryService extends Service {
constructor(configuration) {
Node = require('./node');
super({
serviceType: 'urn:schemas-upnp-org:service:ContentDirectory:1',
serviceId: 'urn:upnp-org:serviceId:ContentDirectory',
route: 'cds'
});
this.addAction('Browse', [{
name: 'ObjectID',
type: 'A_ARG_TYPE_ObjectID'
}, {
name: 'BrowseFlag',
type: 'A_ARG_TYPE_BrowseFlag'
}, {
name: 'Filter',
type: 'A_ARG_TYPE_Filter'
}, {
name: 'StartingIndex',
type: 'A_ARG_TYPE_Index'
}, {
name: 'RequestedCount',
type: 'A_ARG_TYPE_Count'
}, {
name: 'SortCriteria',
type: 'A_ARG_TYPE_SortCriteria'
}], [{
name: 'Result',
type: 'A_ARG_TYPE_Result'
}, {
name: 'NumberReturned',
type: 'A_ARG_TYPE_Count'
}, {
name: 'TotalMatches',
type: 'A_ARG_TYPE_Count'
}, {
name: 'UpdateID',
type: 'A_ARG_TYPE_UpdateID'
}]);
this.addAction('GetSortCapabilities', [], [{
name: 'SortCaps',
type: 'SortCapabilities'
}]);
this.addAction('GetSystemUpdateID', [], [{
name: 'Id',
type: 'SystemUpdateID'
}]);
this.addAction('GetSearchCapabilities', [], [{
name: 'SearchCaps',
type: 'SearchCapabilities'
}]);
this.addAction('Search', [{
name: 'ContainerID',
type: 'A_ARG_TYPE_ObjectID'
}, {
name: 'SearchCriteria',
type: 'A_ARG_TYPE_SearchCriteria'
}, {
name: 'Filter',
type: 'A_ARG_TYPE_Filter'
}, {
name: 'StartingIndex',
type: 'A_ARG_TYPE_Index'
}, {
name: 'RequestedCount',
type: 'A_ARG_TYPE_Count'
}, {
name: 'SortCriteria',
type: 'A_ARG_TYPE_SortCriteria'
}], [{
name: 'Result',
type: 'A_ARG_TYPE_Result'
}, {
name: 'NumberReturned',
type: 'A_ARG_TYPE_Count'
}, {
name: 'TotalMatches',
type: 'A_ARG_TYPE_Count'
}, {
name: 'UpdateID',
type: 'A_ARG_TYPE_UpdateID'
}]);
// addType (name, type, value, valueList, ns, evented,
// moderation_rate, additionalProps, preEventCb, postEventCb)
this.addType('A_ARG_TYPE_BrowseFlag', 'string', '', ['BrowseMetadata',
'BrowseDirectChildren'
]);
this.addType('ContainerUpdateIDs', 'string', 0, [], null, true, 2, [],
() => { // concatenate ContainerUpdateIDs before event
var updateIds = this.updateIds;
this.updateIds = {};
var concat = [];
for (var container in updateIds) {
var updateId = updateIds[container];
if (!updateId) {
continue;
}
concat.push(container, updateId);
}
this.stateVars['ContainerUpdateIDs'].value = concat.join(',');
}, () => { // clean ContainerUpdateIDs after event
this.stateVars['ContainerUpdateIDs'].value = '';
});
this.addType('SystemUpdateID', 'ui4', 0, [], {
dt: Xmlns.MICROSOFT_DATATYPES
}, true, 2);
this.addType('A_ARG_TYPE_Count', 'ui4', 0);
this.addType('A_ARG_TYPE_SortCriteria', 'string', '');
this.addType('A_ARG_TYPE_SearchCriteria', 'string', '');
this.addType('SortCapabilities', 'string', ['dc:title', 'upnp:genre',
'upnp:artist', 'upnp:author', 'upnp:album', 'upnp:rating', 'upnp:originalTrackNumber', 'upnp:originalDiscNumber'
].join(','), [], {
upnp: Xmlns.UPNP_METADATA,
dc: Xmlns.PURL_ELEMENT
});
this.addType('A_ARG_TYPE_Index', 'ui4', 0);
this.addType('A_ARG_TYPE_ObjectID', 'string');
this.addType('A_ARG_TYPE_UpdateID', 'ui4', 0);
this.addType('A_ARG_TYPE_Result', 'string');
this._searchableFields = ['dc:title', 'dc:creator', 'upnp:genre', 'upnp:artist', 'upnp:albumArtist', 'upnp:author', 'upnp:album', 'upnp:actor', 'upnp:director', 'upnp:producer', 'upnp:publisher'];
this.addType('SearchCapabilities', 'string', this._searchableFields.join(','), [], {
upnp: Xmlns.UPNP_METADATA,
dc: Xmlns.PURL_ELEMENT
});
this.addType('A_ARG_TYPE_Filter', 'string');
this.jasminFileMetadatasSupport = (configuration.jasminFileMetadatasSupport !== false);
this.jasminMusicMetadatasSupport = (configuration.jasminMusicMetadatasSupport !== false);
this.jasminMovieMetadatasSupport = (configuration.jasminMovieMetadatasSupport !== false);
this.dlnaSupport = (configuration.dlnaSupport !== false);
this.microsoftSupport = (configuration.microsoftSupport !== false);
this.secDlnaSupport = (configuration.secDlnaSupport !== false);
this._childrenWeakHashmap = new NodeWeakHashmap('childrenList', 5000, true);
this._childrenByTitleWeakHashmap = new NodeWeakHashmap('childrenListByTitle', 5000, true);
this.repositories = [];
// this.systemUpdateId = 0;
this._previousSystemUpdateId = -1;
this.updateIds = {};
this.contentPath = '/' + this.route + '/content/';
this.upnpClasses = configuration.upnpClasses;
this.contentHandlers = configuration.contentHandlers;
this.contentProviders = configuration.contentProviders;
this.contentHandlersByName = {};
this._contentProvidersByProtocol = {};
this.upnpClassesByMimeType = {};
_setupContentHandlerMimeTypes(this.upnpClassesByMimeType, this.upnpClasses, false);
}
/**
*
*/
initialize(upnpServer, callback) {
super.initialize(upnpServer, (error) => {
if (error) {
return callback(error);
}
var contentProvidersByProtocol = this._contentProvidersByProtocol;
Async.eachSeries(this.contentProviders, (contentProvider, callback) => {
var protocol = contentProvider.protocol;
debug('initialize', 'Initialize contentProvider', contentProvider.name, 'for protocol', protocol);
contentProvidersByProtocol[protocol.toLowerCase()] = contentProvider;
// debug("Protocol=",contentProvider.protocol,"platform=",os.platform());
if (protocol === 'file' && os.platform() === 'win32') {
for (var i = 0; i < 26; i++) {
// Map all drives letter
contentProvidersByProtocol[String.fromCharCode(97 + i)] = contentProvider;
}
}
contentProvider.initialize(this, callback);
}, (error) => {
if (error) {
logger.error('Initialize content handlers error', error);
return callback(error);
}
Async.eachSeries(this.contentHandlers, (contentHandler, callback) => {
debug('initialize', 'Initialize contentHandler', contentHandler.name, 'for mimeTypes', contentHandler.mimeTypes);
this.contentHandlersByName[contentHandler.name] = contentHandler;
contentHandler.initialize(this, callback);
}, (error) => {
if (error) {
logger.error('Initialize content handlers error', error);
return callback(error);
}
this._installRoot((error /*, root*/ ) => {
if (error) {
return callback(error);
}
var repositories = upnpServer.configuration.repositories;
this.addRepositories(repositories, (error) => {
if (error) {
return callback(error);
}
// Kept here for Intel upnp toolkit, but not in upnp spec
if (upnpServer.configuration.enableIntelToolkitSupport) {
this._intervalTimer = setInterval(() => this._sendItemChangesEvent(), 1500);
}
this._initConfiguredCollections(() => {
callback(null, this);
});
});
return;
});
});
});
});
}
_initConfiguredCollections(callback) {
Configuration.setRegistry(this.nodeRegistry, () => {
var config = Configuration.getBasicConfig();
var observer = Configuration.getConfigObserver();
observer.on('collectionchange', (operation, collection, collections) => {
switch (operation) {
case 'added':
this._rescanCollection(collection, {});
break;
case 'changed':
this._garbageNonCollectionFiles(collections, () => {
this._rescanCollection(collection, {});
});
break;
case 'removed':
this._garbageNonCollectionFiles(collections);
break;
}
});
this._initCollections(config.collections, callback);
});
}
_initCollections(collections, callback) {
this._rescanCollections(collections, {
useCachedContent: true // indicates that the scan will read cached content from DB (not the real content from HDD)
});
// and add predefined 'Playlists' collection for playlists browsing:
this._getRepositoryForCollection({
name: 'Playlists',
type: 'playlist'
}, callback);
}
_rescanCollections(collections, scan_options, callback) {
var list = collections || [];
Async.eachSeries(list, (itm, cbk) => {
this._rescanCollection(itm, scan_options, cbk);
}, callback);
}
_garbageNonCollectionFiles(collections, callback) {
var folders = [];
for (var col of collections) {
for (var fld of col.folders)
folders.push(fld);
}
this.nodeRegistry.garbageFilesOutOfFolders(folders, (err, files) => {
Async.eachSeries(files, (f, cbk) => {
var node_id = this.nodeRegistry._getNodeIdForFile(f);
this.getNodeById(node_id, (err, node) => {
if (!err && node)
node.remove(true, cbk);
else
cbk();
});
}, callback);
});
}
_getRepositoryForCollection(collection, callback) {
var repository;
this.repositories.forEach((item) => {
if (item.collectionID == collection.id)
repository = item;
});
if (repository) {
callback(repository);
} else { // LS: repository for this collection doesn't exist yet, create one:
var rep_type = collection.type;
if (collection.type == 'movies')
rep_type = 'movie';
if (collection.type == 'classical')
rep_type = 'music';
var clazz = require('./repositories/' + rep_type);
if (!clazz) {
logger.error('Class of repository ' + rep_type + 'not found');
return;
}
var defaultPath = os.homedir() + collection.name;
if (collection.folders && collection.folders.length)
defaultPath = PathNormalizer.normalize(collection.folders[0]);
var mountPath = collection.name;
repository = new clazz(mountPath, {
mountPoint: mountPath,
path: defaultPath,
type: rep_type
});
repository.runInitialScan = false;
this.addRepository(repository, (err, repository) => {
repository.originalPath = defaultPath || repository._directoryURL.path;
repository.collectionID = collection.id;
callback(repository);
});
}
}
_rescanCollection(collection, scan_options, callback) {
this._getRepositoryForCollection(collection, (repository) => {
var list = collection.folders || [];
repository.useCachedContent = (scan_options && scan_options.useCachedContent);
Async.eachSeries(list, (path, cbk) => {
this.rescanPath(path, repository, cbk);
}, () => {
repository.useCachedContent = false;
if (callback)
callback();
});
});
}
/**
*
*/
_installRoot(callback) {
if (this.root) {
return callback(null, this.root);
}
this.initializeRegistry((error) => {
if (error) {
logger.error('Can not initialize registry ' + error.message, error);
return callback(error);
}
this._nodeRegistry.getNodeById(0, (error, node) => {
if (error) {
return callback(error);
}
if (node) {
debug('_installRoot', 'Set root to #', node.id);
this.root = node;
return callback(null, node);
}
var i18n = this.upnpServer.configuration.i18n;
this.createNode('root', UpnpContainer.UPNP_CLASS, {
searchable: false,
restricted: true,
title: i18n.ROOT_NAME,
metadatas: [{
name: 'upnp:writeStatus',
content: 'NOT_WRITABLE'
}]
}, (node) => {
node._path = '/';
node._id = 0; // Force id to 0
node._parentId = -1;
}, (error, node) => {
if (error) {
return callback(error);
}
this.root = node;
callback(null, node);
});
});
});
}
/**
*
*/
addRepositories(repositories, callback) {
if (!repositories || !repositories.length) {
return callback();
}
repositories = repositories.slice(0); // clone
repositories.sort((r1, r2) => r1.mountPath.length - r2.mountPath.length);
debug('addRepositories', 'Adding', repositories.length, 'repositories');
Async.eachSeries(repositories, (repository, callback) => {
debug('addRepositories', 'Adding repository', repository.mountPath);
this.addRepository(repository, callback);
}, callback);
}
/**
*
*/
initializeRegistry(callback) {
var configuration = this.upnpServer.configuration;
var nodeRegistryName = configuration.registryDb || 'sql';
var NodeRegistryClass = require('./db/' + nodeRegistryName + 'Registry');
this._nodeRegistry = new NodeRegistryClass(configuration);
MediaProvider.setRegistry(this.nodeRegistry);
this._nodeRegistry.initialize(this, (error) => {
if (error) {
return callback(error);
}
callback(null);
});
}
/**
*
*/
addRepository(repository, callback) {
var hashKey = JSON.stringify(repository.hashKey);
debug('addRepository', 'Add repository', hashKey);
this._nodeRegistry.registerRepository(repository, hashKey, (error, repository) => {
if (error) {
logger.error('Can not register repository', error);
return callback(error);
}
this._installRoot((error /*, root*/ ) => {
if (error) {
logger.error('Can not install root', error);
return callback(error);
}
debug('addRepository', 'Initialize repository', repository);
repository.initialize(this, (error) => {
if (error) {
return callback(error);
}
this.repositories.push(repository);
callback(null, repository);
});
});
});
}
/**
*
*/
processSoap_Search(xml, request, response, callback) {
// Browse support Search parameter !
this.processSoap_Browse(xml, request, response, callback);
}
/**
*
*/
_newDidlJxml() {
var attrs = {
['xmlns']: Xmlns.DIDL_LITE,
['xmlns:dc']: Xmlns.PURL_ELEMENT,
['xmlns:upnp']: Xmlns.UPNP_METADATA
};
if (this.dlnaSupport) {
attrs['xmlns:dlna'] = Xmlns.DLNA_METADATA;
}
if (this.secDlnaSupport) {
attrs['xmlns:sec'] = Xmlns.SEC_DLNA;
}
if (this.jasminFileMetadatasSupport) {
attrs['xmlns:fm'] = Xmlns.JASMIN_FILEMETADATA;
}
if (this.jasminMusicMetadatasSupport) {
attrs['xmlns:mm'] = Xmlns.JASMIN_MUSICMETADATA;
}
if (this.jasminMovieMetadatasSupport) {
attrs['xmlns:mo'] = Xmlns.JASMIN_MOVIEMETADATA;
}
var xmlDidl = {
_name: 'DIDL-Lite',
_attrs: attrs
};
return xmlDidl;
}
/**
*
*/
_newRepositoryRequest(request) {
var localhost = request.myHostname;
var localport = request.socket.localPort;
var repositoryRequest = {
contentURL: 'http://' + localhost + ':' + localport + this.contentPath,
request: request,
contentDirectoryService: this,
microsoftSupport: this.microsoftSupport,
dlnaSupport: this.dlnaSupport,
secDlnaSupport: this.secDlnaSupport,
jasminFileMetadatasSupport: this.jasminFileMetadatasSupport,
jasminMusicMetadatasSupport: this.jasminMusicMetadatasSupport,
jasminMovieMetadatasSupport: this.jasminMovieMetadatasSupport
};
return repositoryRequest;
}
/**
*
*/
responseSearch(response, request,
containerId, filterCallback, startingIndex, requestedCount, sortCriteria,
searchCriteria, callback) {
if (debug.enabled) {
debug('responseSearch', 'Request containerId=' + containerId + ' filterCallback=' + !!filterCallback + ' startingIndex=' + startingIndex +
' requestedCount=' + requestedCount + ' sortCriteria=' + sortCriteria +
' searchCallback=' + !!searchCriteria);
}
var added_names = {};
this.getNodeById(containerId, (error, item) => {
if (error) {
logger.error('CDS: Can not getNodeById for id', containerId);
return callback(501, error);
}
if (!item) {
return callback(710, 'CDS: Browser Can not find item ' +
containerId);
}
this.emit('Search', request, item);
var processList = (list, node) => {
debug('responseSearch', 'Emit filterList');
this.emit('filterList', request, node, list);
var lxml = [];
var xmlDidl = this._newDidlJxml();
var repositoryRequest = this._newRepositoryRequest(request);
Async.eachSeries(list, (child, callback) => {
if (!child) {
logger.warn('ALERT not a node ', child);
return callback(null, list);
}
this._getNodeJXML(child, null, repositoryRequest,
filterCallback, (error, itemJXML) => {
if (error) {
return callback(error);
}
lxml.push(itemJXML);
setImmediate(callback);
});
}, (error) => {
if (error) {
return callback(501, error);
}
debug('responseSearch', 'Get all nodes', lxml);
sortCriteria = sortCriteria || node.attributes.defaultSort || node.upnpClass.defaultSort;
if (sortCriteria) {
_applySortCriteria(lxml, sortCriteria);
}
debug('responseSearch', 'SortCriteria=', sortCriteria);
var total = lxml.length;
if (startingIndex > 0) {
if (startingIndex > lxml.length) {
lxml = [];
} else {
lxml = lxml.slice(startingIndex);
}
}
if (requestedCount > 0) {
lxml = lxml.slice(0, requestedCount);
}
if (filterCallback) {
lxml.forEach((x) => filterCallback(x));
}
xmlDidl._content = lxml;
var didl = jstoxml.toXML(xmlDidl, {
header: false,
indent: '',
filter: xmlFilters
});
debugDIDL('responseSearch', 'SearchContainer didl=', didl);
this.responseSoap(response, 'Search', {
_name: 'u:SearchResponse',
_attrs: {
'xmlns:u': this.type
},
_content: {
Result: didl,
NumberReturned: lxml.length,
TotalMatches: total,
UpdateID: (node.id) ? node.updateId : this.stateVars['SystemUpdateID'].get()
}
}, (error) => {
if (error) {
return callback(501, error);
}
debug('responseSearch', 'Search end #' + containerId);
callback(null);
});
});
};
var search = this.parseSearchCriteria(searchCriteria.val);
this.doFullTextSearch(search, () => {
var filter = (node) => {
var res;
if (node.upnpClass && node.upnpClass.name && search.classAccepted(node.upnpClass.name) && search.fieldsAccepted(node))
res = true;
if (res && node.isUpnpContainer) {
if (added_names[node.name])
res = false; // to eliminate duplicates of albums (same album nodes are under artists too)
added_names[node.name] = true;
}
return res;
};
if (item.refId) {
this.getNodeById(item.refId, (error, refItem) => {
if (error) {
logger.error('CDS: Can not getNodeById for REF id',
item.refId);
return callback(701, error);
}
if (!refItem) {
return callback(701, 'CDS: Browser Can not find REF item ' +
item.refId);
}
refItem.filterChildNodes(filter, (error, list) => {
if (error) {
logger.warn('Can not scan repositories: ', error);
return callback(710, error);
}
return processList(list, item);
});
});
return;
}
debug('Browser node #', item.id, 'error=', error);
item.filterChildNodes(filter, (error, list) => {
debug('responseSearch', 'Browser node #', item.id, 'filtred error=', error);
if (error) {
logger.warn('Can not scan repositories: ', error);
return callback(710, error);
}
return processList(list, item);
});
});
});
}
_extractBetween(from, to, value) {
// extracts text between two strings an returns array of all occurences
var re = new RegExp('(?:' + from + ')(.*?)(?:' + to + ')', 'gi'); //set ig flag for global search and case insensitive
var ar = value.match(re);
if (ar) {
for (var i = 0; i < ar.length; i++)
ar[i] = ar[i].substring(from.length, ar[i].length - to.length).trim();
}
return ar;
}
_newSearchField(field, operator, value) {
return {
field: field,
operator: operator,
value: value
};
}
_getCountOfChar(char, str) {
var result = 0;
for (var i = 0; i < str.length; i++)
if (str[i] == char)
result++;
return result;
}
_extractSearchField(field, operator, value) {
var from = field + ' ' + operator + ' "';
var to = '"';
var terms = this._extractBetween(from, to, value);
if (terms) {
// this field and operator is presented, use it and get further info:
var sf = this._newSearchField(field, operator, terms[terms.length - 1]);
// check brackets (nest level) and concatenation operator:
var whole = from + sf.value + to;
var idx = value.indexOf(whole);
var pre = value.substring(0, idx);
sf.bracketNestLevel = this._getCountOfChar('(', pre) - this._getCountOfChar(')', pre);
var concatOperator = value.substring(idx + whole.length, idx + whole.length + 4).trim().toLowerCase(); // ' or ', ' and'
if (concatOperator == 'or' || concatOperator == 'and')
sf.concatOperator = concatOperator;
else
sf.concatOperator = 'or';
return sf;
}
}
_parseSearchField(field_id, value, fields) {
var f_c = this._extractSearchField(field_id, 'contains', value);
if (f_c)
fields.push(f_c);
var f_e = this._extractSearchField(field_id, '=', value);
if (f_e)
fields.push(f_e);
}
parseSearchCriteria(value) {
// value examples in case of MMA are
// '(upnp:class derivedfrom "object.item.audioItem" or upnp:class derivedfrom "object.item.videoItem ") and (dc:title contains "hh" or dc:creator contains "hh" or upnp:artist contains "hh" or upnp:albumArtist contains "hh" or upnp:album contains "hh" or upnp:author contains "hh" or upnp:genre contains "hh" )'
// '(upnp:class = "object.container.album.musicAlbum") and (dc:title contains "ggg" or dc:creator contains "ggg" or upnp:artist contains "ggg" or upnp:genre contains "ggg" )'
// value examples for Roku Soundbridge:
// '(upnp:class derivedfrom="object.item.audioItem" and @refIDexists = false and upnp:artist="U2")'
// '(upnp:class = "object.container.person.musicArtist" and upnp:genre = "Jazz" and @refID exists false)'
// '(upnp:class derivedfrom "object.item.audioItem" and @refID exists false and upnp:album = "The Joshua Tree (U2)")'
var res = {};
res.classes = this._extractBetween('upnp:class = "', '"', value) || [];
res.derived_classes = this._extractBetween('upnp:class derivedfrom "', '"', value) || [];
res.classAccepted = function (clsName) {
for (var cls of this.classes) {
if (clsName == cls)
return true;
}
for (var clsd of this.derived_classes) {
if (clsName.startsWith(clsd))
return true;
}
};
res.fields = [];
for (var field_id of this._searchableFields)
this._parseSearchField(field_id, value, res.fields);
res.fieldsAccepted = function (node) {
var title = node.name;
if (node.attributes.title)
title = node.attributes.title;
if (node.attributes.db_id && this.hashed_db_ids[node.attributes.db_id]) // db_id hashed in doFullTextSearch bellow
return true;
for (var f of this.fields) {
if (f.field == 'dc:title' && f.operator == 'contains' && title.indexOf(f.value) > 0)
return true;
if (f.field == 'dc:title' && f.operator == '=' && f.value == title)
return true;
}
};
return res;
}
doFullTextSearch(search, callback) {
// compose full text search phrase from fields:
var phrase = '';
var lastField;
for (var f of search.fields) {
var db_field = f.field.substring(f.field.indexOf(':') + 1); // strip 'dc:' and 'upnp":' prefixes and map upnp fields to db fields
if (db_field == 'albumArtist')
db_field = 'album_artist';
if (db_field == 'author' || db_field == 'creator')
db_field = 'composer';
if (phrase != '')
phrase = phrase + ' ' + f.concatOperator.toUpperCase() + ' ';
if (lastField && f.bracketNestLevel > lastField.bracketNestLevel)
for (var i = lastField.bracketNestLevel; i < f.bracketNestLevel; i++)
phrase = phrase + '(';
var value = this._nodeRegistry.validateFTS( f.value);
phrase = phrase + db_field + ': ' + value;
if (f.operator == 'contains')
phrase = phrase + '*';
if (lastField && f.bracketNestLevel < lastField.bracketNestLevel)
for (var i2 = lastField.bracketNestLevel; i2 < f.bracketNestLevel; i2++)
phrase = phrase + ')';
lastField = f;
}
this._nodeRegistry.getFilesBy({
searchPhrase: phrase
}, (err, files) => {
search.hashed_db_ids = {};
for (var f of files)
search.hashed_db_ids[f.db_id] = true;
callback();
});
}
/**
*
*/
processSoap_Browse(xml, request, response, callback) {
var childNamed = (name, xmlns) => Service._childNamed(xml, name, xmlns);
var browseFlag = null;
var node = childNamed('BrowseFlag', Xmlns.UPNP_SERVICE);
if (node) {
browseFlag = node.val;
}
var searchCriteria = childNamed('SearchCriteria', Xmlns.UPNP_SERVICE);
var filterNode = childNamed('Filter', Xmlns.UPNP_SERVICE);
var filterSearchEngine = new FilterSearchEngine(this, filterNode, searchCriteria);
var objectId = this.root.id;
node = childNamed('ObjectID', Xmlns.UPNP_SERVICE);
if (node) {
objectId = this._nodeRegistry.keyFromString(node.val);
}
debug('processSoap_Browse', 'Browse starting (flags=', browseFlag, ') of item #', objectId);
var startingIndex = -1;
node = childNamed('StartingIndex', Xmlns.UPNP_SERVICE);
if (node) {
startingIndex = parseInt(node.val, 10);
}
var requestedCount = -1;
node = childNamed('RequestedCount', Xmlns.UPNP_SERVICE);
if (node) {
requestedCount = parseInt(node.val, 10);
}
var sortCriteria = null;
node = childNamed('SortCriteria', Xmlns.UPNP_SERVICE);
if (node) {
sortCriteria = node.val;
}
debug('processSoap_Browse', 'Browse sortCriteria=', sortCriteria, 'browseFlag=', browseFlag,
'requestedCount=', requestedCount, 'objectId=', objectId,
'startingIndex=', startingIndex);
if (browseFlag === 'BrowseMetadata') {
this.processBrowseMetadata(response, request, objectId,
filterSearchEngine, callback);
return;
}
if (browseFlag === 'BrowseDirectChildren') {
this.processBrowseDirectChildren(response, request, objectId,
filterSearchEngine, startingIndex, requestedCount, sortCriteria, !!searchCriteria, callback);
return;
}
if (searchCriteria) {
this.responseSearch(response, request, objectId, filterSearchEngine.func, startingIndex, requestedCount, sortCriteria, searchCriteria, callback);
return;
}
var error = new Error('Unknown browseFlag \'' + browseFlag + '\'');
callback(error);
}
/**
*
*/
processBrowseMetadata(response, request, objectId, filterSearchEngine, callback) {
debug('processBrowseMetadata', 'Browse objectId=', objectId);
// logger.info("Request ObjectId=" + objectId);
this.getNodeById(objectId, (error, node) => {
if (error) {
return callback(701, error);
}
if (!node) {
return callback(701, 'CDS: BrowseObject Can not find node ' +
objectId);
}
debug('processBrowseMetadata', 'BrowseObject node=#', node.id, ' error=', error);
this.emit('BrowseMetadata', request, node);
var repositoryRequest = this._newRepositoryRequest(request);
var produceDidl = (node, nodeXML) => {
var xmlDidl = this._newDidlJxml();
xmlDidl._content = nodeXML;
var didl = jstoxml.toXML(xmlDidl, {
header: false,
indent: ' ',
filter: xmlFilters
});
if (debugDIDL.enabled) {
debugDIDL('processBrowseMetadata', 'BrowseObject didl=', didl);
}
this.responseSoap(response, 'Browse', {
_name: 'u:BrowseResponse',
_attrs: {
'xmlns:u': this.type
},
_content: {
Result: didl,
NumberReturned: 1,
TotalMatches: 1,
UpdateID: (node.id) ? node.updateId : this.stateVars['SystemUpdateID']
.get()
}
}, (code, error) => {
if (error) {
return callback(code, error);
}
debug('processBrowseDirectChildren', 'Browse end #', node.id);
// logger.debug("CDS: Browse end " + containerId);
callback(null);
});
};
filterSearchEngine.start(node);
this._getNodeJXML(node, null, repositoryRequest,
filterSearchEngine.func, (error, nodeJXML) => {
if (error) {
return callback(500, error);
}
nodeJXML = filterSearchEngine.end(nodeJXML);
produceDidl(node, nodeJXML, callback);
});
});
}
/**
*
*/
_getNodeJXML(node, inheritedAttributes, repositoryRequest, filterCallback, callback) {
debug('_getNodeJXML of #', node.id, 'upnpClass=', node.upnpClass);
var refId = node.refId;
if (refId) {
node.resolveLink((error, refNode) => {
if (error) {
return callback(error);
}
var linkAttributes = node.attributes;
this._getNodeJXML(refNode, linkAttributes, repositoryRequest,
filterCallback, (error, refNodeJXML) => {
if (error) {
return callback(error);
}
refNodeJXML._attrs.id = node.id;
refNodeJXML._attrs.refID = refNode.id;
refNodeJXML._attrs.parentID = node.parentId;
return callback(null, refNodeJXML);
});
});
return;
}
var itemClass = node.upnpClass;
assert(itemClass, 'ItemClass is not defined for node');
var attributes = node.attributes || {};
if (inheritedAttributes) {
attributes = Object.assign({}, attributes, inheritedAttributes);
// console.log("Merged attribute of #" + node.id + " ", attributes, "from=", node.attributes, "inherit=",
// inheritedAttributes);
}
itemClass.toJXML(node, attributes, repositoryRequest, filterCallback, (error, itemJXML) => {
if (error) {
return callback(error);
}
this._emitToJXML(node, attributes, repositoryRequest,
filterCallback, itemJXML, (error) => callback(error, itemJXML));
});
}
/**
*
*/
processBrowseDirectChildren(response, request, containerId, filterSearchEngine, startingIndex,
requestedCount, sortCriteria, searchMode, callback) {
debug('processBrowseDirectChildren', 'Request containerId=', containerId, 'startingIndex=', startingIndex,
'requestedCount=', requestedCount, 'sortCriteria=', sortCriteria);
this.getNodeById(containerId, (error, node) => {
if (error) {
logger.error('CDS: Can not getNodeById for id #', containerId);
return callback(501, error);
}
if (!node) {
return callback(710, 'CDS: Browser Can not find node #' + containerId);
}
this.emit('BrowseDirectChildren', request, node);
var processList = (list, node) => {
this.emit('filterList', request, node, list);
var lxml = [];
var xmlDidl = this._newDidlJxml();
var repositoryRequest = this._newRepositoryRequest(request);
Async.eachSeries(list, (child, callback) => {
if (!child) {
logger.warn('ALERT not a node ', child);
return callback(null, list);
}
filterSearchEngine.start(child);
this._getNodeJXML(child, null, repositoryRequest,
filterSearchEngine.func, (error, nodeJXML) => {
if (error) {
return callback(error);
}
nodeJXML = filterSearchEngine.end(nodeJXML);
if (nodeJXML) {
lxml.push(nodeJXML);
}
setImmediate(callback);
});
}, (error) => {
if (error) {
return callback(501, error);
}
sortCriteria = sortCriteria || (node.attributes && node.attributes.defaultSort) || node.upnpClass.defaultSort;
if (sortCriteria) {
_applySortCriteria(lxml, sortCriteria);
}
var total = lxml.length;
if (startingIndex > 0) {
if (startingIndex > lxml.length) {
lxml = [];
} else {
lxml = lxml.slice(startingIndex);
}
}
if (requestedCount > 0) {
lxml = lxml.slice(0, requestedCount);
}
xmlDidl._content = lxml;
var didl = jstoxml.toXML(xmlDidl, {
header: false,
indent: '',
filter: xmlFilters
});
if (debugDIDL.enabled) {
debugDIDL('processBrowseDirectChildren', 'BrowseContainer didl=', didl);
}
this.responseSoap(response,
(searchMode) ? 'Search' : 'Browse', {
_name: (searchMode) ? 'u:SearchResponse' : 'u:BrowseResponse',
_attrs: {
'xmlns:u': this.type
},
_content: {
Result: didl,
NumberReturned: lxml.length,
TotalMatches: total,
UpdateID: (node.id) ? node.updateId : this.stateVars['SystemUpdateID']
.get()
}
}, (error) => {
if (error) {
return callback(501, error);
}
debug('processBrowseDirectChildren', 'Browse end #', containerId);
callback(null);
});
});
};
if (node.refId) {
this.getNodeById(node.refId, (error, refNode) => {
if (error) {
logger.error('CDS: Can not getNodeById for REF id',
node.refId);
return callback(701, error);
}
if (!refNode) {
return callback(701, 'CDS: Browser Can not find REF node ' +
node.refId);
}
refNode.listChildren((error, list) => {
if (error) {
logger.warn('Can not scan repositories: ', error);
return callback(501, error);
}
processList(list, refNode);
});
});
return;
}
debug('Browser node #', node.id, 'error=', error);
node.browseChildren({
request: request
}, (error, list) => {
if (error) {
logger.error('Can not scan repositories: ', error);
return callback(710, error);
}
debug('List children =>', list.length, 'nodes');
processList(list, node);
});
});
}
/**
*
*/
browseNode(node, options, callback) {
if (arguments.length === 2) {
callback = options;
options = undefined;
}
options = options || {};
var path = node.path;
debug('browseNode nodeID=#', node.id, 'path=', path, 'repositories.count=', this.repositories.length);
var list = [];
this.asyncEmit('browse', list, node, options, (error) => {
if (error) {
logger.error('CDS: browseNode path=\'' + path + '\' returns error', error);
return callback(error);
}
debug('browseNode #', node.id, 'path=', path, 'returns=', list.length, 'elements.');
callback(null, list);
});
}
/**
*
*/
createNodeRef(targetNode, name, initCallback, callback) {
assert(targetNode instanceof Node, 'Invalid targetNode parameter');
assert(name === undefined || name === null || typeof (name) === 'string', 'Invalid name parameter');
assert(initCallback === undefined || initCallback === null || typeof (initCallback) === 'function',
'Invalid initCallback parameter');
assert(typeof (callback) === 'function', 'Invalid callback parameter');
if (name === targetNode.name) {
// no need to have a name if the referenced has the same !
name = undefined;
}
Node.createRef(targetNode, name, (error, node) => {
if (error) {
return callback(error);
}
if (initCallback) {
initCallback(node);
}
this.registerNode(node, callback);
});
}
/**
*
*/
createNode(name, upnpClass, attributes, initCallback, callback) {
// assert(!attributes, "Invalid attributes parameter"); // It can be undefined ! (link)
assert(upnpClass, 'Invalid upnpClass parameter');
assert(initCallback === undefined || initCallback === null || typeof (initCallback) === 'function',
'Invalid initCallback parameter');
assert(typeof (callback) === 'function', 'Invalid callback parameter');
if (typeof (upnpClass) === 'string') {
var uc = this.upnpClasses[upnpClass];
assert(uc, 'Item class is not defined for ' + upnpClass);
upnpClass = uc;
}
assert(upnpClass instanceof UpnpItem, 'Upnpclass must be an item (name=' +
name + ' upnpClass=' + upnpClass + ')');
Node.create(this, name, upnpClass, attributes, (error, node) => {
if (error) {
return callback(error);
}
if (initCallback) {
initCallback(node);
}
this.registerNode(node, callback);
});
}
/**
*
*/
newNodeRef(parent, targetNode, name, initCallback, before, callback) {
this.createNodeRef(targetNode, name, initCallback, (error, node) => {
if (error) {
debug('newNodeRef: createNodeRef error=', error);
return callback(error);
}
parent.insertBefore(node, before, (error) => {
if (error) {
debug('newNodeRef: insertBefore error=', error);
return callback(error);
}
return callback(null, node, node.id);
});
});
}
/**
*
*/
newNode(parentNode, name, upnpClass, attributes, initCallback, before, callback) {
assert(parentNode instanceof Node, 'Invalid parentNode parameter');
assert(typeof (name) === 'string', 'Invalid name parameter');
assert(typeof (callback) === 'function', 'Invalid callback parameter');
attributes = attributes || {};
upnpClass = upnpClass || UpnpItem.UPNP_CLASS;
this.createNode(name, upnpClass, attributes, initCallback, (error, node) => {
if (error) {
logger.error('Can not create node name=', name, 'error=', error);
return callback(error);
}
parentNode.insertBefore(node, before, (error) => {
if (error) {
logger.error('Append child error #', node.id, 'error=', error);
return callback(error);
}
callback(null, node, node.id);
});
});
}
/**
*
*/
registerUpdate(node) {
// Very expensive, this function is called very very often
this.updateIds[node.id] = node.updateId;
this.stateVars['SystemUpdateID'].set(this.stateVars['SystemUpdateID'].get() + 1);
this.stateVars['ContainerUpdateIDs'].moderate();
}
/**
*
*/
registerNode(node, callback) {
this._nodeRegistry.registerNode(node, (error) => {
if (error) {
return callback(error);
}
this.asyncEmit('newNode', node, () => callback(null, node));
});
}
/**
*
*/
saveNode(node, modifiedProperties, callback) {
var upnpServer = this.upnpServer;
if (upnpServer.logActivity && node.contentURL) {
upnpServer.logActivity('Processed ' + node.contentURL.path);
}
this._nodeRegistry.saveNode(node, modifiedProperties, (error) => {
if (error) {
return callback(error);
}
this.asyncEmit('saveNode', node, modifiedProperties, callback);
});
}
/**
*
*/
getNodeById(id, options, callback) {
if (arguments.length === 2) {
callback = options;
options = null;
}
this._nodeRegistry.getNodeById(id, callback);
}
/**
*
*/
allocateNodeId(node, callback) {
this._nodeRegistry.allocateNodeId(node, callback);
}
/**
*
*/
unregisterNode(node, callback) {
assert(node instanceof Node, 'Invalid node parameter');
assert(typeof (callback) === 'function', 'Invalid callback parameter');
this.asyncEmit('deleteNode', node, (error) => {
if (error) {
return callback(error);
}
this._nodeRegistry.unregisterNode(node, callback);
});
}
/**
*
*/
processRequest(request, response, path, callback) {
this._lastRequestDate = Date.now();
request.contentDirectoryService = this;
var reg = /([^/]+)(\/.*)?/.exec(path);
if (!reg) {
return callback('Invalid path (' + path + ')');
}
var segment = reg[1];
var action = reg[2] && reg[2].slice(1);
switch (segment) {
case 'content':
var parameters = action.split('/');
var nid = parameters.shift();
var id = this._nodeRegistry.keyFromString(nid);
debug('processRequest: Request node=', id, 'requestId=', nid, 'parameters=', parameters, 'request=', path);
this.getNodeById(id, (error, node) => {
if (error) {
logger.error('processRequest: GetNodeById id=', id, ' throws error=', error);
return callback(error);
}
if (!node || !node.id) {
logger.error('Send content of node=#', id, 'not found');
this.emit('request-error', request, id);
response.writeHead(404, 'Node #' + id + ' not found');
response.end();
return callback(null, true);
}
node.resolveLink((error, nodeRef) => {
if (error) {
logger.error('processRequest: ResolveLink error node=', id, 'error=', error);
return callback(error);
}
this.emit('request', request, nodeRef, node, parameters);
if (request.method == 'DELETE')
this.processNodeDeletion(nodeRef, request, response, parameters, callback);
else
this.processNodeContent(nodeRef, request, response, path, parameters, callback);
});
});
return;
case 'desc':
id = 0;
if (action) {
parameters = action.split('/');
nid = parameters.shift();
id = this._nodeRegistry.keyFromString(nid);
}
this.getNodeById(id, (error, node) => {
if (error) {
logger.error('/tree get node #', id, 'returns error', error);
return callback(error);
}
if (!node) {
return callback('Node not found !');
}
var string = JSON.stringify(node.attributes, null, 2);
response.setHeader('Content-Type', 'application/json; charset="utf-8"');
response.end(string, 'UTF8');
callback(null, true);
});
return;
case 'tree':
id = 0;
if (action) {
parameters = action.split('/');
nid = parameters.shift();
id = this._nodeRegistry.keyFromString(nid);
}
this.getNodeById(id, (error, node) => {
if (error) {
logger.error('/tree get node #', id, 'returns error', error);
return callback(error);
}
if (!node) {
return callback('Node not found !');
}
node.browseChildren({
request: request
}, (error /*, children*/ ) => {
if (error) {
logger.error('/tree list children of #', id, 'returns error', error);
return callback(error);
}
node.treeString((error, string) => {
if (error) {
logger.error('/tree treeString() returns error', error);
return callback(error);
}
response.setHeader('Content-Type', 'text/plain; charset="utf-8"');
response.end(string, 'UTF8');
callback(null, true);
});
});
});
return;
}
super.processRequest(request, response, path, callback);
}
getRepositoryForPath(path) {
var repository = this.repositories[0];
this.repositories.forEach((item) => {
if (path.indexOf(item.mountPath) == 0) // LS: like '/Video' within '/Video/My movies/movie.mp4', TODO: do this rather based on track type
repository = item;
});
return repository;
}
rescanPath(path, repository, callback) {
path = PathNormalizer.normalize(PathNormalizer.removeLastSlash(path));
var dt = Date.now();
repository._directoryURL = this.newURL(path);
repository.scan(this, repository._mountPathNode, (error) => {
if (error) {
logger.info('Scanning of ' + path + ' has failed: ' + error);
} else {
var s = Math.floor((Date.now() - dt) / 1000);
logger.info(`Scan of repository ${path} has been finished in ${s} second${(s > 1) ? 's' : ''}`);
}
repository._directoryURL = this.newURL(repository.originalPath);
if (callback)
callback(error);
});
}
scanFile(path, repository, callback) {
path = PathNormalizer.normalize(path);
var dt = Date.now();
var infos = {};
infos.contentURL = this.newURL(path);
repository.processFile(repository._mountPathNode, infos, (error) => {
if (error) {
logger.error('Scanning of ' + path + ' has failed: ' + error);
} else {
var s = Math.floor((Date.now() - dt) / 1000);
logger.debug(`Scan of file ${path} has been finished in ${s} second${(s > 1) ? 's' : ''}`);
}
if (callback)
callback(error);
});
}
processNodeDeletion(node, request, response, parameters, callback) {
if (!node.isUpnpContainer) {
// this is a file deletion
var contentURL = node.contentURL;
if (!contentURL) {
logger.error('Resource not found for node #', node.id);
response.writeHead(404, 'Resource not found for node #' + node.id);
response.end();
return callback(null, true);
}
var path = contentURL.path;
fs.unlink(path, (err) => {
if (!err) {
this.nodeRegistry.deleteMetas(node.attributes);
node.remove(true, () => {
response.writeHead(200, 'OK');
response.end();
callback(null, true);
});
} else {
response.writeHead(423, 'Locked');
response.end(err);
callback(err, true);
}
});
} else {
// this is a container deletion (like playlist)
var _failure = function (error_description) {
logger.error(error_description);
response.writeHead(400, 'Bad Request');
response.end(error_description);
callback(null, true);
};
if (node.upnpClass.name == 'object.container.playlistContainer') {
this.getNodeById(node.parentId, (err, parent) => {
if (!err && parent) {
this.nodeRegistry.deletePlaylist({
guid: node.guid,
parent_guid: parent.guid
}, (err) => {
if (!err) {
node.remove(false, () => {
response.writeHead(200, 'OK');
response.end();
callback(null, true);
});
} else {
_failure(err);
}
});
} else {
_failure(err);
}
});
} else {
_failure('Deletion not supported for class ' + node.upnpClass.name);
}
}
}
/**
*
*/
processNodeContent(node, request, response, path, parameters, callback) {
var contentHandlerName = parameters[0];
if (contentHandlerName !== undefined) {
var contentHandler = this.contentHandlersByName[contentHandlerName];
debug('Process request: contentHandler key=', contentHandlerName); // , " handler=",contentHandler);
if (!contentHandler) {
logger.error('Content handler not found: ' + contentHandlerName + ' for node #' + node.id);
response.writeHead(404, 'Content handler not found: ' + contentHandlerName);
response.end();
return callback(null, true);
}
parameters.shift();
contentHandler.processRequest(node, request, response, path, parameters, callback);
return;
}
var contentURL = node.contentURL;
if (!contentURL) {
logger.error('Resource not found for node #', node.id);
response.writeHead(404, 'Resource not found for node #' + node.id);
response.end();
return callback(null, true);
}
var attributes = node.attributes || {};
this.sendContentURL({
contentURL: contentURL,
mtime: node.contentTime,
hash: node.contentHash,
size: attributes.size,
mimeType: attributes.mimeType
}, request, response, callback);
}
sendContentStream(attributes, request, response, callback) {
debug('sendContentStream', 'headers=', request.headers, 'headersSent=', response.headersSent);
const stream = attributes.stream;
if (attributes.mimeType !== undefined) {
response.setHeader('Content-Type', attributes.mimeType);
}
response.setHeader('Content-Length', attributes.size);
if (attributes.duration !== undefined) {
response.setHeader('Content-Duration', attributes.duration);
response.setHeader('X-Content-Duration', attributes.duration); // Older Mozilla version
}
// var opts = {};
// var ranges = request.headers.range;
// if (ranges) {
// // var rs = rangeParser(attributes.size /*999999999*/ /*todo*/, ranges);
// var rs = rangeParser(999999999 /*todo*/, ranges);
// debug('sendContentURL', 'RangeParser=', rs, 'ranges=', rang