@openveo/publish
Version:
OpenVeo video publication plugin
1,438 lines (1,249 loc) • 79.6 kB
JavaScript
'use strict';
/**
* @module publish/controllers/VideoController
*/
var util = require('util');
var path = require('path');
var fs = require('fs');
var url = require('url');
var async = require('async');
var openVeoApi = require('@openveo/api');
var coreApi = process.api.getCoreApi();
var fileSystemApi = openVeoApi.fileSystem;
var configDir = fileSystemApi.getConfDir();
var HTTP_ERRORS = process.requirePublish('app/server/controllers/httpErrors.js');
var VideoProvider = process.requirePublish('app/server/providers/VideoProvider.js');
var PropertyProvider = process.requirePublish('app/server/providers/PropertyProvider.js');
var PoiProvider = process.requirePublish('app/server/providers/PoiProvider.js');
var STATES = process.requirePublish('app/server/packages/states.js');
var PublishManager = process.requirePublish('app/server/PublishManager.js');
var mediaPlatformFactory = process.requirePublish('app/server/providers/mediaPlatforms/factory.js');
var TYPES = process.requirePublish('app/server/providers/mediaPlatforms/types.js');
var platforms = require(path.join(configDir, 'publish/videoPlatformConf.json'));
var publishConf = require(path.join(configDir, 'publish/publishConf.json'));
var MultipartParser = openVeoApi.multipart.MultipartParser;
var ContentController = openVeoApi.controllers.ContentController;
var ResourceFilter = openVeoApi.storages.ResourceFilter;
var env = (process.env.NODE_ENV === 'production') ? 'prod' : 'dev';
/**
* Defines a controller to handle actions relative to videos' routes.
*
* @class VideoController
* @extends ContentController
* @constructor
* @see {@link https://github.com/veo-labs/openveo-api|OpenVeo API documentation} for more information about ContentController
*/
function VideoController() {
VideoController.super_.call(this);
}
module.exports = VideoController;
util.inherits(VideoController, ContentController);
/**
* Tests if given points of interest are expressed in percentage instead of milliseconds.
*
* Old versions of OpenVeo Publish were using percentage instead of milliseconds for chapters, tags and cuts.
*
* @static
* @private
* @memberof module:publish/controllers/VideoController~VideoController
* @param {Array} pointsOfInterest The list of points of interest
* @param {Number} pointsOfInterest[].value Point of interest value
* @return {Boolean} true if unit is percentage, false if unit is milliseconds
*/
function isPointsOfInterestOldUnit(pointsOfInterest) {
if (!pointsOfInterest || !pointsOfInterest.length) return false;
var sortedPointsOfInterest = pointsOfInterest.sort(function(a, b) {
switch (true) {
case a.value < b.value:
return -1;
case a.value > b.value:
return 1;
default:
return 0;
}
});
return sortedPointsOfInterest[sortedPointsOfInterest.length - 1].value <= 1 &&
sortedPointsOfInterest[sortedPointsOfInterest.length - 1].value !== 0;
}
/**
* Resolves medias resources urls using CDN url.
*
* Medias may have attached resources like files associated to tags, timecodes images, thumbnail image and
* so on. These resources must be accessible through an url. As all resources must, in the future, reside in
* a CDN, resolveResourcesUrls transforms all resources URIs to URLs based on CDN.
*
* @static
* @private
* @memberof module:publish/controllers/VideoController~VideoController
* @param {Array} medias The list of medias
*/
function resolveResourcesUrls(medias) {
var cdnUrl = coreApi.getCdnUrl();
var wowzaStreamPath = platforms[TYPES.WOWZA] && platforms[TYPES.WOWZA].streamPath &&
(new url.URL(platforms[TYPES.WOWZA].streamPath)).href;
var removeFirstSlashRegExp = new RegExp(/^\//);
if (medias && medias.length) {
medias.forEach(function(media) {
// Timecodes
if (media.timecodes) {
media.timecodes.forEach(function(timecode) {
if (timecode.image) {
if (timecode.image.small)
timecode.image.small.url = cdnUrl + timecode.image.small.url.replace(removeFirstSlashRegExp, '');
if (timecode.image.large)
timecode.image.large = cdnUrl + timecode.image.large.replace(removeFirstSlashRegExp, '');
}
});
}
// Tags
if (media.tags) {
media.tags.forEach(function(tag) {
if (tag.file && tag.file.url)
tag.file.url = cdnUrl + tag.file.url.replace(removeFirstSlashRegExp, '');
});
}
// Thumbnail
if (media.thumbnail)
media.thumbnail = cdnUrl + media.thumbnail.replace(removeFirstSlashRegExp, '');
// Local videos are hosted in local and consequently delivered by OpenVeo HTTP server
if (media.type === TYPES.LOCAL && media.sources) {
media.sources.forEach(function(source) {
if (source.files) {
source.files.forEach(function(file) {
if (file.link)
file.link = cdnUrl + file.link.replace(removeFirstSlashRegExp, '');
});
}
});
}
// Wowza videos links are relative to the streamPath defined in configuration
if (media.type === TYPES.WOWZA && media.sources) {
media.sources.forEach(function(source) {
if (source.adaptive) {
source.adaptive.forEach(function(adaptiveSource) {
if (adaptiveSource.link)
adaptiveSource.link = wowzaStreamPath + adaptiveSource.link;
});
}
});
}
});
}
}
/**
* Updates a point of interest associated to the given media.
*
* If point of interest does not exist it is created.
*
* @example
* // Response example
* {
* "total": 1,
* "poi": ...
* }
*
* @memberof module:publish/controllers/VideoController~VideoController
* @this module:publish/controllers/VideoController~VideoController
* @private
* @param {String} type The type of point of interest (either 'tags' or 'chapters')
* @param {Request} request ExpressJS HTTP Request
* @param {Object} [request.body] Request multipart body
* @param {Object} [request.body.info] Point of interest information
* @param {Number} [request.body.info.value] The point of interest time in milliseconds
* @param {String} [request.body.info.name] The point of interest name
* @param {String} [request.body.info.description] The point of interest description
* @param {String} [request.body.file] The multipart file associated to the point of interest
* @param {Object} request.params Request's parameters
* @param {String} request.params.id The media id the point of interest belongs to
* @param {String} [request.params.poiid] The point of interest id
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
function updatePoiAction(type, request, response, next) {
if (!request.params.id) return next(HTTP_ERRORS.UPDATE_POI_MISSING_PARAMETERS);
var self = this;
var params;
try {
params = openVeoApi.util.shallowValidateObject(request.params, {
id: {type: 'string', required: true},
poiid: {type: 'string'}
});
} catch (error) {
return next(HTTP_ERRORS.UPDATE_POI_WRONG_PARAMETERS);
}
var fileInfo = null;
var media;
var poi;
var mediaId = params.id;
var totalUpdatedPois = 0;
var poiId = params.poiid;
var provider = this.getProvider();
var poiFileDestinationPath = path.join(process.rootPublish, 'assets/player/videos', mediaId, 'uploads');
var mediaFilter = new ResourceFilter().equal('id', mediaId);
async.series([
// Parse body multipart data
function(callback) {
var parser = new MultipartParser(request, [
{
name: 'file',
destinationPath: poiFileDestinationPath,
maxCount: 1
}
], {
fileSize: 20 * 1000 * 1000
});
parser.parse(function(parseError) {
if (parseError) {
process.logger.error(parseError.message, {error: parseError, method: 'updatePoiAction'});
return callback(HTTP_ERRORS.UPDATE_POI_UPLOAD_ERROR);
}
if (!request.body.info) return callback(HTTP_ERRORS.UPDATE_POI_MISSING_PARAMETERS);
var file = request.files.file ? request.files.file[0] : null;
var info = JSON.parse(request.body.info);
try {
poi = openVeoApi.util.shallowValidateObject(info, {
value: {type: 'number'},
name: {type: 'string'},
description: {type: 'string'}
});
} catch (error) {
return callback(HTTP_ERRORS.UPDATE_POI_WRONG_PARAMETERS);
}
if (file) {
fileInfo = {
originalName: file.originalname,
mimeType: file.mimetype,
fileName: file.filename,
size: file.size,
path: file.path
};
poi.file = fileInfo;
poi.file.url = provider.getPoiFilePath(mediaId, poi.file);
} else if (info.file) {
fileInfo = info.file;
delete poi.file;
} else {
poi.file = null;
}
callback();
});
},
// Fetch media and make sure user has enough privilege to update the media
function(callback) {
provider.getOne(
mediaFilter,
{
include: ['id', 'metadata', type]
},
function(getOneError, fetchedMedia) {
media = fetchedMedia;
if (getOneError) {
process.logger.error(getOneError.message, {error: getOneError, method: 'updatePoiAction'});
return callback(HTTP_ERRORS.UPDATE_POI_GET_ONE_ERROR);
}
if (!self.isUserAuthorized(request.user, media, ContentController.OPERATIONS.UPDATE))
return callback(HTTP_ERRORS.UPDATE_POI_FORBIDDEN);
callback();
}
);
},
// Create / update point of interest
function(callback) {
var poiProvider = new PoiProvider(coreApi.getDatabase());
if (poiId) {
// Point of interest already exists
// Update it
poiProvider.updateOne(new ResourceFilter().equal('id', poiId), poi, function(updateError, total) {
if (updateError) {
process.logger.error(updateError.message, {error: updateError, method: 'updatePoiAction'});
return callback(HTTP_ERRORS.UPDATE_POI_UPDATE_ERROR);
}
totalUpdatedPois = total;
callback();
});
} else {
// Point of interest does not exist
// Create it
poiProvider.add([poi], function(createError, total, pois) {
if (createError) {
process.logger.error(createError.message, {error: createError, method: 'updatePoiAction'});
return callback(HTTP_ERRORS.UPDATE_POI_CREATE_ERROR);
}
poiId = pois[0].id;
totalUpdatedPois = total;
callback();
});
}
},
// Add point of interest to the media if not already associated to it
function(callback) {
if (media[type] && media[type].indexOf(poiId) !== -1) return callback();
var data = {};
data[type] = media[type] || [];
data[type].push(poiId);
provider.updateOne(mediaFilter, data, function(updateMediaError, total) {
if (updateMediaError) {
process.logger.error(updateMediaError.message, {error: updateMediaError, method: 'updatePoiAction'});
return callback(HTTP_ERRORS.UPDATE_POI_UPDATE_MEDIA_ERROR);
}
callback();
});
}
], function(error) {
if (error) return next(error);
poi.id = poiId;
poi.file = fileInfo;
response.send({total: totalUpdatedPois, poi: poi});
});
}
/**
* Removes points of interest from a media.
*
* @example
*
* // Response example
* {
* "total": 1
* }
*
* @memberof module:publish/controllers/VideoController~VideoController
* @this module:publish/controllers/VideoController~VideoController
* @private
* @param {String} type The type of points of interest (either 'tags' or 'chapters')
* @param {Request} request ExpressJS HTTP Request
* @param {Object} request.params Request parameters
* @param {String} request.params.id The media id
* @param {String} request.params.poiids A comma separated list of points of interest ids to remove
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
function removePoisAction(type, request, response, next) {
if (!request.params.id || !request.params.poiids) return next(HTTP_ERRORS.REMOVE_POIS_MISSING_PARAMETERS);
var self = this;
var params;
try {
params = openVeoApi.util.shallowValidateObject(request.params, {
id: {type: 'string', required: true},
poiids: {type: 'string', required: true}
});
} catch (error) {
return next(HTTP_ERRORS.REMOVE_POIS_WRONG_PARAMETERS);
}
var media;
var totalRemovedPois = 0;
var poiIds = params.poiids.split(',');
var provider = this.getProvider(request);
var mediaFilter = new ResourceFilter().equal('id', params.id);
async.series([
// Fetch the media and make sure user has enough privilege to update the media
function(callback) {
provider.getOne(
mediaFilter,
{
include: ['id', 'metadata', type]
},
function(getOneError, fetchedMedia) {
media = fetchedMedia;
if (getOneError) {
process.logger.error(getOneError.message, {error: getOneError, method: 'removePoisAction'});
return callback(HTTP_ERRORS.REMOVE_POIS_GET_ONE_ERROR);
}
if (!self.isUserAuthorized(request.user, media, ContentController.OPERATIONS.UPDATE))
return callback(HTTP_ERRORS.REMOVE_POIS_FORBIDDEN);
callback();
}
);
},
// Remove points of interest
function(callback) {
var poiProvider = new PoiProvider(coreApi.getDatabase());
poiProvider.remove(new ResourceFilter().in('id', poiIds), function(removeError, total) {
if (removeError) {
process.logger.error(removeError.message, {error: removeError, method: 'removePoisAction'});
return callback(HTTP_ERRORS.REMOVE_POIS_REMOVE_ERROR);
}
totalRemovedPois = total;
callback();
});
},
// Remove points of interest from the media
function(callback) {
var poisIdsToKeep = media[type].filter(function(poiId) {
return poiIds.indexOf(poiId) === -1;
});
var data = {};
data[type] = poisIdsToKeep;
provider.updateOne(mediaFilter, data, function(updateMediaError, total) {
if (updateMediaError) {
process.logger.error(updateMediaError.message, {error: updateMediaError, method: 'removePoisAction'});
return callback(HTTP_ERRORS.REMOVE_POIS_UPDATE_MEDIA_ERROR);
}
callback();
});
}
], function(error) {
if (error) return next(error);
response.send({total: totalRemovedPois});
});
}
/**
* Replaces media chapters ids and tags ids by detailed points of interest.
*
* @memberof module:publish/controllers/VideoController~VideoController
* @this module:publish/controllers/VideoController~VideoController
* @private
* @param {Object} media The media to populate
* @param {Array} media.chapters The media chapters
* @param {Array} media.tags The media tags
* @param {Function} callback Function to call when media has been populated
*/
function populateMediaWithPois(media, callback) {
var poisIds = (media.chapters || []).concat(media.tags || []);
if (!poisIds.length) {
media.needPointsOfInterestUnitConversion = isPointsOfInterestOldUnit((media.cut || []));
return callback();
}
var poiProvider = new PoiProvider(coreApi.getDatabase());
poiProvider.getAll(
new ResourceFilter().in('id', poisIds),
{
exclude: ['_id']
},
{
id: 'desc'
},
function(getPoisError, fetchedPois) {
var pois = fetchedPois || [];
if (getPoisError) {
return callback(getPoisError);
}
if (media.tags) {
media.tags = pois.filter(function(poi) {
if (poi.file) delete poi.file.path;
return media.tags.indexOf(poi.id) !== -1;
});
}
if (media.chapters) {
media.chapters = pois.filter(function(poi) {
if (poi.file) delete poi.file.path;
return media.chapters.indexOf(poi.id) !== -1;
});
}
media.needPointsOfInterestUnitConversion = isPointsOfInterestOldUnit(pois.concat(media.cut));
callback();
}
);
}
/**
* Updates the given media with corresponding information from its video platform.
*
* If information from the video platform have already been fetched for this media this does nothing.
*
* @memberof module:publish/controllers/VideoController~VideoController
* @this module:publish/controllers/VideoController~VideoController
* @private
* @param {Object} media The media to update
* @param {String} media.id The media id
* @param {String} media.type The id of the associated media platform
* @param {Array} media.mediaId The list of medias in the media platform. Could have several media ids if media has
* multiple sources
* @param {Boolan} media.available true if the media is available, false otherwise, if true information from the video
* platform have already been fetched then this does nothing
* @param {Array} media.sources The list of media sources
* @param {Function} callback Function to call when media has been updated
*/
function updateMediaWithPlatformInfo(media, callback) {
if (
!media.type ||
!media.mediaId ||
(media.available &&
(
media.sources.length == media.mediaId.length ||
media.type === TYPES.YOUTUBE
)
)
) {
// Info from video platform already retrieved for this media
return callback();
}
// Get information about the media from the medias platform
var mediaPlatformProvider = mediaPlatformFactory.get(media.type, platforms[media.type]);
// Compatibility with old mediaId format
var mediasIds = !Array.isArray(media.mediaId) ? [media.mediaId] : media.mediaId;
var provider = this.getProvider();
// Get media availability and sources
mediaPlatformProvider.getMediasInfo(mediasIds, media.mediasHeights, function(error, info) {
if (error) return callback(error);
media.available = info.available;
media.sources = info.sources;
provider.updateOne(new ResourceFilter().equal('id', media.id), info, callback);
});
}
/**
* Displays video player template.
*
* Checks first if the video id is valid and if the video is published
* before returning the template.
*
* @param {Request} request ExpressJS HTTP Request
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
VideoController.prototype.displayVideoAction = function(request, response, next) {
var publishPlugin;
var plugins = process.api.getPlugins();
response.locals.scripts = [];
response.locals.css = [];
response.locals.languages = ['"en"', '"fr"'];
plugins.forEach(function(subPlugin) {
if (subPlugin.name === 'publish')
publishPlugin = subPlugin;
});
if (publishPlugin) {
if (publishPlugin.custom) {
var customScripts = publishPlugin.custom.scriptFiles;
var playerScripts = customScripts.publishPlayer;
// Custom scripts
if (customScripts && customScripts.base) {
response.locals.scripts = response.locals.scripts.concat(customScripts.base.map(function(customScript) {
return path.join(publishPlugin.mountPath, customScript);
}));
}
// Custom player scripts
if (playerScripts && playerScripts[env]) {
response.locals.scripts = response.locals.scripts.concat(playerScripts[env].map(function(playerScript) {
return path.join(publishPlugin.mountPath, playerScript);
}));
}
// Custom CSS
if (publishPlugin.custom.cssFiles) {
response.locals.css = response.locals.css.concat(
publishPlugin.custom.cssFiles.map(function(cssFile) {
return path.join(publishPlugin.mountPath, cssFile);
})
);
}
}
response.render('player', response.locals);
} else
next();
};
/**
* Gets all media platforms available.
*
* @example
* {
* "platforms" : [
* ...
* ]
* }
*
* @param {Request} request ExpressJS HTTP Request
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
VideoController.prototype.getPlatformsAction = function(request, response) {
response.send({
platforms: Object.keys(platforms) ? Object.keys(platforms).filter(function(value) {
return platforms[value];
}) : []
});
};
/**
* Gets a ready media.
*
* A ready media is a media with a state set to ready or published.
* Connected users may have access to ready medias but unconnected users can only access published medias.
*
* @example
* // Response example
* {
* "entity" : {
* "id": ..., // The media id
* "state": ..., // The media state
* "date": ..., // The media published date as a timestamp
* "type": ..., // The video associated platform
* "errorCode": ..., // The media error code or -1 if no error
* "category": ..., // The media category
* "properties": {...}, // The media custom properties
* "link": ..., // The media URL
* "mediaId": [...], // The media id on the video platform
* "available": ..., // The media availability on the video platform
* "thumbnail": ..., // The media thumbnail URL
* "title": ..., // The media title
* "leadParagraph": ..., // The media lead paragraph
* "description": ..., // The media description
* "chapters": [...], // The media chapters
* "tags": [...], // The media tags
* "cut": [...], // The media begin and end cuts
* "timecodes": [...], // The media associated images
* }
* }
*
* @param {Request} request ExpressJS HTTP Request
* @param {Object} request.params Request's parameters
* @param {String} request.params.id The media id
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
VideoController.prototype.getVideoReadyAction = function(request, response, next) {
if (!request.params.id) return next(HTTP_ERRORS.GET_VIDEO_READY_MISSING_PARAMETERS);
var params;
var self = this;
try {
params = openVeoApi.util.shallowValidateObject(request.params, {
id: {type: 'string', required: true}
});
} catch (error) {
return next(HTTP_ERRORS.GET_VIDEO_READY_WRONG_PARAMETERS);
}
var media;
var provider = this.getProvider();
async.series([
// Get video
function(callback) {
provider.getOne(
new ResourceFilter().equal('id', params.id),
null,
function(getOneError, fetchedMedia) {
media = fetchedMedia;
if (getOneError) {
process.logger.error(getOneError.message, {error: getOneError, method: 'getVideoReadyAction'});
return callback(HTTP_ERRORS.GET_VIDEO_READY_ERROR);
}
if (!media) {
process.logger.warn('Not found', {method: 'getVideoReadyAction', entity: params.id});
return callback(HTTP_ERRORS.GET_VIDEO_READY_NOT_FOUND);
}
// Media not ready
if (media.state !== STATES.READY && media.state !== STATES.PUBLISHED)
return callback(HTTP_ERRORS.GET_VIDEO_READY_NOT_READY_ERROR);
// User without enough privilege to read the media in ready state
if (media.state === STATES.READY &&
!self.isUserAuthorized(request.user, media, ContentController.OPERATIONS.READ)
) {
return callback(HTTP_ERRORS.GET_VIDEO_READY_FORBIDDEN);
}
callback();
}
);
},
// Populate media with points of interest
function(callback) {
populateMediaWithPois(media, function(populateError) {
if (populateError) {
process.logger.error(populateError.message, {error: populateError, method: 'getVideoReadyAction'});
return callback(HTTP_ERRORS.GET_VIDEO_READY_POPULATE_WITH_POIS_ERROR);
}
callback();
});
},
// Update media with information from the video platform
function(callback) {
updateMediaWithPlatformInfo.call(self, media, function(error) {
if (error) {
process.logger.error(error.message, {error: error, method: 'getVideoReadyAction'});
return callback(HTTP_ERRORS.GET_VIDEO_READY_UPDATE_MEDIA_WITH_PLATFORM_INFO_ERROR);
}
callback();
});
}
], function(error) {
if (error) return next(error);
resolveResourcesUrls([media]);
return response.send({
entity: media
});
});
};
/**
* Gets a media.
*
* @example
* // Response example
* {
* "entity" : {
* "id": ..., // The media id
* "state": ..., // The media state
* "date": ..., // The media published date as a timestamp
* "type": ..., // The video associated platform
* "errorCode": ..., // The media error code or -1 if no error
* "category": ..., // The media category
* "properties": {...}, // The media custom properties
* "link": ..., // The media URL
* "mediaId": [...], // The media id on the video platform
* "available": ..., // The media availability on the video platform
* "thumbnail": ..., // The media thumbnail URL
* "title": ..., // The media title
* "leadParagraph": ..., // The media lead paragraph
* "description": ..., // The media description
* "chapters": [...], // The media chapters
* "tags": [...], // The media tags
* "cut": [...], // The media begin and end cuts
* "timecodes": [...], // The media associated images
* }
* }
*
* @param {Request} request ExpressJS HTTP Request
* @param {Object} request.params Request parameters
* @param {String} request.params.id The id of the media to retrieve
* @param {Object} request.query Request query
* @param {(String|Array)} [request.query.include] The list of fields to include from returned media
* @param {(String|Array)} [request.query.exclude] The list of fields to exclude from returned media. Ignored if
* include is also specified.
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
VideoController.prototype.getEntityAction = function(request, response, next) {
if (!request.params.id) return next(HTTP_ERRORS.GET_MEDIA_MISSING_PARAMETERS);
var entityId = request.params.id;
var provider = this.getProvider();
var self = this;
var query;
var fields;
var media;
request.query = request.query || {};
try {
query = openVeoApi.util.shallowValidateObject(request.query, {
include: {type: 'array<string>'},
exclude: {type: 'array<string>'}
});
} catch (error) {
return next(HTTP_ERRORS.GET_MEDIA_WRONG_PARAMETERS);
}
// Make sure "metadata" field is not excluded
fields = this.removeMetatadaFromFields({
exclude: query.exclude,
include: query.include
});
async.series([
// Fetch media and make sure user has enough privilege to read it
function(callback) {
provider.getOne(
new ResourceFilter().equal('id', entityId),
fields,
function(error, fetchedMedia) {
media = fetchedMedia;
if (error) {
process.logger.error(error.message, {error: error, method: 'getEntityAction', entity: entityId});
return next(HTTP_ERRORS.GET_MEDIA_ERROR);
}
if (!media) {
process.logger.warn('Not found', {method: 'getEntityAction', entity: entityId});
return next(HTTP_ERRORS.GET_MEDIA_NOT_FOUND);
}
// User without enough privilege to read the media
if (!self.isUserAuthorized(request.user, media, ContentController.OPERATIONS.READ)) {
return next(HTTP_ERRORS.GET_MEDIA_FORBIDDEN);
}
callback();
}
);
},
// Get media points of interest
function(callback) {
populateMediaWithPois(media, function(populateError) {
if (populateError) {
process.logger.error(populateError.message, {error: populateError, method: 'getEntityAction'});
return callback(HTTP_ERRORS.GET_MEDIA_POPULATE_WITH_POIS_ERROR);
}
callback();
});
},
// Update media with information from the video platform
function(callback) {
updateMediaWithPlatformInfo.call(self, media, function(error) {
if (error) {
process.logger.error(error.message, {error: error, method: 'getEntityAction'});
return callback(HTTP_ERRORS.GET_MEDIA_UPDATE_MEDIA_WITH_PLATFORM_INFO_ERROR);
}
callback();
});
}
], function(error) {
if (error) return next(error);
resolveResourcesUrls([media]);
return response.send({
entity: media
});
});
};
/**
* Adds a media.
*
* @param {Request} request ExpressJS HTTP Request
* @param {Object} request.body The media information as multipart body
* @param {Object} [request.body.file] The media file as multipart data
* @param {Object} [request.body.thumbnail] The media thumbnail as multipart data
* @param {Object} request.body.info The media information
* @param {String} request.body.info.title The media title
* @param {Object} [request.body.info.properties] The media custom properties values with property id as keys
* @param {String} [request.body.info.category] The media category id it belongs to
* @param {(Date|Number|String)} [request.body.info.date] The media date
* @param {String} [request.body.info.leadParagraph] The media lead paragraph
* @param {String} [request.body.info.description] The media description
* @param {Array} [request.body.info.groups] The media content groups it belongs to
* @param {String} [request.body.info.platform] The platform to upload the file to
* @param {String} [request.body.info.user] The id of the OpenVeo user to use as the video owner
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
VideoController.prototype.addEntityAction = function(request, response, next) {
if (!request.body) return next(HTTP_ERRORS.ADD_MEDIA_MISSING_PARAMETERS);
var self = this;
var mediaId;
var categoriesIds;
var groupsIds;
var customProperties;
var params;
var mediaPackageType;
var parser = new MultipartParser(request, [
{
name: 'file',
destinationPath: publishConf.videoTmpDir,
maxCount: 1,
unique: true
},
{
name: 'thumbnail',
destinationPath: publishConf.videoTmpDir,
maxCount: 1
}
]);
async.parallel([
// Get the list of categories
function(callback) {
coreApi.taxonomyProvider.getTaxonomyTerms('categories', function(error, terms) {
if (error) {
process.logger.error(error.message, {error: error, method: 'addEntityAction'});
categoriesIds = [];
} else
categoriesIds = openVeoApi.util.getPropertyFromArray('id', terms, 'items');
callback();
});
},
// Get the list of groups
function(callback) {
coreApi.groupProvider.getAll(null, null, {id: 'desc'}, function(error, groups) {
if (error) {
process.logger.error(error.message, {error: error, method: 'addEntityAction'});
return callback(HTTP_ERRORS.ADD_MEDIA_GROUPS_ERROR);
}
groupsIds = openVeoApi.util.getPropertyFromArray('id', groups);
callback();
});
},
// Get the list of custom properties
function(callback) {
var database = coreApi.getDatabase();
var propertyProvider = new PropertyProvider(database);
propertyProvider.getAll(null, null, {id: 'desc'}, function(error, properties) {
if (error) {
process.logger.error(error.message, {error: error, method: 'addEntityAction'});
return callback(HTTP_ERRORS.ADD_MEDIA_CUSTOM_PROPERTIES_ERROR);
}
customProperties = properties;
callback();
});
}
], function(error) {
if (error) return next(error);
async.series([
// Parse multipart body
function(callback) {
parser.parse(function(error) {
if (error) {
process.logger.error(error.message, {error: error, method: 'addEntityAction'});
return callback(HTTP_ERRORS.ADD_MEDIA_PARSE_ERROR);
}
if (!request.body.info) return callback(HTTP_ERRORS.ADD_MEDIA_MISSING_INFO_PARAMETERS);
request.body.info = JSON.parse(request.body.info);
callback();
});
},
// Validate file
function(callback) {
if (!request.files || !request.files.file || !request.files.file.length)
return callback(HTTP_ERRORS.ADD_MEDIA_MISSING_FILE_PARAMETER);
openVeoApi.util.validateFiles({
file: request.files.file[0].path,
validateExtension: true
}, {
file: {in: [fileSystemApi.FILE_TYPES.MP4, fileSystemApi.FILE_TYPES.TAR, fileSystemApi.FILE_TYPES.ZIP]}
}, function(validateError, files) {
if (validateError || (files.file && !files.file.isValid)) {
if (validateError)
process.logger.error(validateError.message, {error: validateError, method: 'addEntityAction'});
callback(HTTP_ERRORS.ADD_MEDIA_WRONG_FILE_PARAMETER);
} else {
mediaPackageType = files.file.type;
callback();
}
});
},
// Validate custom properties
function(callback) {
var validationDescriptor = {};
// Iterate through custom properties values
for (var id in request.body.info.properties) {
var value = request.body.info.properties[id];
// Iterate through custom properties descriptors
for (var i = 0; i < customProperties.length; i++) {
var customProperty = customProperties[i];
if (customProperties[i].id === id) {
// Found custom property description corresponding to the custom property from request
// Add its validation descriptor
if (customProperty.type === PropertyProvider.TYPES.BOOLEAN)
validationDescriptor[id] = {type: 'boolean'};
else if (customProperty.type === PropertyProvider.TYPES.LIST && value !== null)
validationDescriptor[id] = {type: 'string'};
else if (customProperty.type === PropertyProvider.TYPES.TEXT)
validationDescriptor[id] = {type: 'string'};
else if (customProperty.type === PropertyProvider.TYPES.DATE_TIME)
validationDescriptor[id] = {type: 'number'};
break;
}
}
}
try {
request.body.info.properties = openVeoApi.util.shallowValidateObject(
request.body.info.properties,
validationDescriptor
);
} catch (validationError) {
process.logger.error(validationError.message, {error: validationError, method: 'addEntityAction'});
return callback(HTTP_ERRORS.ADD_MEDIA_WRONG_PROPERTIES_PARAMETER);
}
callback();
},
// Validate other parameters
function(callback) {
try {
var validationDescriptor = {
title: {type: 'string', required: true},
date: {type: 'number', default: Date.now()},
leadParagraph: {type: 'string'},
description: {type: 'string'},
groups: {type: 'array<string>', in: groupsIds},
user: {type: 'string'}
};
if (request.body.info.category)
validationDescriptor.category = {type: 'string', in: categoriesIds};
if (request.body.info.platform)
validationDescriptor.platform = {type: 'string', in: Object.keys(platforms)};
params = openVeoApi.util.shallowValidateObject(request.body.info, validationDescriptor);
} catch (validationError) {
process.logger.error(validationError.message, {error: validationError, method: 'addEntityAction'});
return callback(HTTP_ERRORS.ADD_MEDIA_WRONG_PARAMETERS);
}
callback();
},
// Make sure that user exists
function(callback) {
if (!params.user) return callback();
coreApi.userProvider.getOne(new ResourceFilter().equal('id', params.user), null, function(error, fetchedUser) {
if (error) {
process.logger.error(error.message, {error: error, method: 'addEntityAction'});
return callback(HTTP_ERRORS.ADD_MEDIA_VERIFY_OWNER_ERROR);
} else if (!fetchedUser) {
process.logger.error('User "' + params.user + '" does not exist', {method: 'addEntityAction'});
return callback(HTTP_ERRORS.ADD_MEDIA_WRONG_USER_PARAMETER);
}
callback();
});
},
// Add new media
function(callback) {
var pathDescriptor = path.parse(request.files.file[0].path);
var publishManager = self.getPublishManager();
var listener = function(mediaPackage) {
if (mediaPackage.originalPackagePath === request.files.file[0].path) {
mediaId = mediaPackage.id;
publishManager.removeListener('stateChanged', listener);
callback();
}
};
// Make sure process has started before sending back response to the client
publishManager.on('stateChanged', listener);
publishManager.publish({
originalPackagePath: request.files.file[0].path,
originalThumbnailPath: request.files.thumbnail ? request.files.thumbnail[0].path : undefined,
originalFileName: pathDescriptor.name,
title: params.title,
date: params.date,
leadParagraph: params.leadParagraph,
description: params.description,
category: params.category,
groups: params.groups,
user: params.user || (request.user.type === 'oAuthClient' ? coreApi.getSuperAdminId() : request.user.id),
properties: request.body.info.properties,
packageType: mediaPackageType,
type: params.platform
});
}
], function(error) {
if (error) {
if (request.files && request.files.file && request.files.file.length && request.files.file[0].path) {
// Remove temporary file
fs.unlink(request.files.file[0].path, function(unlinkError) {
if (unlinkError) {
process.logger.error(unlinkError.message, {error: unlinkError, method: 'addEntityAction'});
return next(HTTP_ERRORS.ADD_MEDIA_REMOVE_FILE_ERROR);
}
next(error);
});
} else
next(error);
} else response.send({id: mediaId});
});
});
};
/**
* Updates a media.
*
* @example
* // Response example
* {
* "total": 1
* }
*
* @param {Request} request ExpressJS HTTP Request
* @param {String} request.params.id Id of the media to update
* @param {Object} request.body The media information as multipart body
* @param {Object} [request.body.thumbnail] The media thumbnail as multipart data
* @param {Object} request.body.info The media information
* @param {String} [request.body.info.title] The media title
* @param {Object} [request.body.info.properties] The media custom properties values with property id as keys
* @param {String} [request.body.info.category] The media category id it belongs to
* @param {(Date|Number|String)} [request.body.info.date] The media date
* @param {String} [request.body.info.leadParagraph] The media lead paragraph
* @param {String} [request.body.info.description] The media description
* @param {Array} [request.body.info.groups] The media content groups it belongs to
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
VideoController.prototype.updateEntityAction = function(request, response, next) {
if (!request.body || !request.params.id) return next(HTTP_ERRORS.UPDATE_MEDIA_MISSING_PARAMETERS);
var media;
var totalUpdated;
var self = this;
var mediaId = request.params.id;
var provider = this.getProvider();
var parser = new MultipartParser(request, [
{
name: 'thumbnail',
destinationPath: publishConf.videoTmpDir,
maxCount: 1
}
]);
parser.parse(function(error) {
if (error) {
process.logger.error(error.message, {error: error, method: 'updateEntityAction'});
next(HTTP_ERRORS.UPDATE_MEDIA_PARSE_ERROR);
}
var info = JSON.parse(request.body.info);
var files = request.files;
var thumbnail = files.thumbnail ? files.thumbnail[0] : undefined;
var imageDir = path.normalize(process.rootPublish + '/assets/player/videos/' + mediaId);
async.series([
// Verify that user has enough privilege to update the media
function(callback) {
provider.getOne(
new ResourceFilter().equal('id', mediaId), null, function(error, fetchedMedia) {
if (error) {
process.logger.error(error.message, {error: error, method: 'updateEntityAction'});
return callback(HTTP_ERRORS.UPDATE_MEDIA_GET_ONE_ERROR);
}
if (!fetchedMedia) return callback(HTTP_ERRORS.UPDATE_MEDIA_NOT_FOUND_ERROR);
media = fetchedMedia;
if (self.isUserAuthorized(request.user, media, ContentController.OPERATIONS.UPDATE)) {
// User is authorized to update but he must be owner to update the owner
if (!self.isUserOwner(media, request.user) &&
!self.isUserAdmin(request.user) &&
!self.isUserManager(request.user)) {
delete info['user'];
}
callback();
} else
callback(HTTP_ERRORS.UPDATE_MEDIA_FORBIDDEN);
}
);
},
// Validate the file
function(callback) {
if (!thumbnail) return callback();
openVeoApi.util.validateFiles(
{thumbnail: thumbnail.path},
{thumbnail: {in: [fileSystemApi.FILE_TYPES.JPG]}},
function(error, files) {
if (error)
process.logger.warn(error.message, {error: error, action: 'updateEntity', mediaId: mediaId});
if (!files.thumbnail.isValid) return callback(HTTP_ERRORS.INVALID_VIDEO_THUMBNAIL);
callback();
}
);
},
// Copy the file
function(callback) {
if (!thumbnail) return callback();
fileSystemApi.copy(thumbnail.path, path.join(imageDir, 'thumbnail.jpg'), function(error) {
if (error) {
process.logger.warn(
error.message,
{error: error, action: 'updateEntityAction', mediaId: mediaId, thumbnail: thumbnail.path}
);
}
fileSystemApi.rm(thumbnail.path, function(error) {
if (error) {
process.logger.warn(
error.message,
{error: error, action: 'updateEntityAction', mediaId: mediaId, thumbnail: thumbnail.path}
);
}
callback();
});
});
},
// Clear image thumbnail cache
function(callback) {
if (!thumbnail) return callback();
coreApi.clearImageCache(path.join(mediaId, 'thumbnail.jpg'), 'publish', function(error) {
if (error) {
process.logger.warn(
error.message,
{error: error, action: 'updateEntityAction', mediaId: mediaId}
);
}
callback();
});
},
// Update the media
function(callback) {
if (thumbnail) info.thumbnail = '/publish/' + mediaId + '/thumbnail.jpg';
provider.updateOne(
new ResourceFilter().equal('id', mediaId),
info,
function(error, total) {
if (error) {
process.logger.error(error.message, {error: error, method: 'updateEntityAction', entity: mediaId});
return callback(HTTP_ERRORS.UPDATE_MEDIA_ERROR);
}
totalUpdated = total;
callback();
}
);
},
// Synchronize the media with the media platform
function(callback) {
if (!media.type || !media.mediaId) return callback();
var mediaPlatformProvider = mediaPlatformFactory.get(media.type, platforms[media.type]);
mediaPlatformProvider.update(media, info, false, function(error) {
if (error) {
process.logger.error(error.message, {error: error, method: 'updateEntityAction', entity: mediaId});
return callback(HTTP_ERRORS.UPDATE_MEDIA_SYNCHRONIZE_ERROR);
}
callback();
});
}
], function(error) {
if (error) return next(error);
response.send({total: totalUpdated});
});
});
};
/**
* Gets medias.
*
* @example
* // Response example
* {
* "entities" : [ ... ],
* "pagination" : {
* "limit": ..., // The limit number of medias by page
* "page": ..., // The actual page
* "pages": ..., // The total number of pages
* "size": ... // The total number of medias
* }
*
* @param {Request} request ExpressJS HTTP Request
* @param {Object} request.query Request's query parameters
* @param {String} [request.query.query] To search on both medias title and description
* @param {Number} [request.query.useSmartSearch=1] 1 to use a more advanced search mechanism, 0 to use a simple search
* based on a regular expression
* @param {Number} [request.query.searchInPois=0] 1 to also search in points of interest (tags / chapters) titles and
* descriptions when useSmartSearch is set to 1
* @param {(String|Array)} [request.query.include] The list of fields to include from returned medias
* @param {(String|Array)} [request.query.exclude] The list of fields to exclude from returned medias. Ignored if
* include is also specified.
* @param {(String|Array)} [request.query.states] To filter medias by state
* @param {String} [request.query.dateStart] To filter medias after or equal to a date (in format mm/dd/yyyy)
* @param {String} [request.query.dateEnd] To get medias before a date (in format mm/dd/yyyy)
* @param {(String|Array)} [request.query.categories] To filter medias by category
* @param {(String|Array)} [request.query.groups] To filter medias by group
* @param {(String|Array)} [request.query.user] To filter medias by user
* @param {String} [request.query.sortBy="date"] To sort medias by either **title**, **description**, **date**,
* **state**, **views** or **category**
* @param {String} [request.query.sortOrder="desc"] Sort order (either **asc** or **desc**)
* @param {String} [request.query.page=0] The expected page
* @param {String} [request.query.limit=10] To limit the number of medias per page
* @param {Object} [request.query.properties] A list of properties with the property id as the key and the expected
* property value as the value
* @param {Response} response ExpressJS HTTP Response
* @param {Function} next Function to defer execution to the next registered middleware
*/
VideoController.prototype.getEntitiesAction = function(request, response, next) {
var params;
var fields;
var self = this;
var medias = [];
var properties = [];
var pagination = {};
var provider = this.getProvider();
var poiProvider = new PoiProvider(coreApi.getDatabase());
var orderedProperties = ['title', 'description', 'date', 'state', 'views', 'category'];
try {
params = openVeoApi.util.shallowValidateObject(request.query, {
query: {type: 'string'},
useSmartSearch: {type: 'number', in: [0, 1], default: 1},
searchInPois: {type: 'number', in: [0, 1], default: 0},
include: {type: 'array<string>'},
exclude: {type: 'array<string>'},
states: {type: 'array<number>'},
dateStart: {type: 'date'},
dateEnd: {type: 'date'},
categories: {type: 'array<string>'},
groups: {type: 'array<string>'},
user: {type: 'array<string>'},
properties: {type: 'object', default: {}},
limit: {type: 'number', gt: 0},
page: {type: 'number', gte: 0, default: 0},
sortBy: {type: 'string', in: orderedProperties, default: 'date'},
sortOrder: {type: 'string', in: ['asc', 'desc'], default: 'desc'}
});
} catch (error) {
return next(HTTP_ERRORS.GET_VIDEOS_WRONG_PARAMETERS);
}
// Build sort
var sort = {};
// Build filter
var filter = new ResourceFilter();
var querySearchFilters = [];
// Add search query
if (params.query) {
if (params.useSmartSearch) {
querySearchFilters.push(new ResourceFilter().search('"' + params.query + '"'));
sort['score'] = 'score';
} else {
var queryRegExp = new RegExp(openVeoApi.util.escapeTextForRegExp(params.query), 'i');
filter.or([
new ResourceFilter().regex('title', queryRegExp),
new ResourceFilter().regex('description', queryRegExp)
]);
}
}
// Sort
sort[params.sortBy] = params.sortOrder;
// Add states
if (params.states && params.states.length) filter.in('state', params.states);
// Add groups
if (params.groups && params.groups