X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fcontrollers%2Fapi%2Fremote%2Fvideos.ts;h=3ecc62ada1466a667c3983dd09898bd8432fc722;hb=b7a485121d71c95fcf5e432e4cc745cf91af4f93;hp=d9cc08fb41710c13b92d18aaf07b808eb064679f;hpb=69818c9394366b954b6ba3bd697bd9d2b09f2a16;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts index d9cc08fb4..3ecc62ada 100644 --- a/server/controllers/api/remote/videos.ts +++ b/server/controllers/api/remote/videos.ts @@ -1,6 +1,6 @@ import * as express from 'express' +import * as Bluebird from 'bluebird' import * as Sequelize from 'sequelize' -import { eachSeries, waterfall } from 'async' import { database as db } from '../../../initializers/database' import { @@ -16,24 +16,41 @@ import { remoteQaduVideosValidator, remoteEventsVideosValidator } from '../../../middlewares' +import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers' +import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib' +import { PodInstance, VideoFileInstance } from '../../../models' import { - logger, - commitTransaction, - retryTransactionWrapper, - rollbackTransaction, - startSerializableTransaction -} from '../../../helpers' -import { quickAndDirtyUpdatesVideoToFriends } from '../../../lib' -import { PodInstance, VideoInstance } from '../../../models' + RemoteVideoRequest, + RemoteVideoCreateData, + RemoteVideoUpdateData, + RemoteVideoRemoveData, + RemoteVideoReportAbuseData, + RemoteQaduVideoRequest, + RemoteQaduVideoData, + RemoteVideoEventRequest, + RemoteVideoEventData, + RemoteVideoChannelCreateData, + RemoteVideoChannelUpdateData, + RemoteVideoChannelRemoveData, + RemoteVideoAuthorRemoveData, + RemoteVideoAuthorCreateData +} from '../../../../shared' +import { VideoInstance } from '../../../models/video/video-interface' const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] // Functions to call when processing a remote request -const functionsHash = {} -functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper -functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper -functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo -functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo +// FIXME: use RemoteVideoRequestType as id type +const functionsHash: { [ id: string ]: (...args) => Promise } = {} +functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper +functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper const remoteVideosRouter = express.Router() @@ -67,457 +84,504 @@ export { // --------------------------------------------------------------------------- function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) { - const requests = req.body.data + const requests: RemoteVideoRequest[] = req.body.data const fromPod = res.locals.secure.pod // We need to process in the same order to keep consistency - // TODO: optimization - eachSeries(requests, function (request: any, callbackEach) { + Bluebird.each(requests, request => { const data = request.data // Get the function we need to call in order to process the request const fun = functionsHash[request.type] if (fun === undefined) { - logger.error('Unkown remote request type %s.', request.type) - return callbackEach(null) + logger.error('Unknown remote request type %s.', request.type) + return } - fun.call(this, data, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) + return fun.call(this, data, fromPod) }) + .catch(err => logger.error('Error managing remote videos.', err)) - // We don't need to keep the other pod waiting + // Don't block the other pod return res.type('json').status(204).end() } function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) { - const requests = req.body.data + const requests: RemoteQaduVideoRequest[] = req.body.data const fromPod = res.locals.secure.pod - eachSeries(requests, function (request: any, callbackEach) { + Bluebird.each(requests, request => { const videoData = request.data - quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) + return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod) }) + .catch(err => logger.error('Error managing remote videos.', err)) return res.type('json').status(204).end() } function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) { - const requests = req.body.data + const requests: RemoteVideoEventRequest[] = req.body.data const fromPod = res.locals.secure.pod - eachSeries(requests, function (request: any, callbackEach) { + Bluebird.each(requests, request => { const eventData = request.data - processVideosEventsRetryWrapper(eventData, fromPod, callbackEach) - }, function (err) { - if (err) logger.error('Error managing remote videos.', { error: err }) + return processVideosEventsRetryWrapper(eventData, fromPod) }) + .catch(err => logger.error('Error managing remote videos.', err)) return res.type('json').status(204).end() } -function processVideosEventsRetryWrapper (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) { const options = { arguments: [ eventData, fromPod ], errorMessage: 'Cannot process videos events with many retries.' } - retryTransactionWrapper(processVideosEvents, options, finalCallback) + await retryTransactionWrapper(processVideosEvents, options) } -function processVideosEvents (eventData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { - waterfall([ - startSerializableTransaction, +async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { + await db.sequelize.transaction(async t => { + const sequelizeOptions = { transaction: t } + const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t) - function findVideo (t, callback) { - fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) - }) - }, - - function updateVideoIntoDB (t, videoInstance, callback) { - const options = { transaction: t } - - let columnToUpdate - let qaduType + let columnToUpdate + let qaduType - switch (eventData.eventType) { - case REQUEST_VIDEO_EVENT_TYPES.VIEWS: - columnToUpdate = 'views' - qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS - break + switch (eventData.eventType) { + case REQUEST_VIDEO_EVENT_TYPES.VIEWS: + columnToUpdate = 'views' + qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS + break - case REQUEST_VIDEO_EVENT_TYPES.LIKES: - columnToUpdate = 'likes' - qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES - break + case REQUEST_VIDEO_EVENT_TYPES.LIKES: + columnToUpdate = 'likes' + qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES + break - case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: - columnToUpdate = 'dislikes' - qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES - break - - default: - return callback(new Error('Unknown video event type.')) - } + case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: + columnToUpdate = 'dislikes' + qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES + break - const query = {} - query[columnToUpdate] = eventData.count - - videoInstance.increment(query, options).asCallback(function (err) { - return callback(err, t, videoInstance, qaduType) - }) - }, - - function sendQaduToFriends (t, videoInstance, qaduType, callback) { - const qadusParams = [ - { - videoId: videoInstance.id, - type: qaduType - } - ] - - quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) { - return callback(err, t) - }) - }, + default: + throw new Error('Unknown video event type.') + } - commitTransaction + const query = {} + query[columnToUpdate] = eventData.count - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - logger.debug('Cannot process a video event.', { error: err }) - return rollbackTransaction(err, t, finalCallback) - } + await videoInstance.increment(query, sequelizeOptions) - logger.info('Remote video event processed for video %s.', eventData.remoteId) - return finalCallback(null) + const qadusParams = [ + { + videoId: videoInstance.id, + type: qaduType + } + ] + await quickAndDirtyUpdatesVideoToFriends(qadusParams, t) }) + + logger.info('Remote video event processed for video with uuid %s.', eventData.uuid) } -function quickAndDirtyUpdateVideoRetryWrapper (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) { const options = { arguments: [ videoData, fromPod ], errorMessage: 'Cannot update quick and dirty the remote video with many retries.' } - retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback) + await retryTransactionWrapper(quickAndDirtyUpdateVideo, options) } -function quickAndDirtyUpdateVideo (videoData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { - let videoName +async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) { + let videoUUID = '' - waterfall([ - startSerializableTransaction, + await db.sequelize.transaction(async t => { + const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t) + const sequelizeOptions = { transaction: t } - function findVideo (t, callback) { - fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) - }) - }, + videoUUID = videoInstance.uuid - function updateVideoIntoDB (t, videoInstance, callback) { - const options = { transaction: t } + if (videoData.views) { + videoInstance.set('views', videoData.views) + } - videoName = videoInstance.name + if (videoData.likes) { + videoInstance.set('likes', videoData.likes) + } - if (videoData.views) { - videoInstance.set('views', videoData.views) - } + if (videoData.dislikes) { + videoInstance.set('dislikes', videoData.dislikes) + } - if (videoData.likes) { - videoInstance.set('likes', videoData.likes) - } + await videoInstance.save(sequelizeOptions) + }) - if (videoData.dislikes) { - videoInstance.set('dislikes', videoData.dislikes) - } + logger.info('Remote video with uuid %s quick and dirty updated', videoUUID) +} - videoInstance.save(options).asCallback(function (err) { - return callback(err, t) - }) - }, +// Handle retries on fail +async function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { + const options = { + arguments: [ videoToCreateData, fromPod ], + errorMessage: 'Cannot insert the remote video with many retries.' + } - commitTransaction + await retryTransactionWrapper(addRemoteVideo, options) +} + +async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { + logger.debug('Adding remote video "%s".', videoToCreateData.uuid) - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - logger.debug('Cannot quick and dirty update the remote video.', { error: err }) - return rollbackTransaction(err, t, finalCallback) + await db.sequelize.transaction(async t => { + const sequelizeOptions = { + transaction: t } - logger.info('Remote video %s quick and dirty updated', videoName) - return finalCallback(null) + const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid) + if (videoFromDatabase) throw new Error('UUID already exists.') + + const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) + if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') + + const tags = videoToCreateData.tags + const tagInstances = await db.Tag.findOrCreateTags(tags, t) + + const videoData = { + name: videoToCreateData.name, + uuid: videoToCreateData.uuid, + category: videoToCreateData.category, + licence: videoToCreateData.licence, + language: videoToCreateData.language, + nsfw: videoToCreateData.nsfw, + description: videoToCreateData.truncatedDescription, + channelId: videoChannel.id, + duration: videoToCreateData.duration, + createdAt: videoToCreateData.createdAt, + // FIXME: updatedAt does not seems to be considered by Sequelize + updatedAt: videoToCreateData.updatedAt, + views: videoToCreateData.views, + likes: videoToCreateData.likes, + dislikes: videoToCreateData.dislikes, + remote: true + } + + const video = db.Video.build(videoData) + await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData) + const videoCreated = await video.save(sequelizeOptions) + + const tasks = [] + for (const fileData of videoToCreateData.files) { + const videoFileInstance = db.VideoFile.build({ + extname: fileData.extname, + infoHash: fileData.infoHash, + resolution: fileData.resolution, + size: fileData.size, + videoId: videoCreated.id + }) + + tasks.push(videoFileInstance.save(sequelizeOptions)) + } + + await Promise.all(tasks) + + await videoCreated.setTags(tagInstances, sequelizeOptions) }) + + logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) } // Handle retries on fail -function addRemoteVideoRetryWrapper (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +async function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { const options = { - arguments: [ videoToCreateData, fromPod ], - errorMessage: 'Cannot insert the remote video with many retries.' + arguments: [ videoAttributesToUpdate, fromPod ], + errorMessage: 'Cannot update the remote video with many retries' } - retryTransactionWrapper(addRemoteVideo, options, finalCallback) + await retryTransactionWrapper(updateRemoteVideo, options) } -function addRemoteVideo (videoToCreateData: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { - logger.debug('Adding remote video "%s".', videoToCreateData.remoteId) +async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { + logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) + let videoInstance: VideoInstance + let videoFieldsSave: object - waterfall([ + try { + await db.sequelize.transaction(async t => { + const sequelizeOptions = { + transaction: t + } - startSerializableTransaction, + const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t) + videoFieldsSave = videoInstance.toJSON() + const tags = videoAttributesToUpdate.tags - function assertRemoteIdAndHostUnique (t, callback) { - db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) { - if (err) return callback(err) + const tagInstances = await db.Tag.findOrCreateTags(tags, t) - if (video) return callback(new Error('RemoteId and host pair is not unique.')) + videoInstance.set('name', videoAttributesToUpdate.name) + videoInstance.set('category', videoAttributesToUpdate.category) + videoInstance.set('licence', videoAttributesToUpdate.licence) + videoInstance.set('language', videoAttributesToUpdate.language) + videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) + videoInstance.set('description', videoAttributesToUpdate.truncatedDescription) + videoInstance.set('duration', videoAttributesToUpdate.duration) + videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) + videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) + videoInstance.set('views', videoAttributesToUpdate.views) + videoInstance.set('likes', videoAttributesToUpdate.likes) + videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) - return callback(null, t) - }) - }, + await videoInstance.save(sequelizeOptions) - function findOrCreateAuthor (t, callback) { - const name = videoToCreateData.author - const podId = fromPod.id - // This author is from another pod so we do not associate a user - const userId = null + // Remove old video files + const videoFileDestroyTasks: Bluebird[] = [] + for (const videoFile of videoInstance.VideoFiles) { + videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions)) + } + await Promise.all(videoFileDestroyTasks) + + const videoFileCreateTasks: Bluebird[] = [] + for (const fileData of videoAttributesToUpdate.files) { + const videoFileInstance = db.VideoFile.build({ + extname: fileData.extname, + infoHash: fileData.infoHash, + resolution: fileData.resolution, + size: fileData.size, + videoId: videoInstance.id + }) + + videoFileCreateTasks.push(videoFileInstance.save(sequelizeOptions)) + } - db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { - return callback(err, t, authorInstance) - }) - }, + await Promise.all(videoFileCreateTasks) - function findOrCreateTags (t, author, callback) { - const tags = videoToCreateData.tags + await videoInstance.setTags(tagInstances, sequelizeOptions) + }) - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, author, tagInstances) - }) - }, - - function createVideoObject (t, author, tagInstances, callback) { - const videoData = { - name: videoToCreateData.name, - remoteId: videoToCreateData.remoteId, - extname: videoToCreateData.extname, - infoHash: videoToCreateData.infoHash, - category: videoToCreateData.category, - licence: videoToCreateData.licence, - language: videoToCreateData.language, - nsfw: videoToCreateData.nsfw, - description: videoToCreateData.description, - authorId: author.id, - duration: videoToCreateData.duration, - createdAt: videoToCreateData.createdAt, - // FIXME: updatedAt does not seems to be considered by Sequelize - updatedAt: videoToCreateData.updatedAt, - views: videoToCreateData.views, - likes: videoToCreateData.likes, - dislikes: videoToCreateData.dislikes - } + logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid) + } catch (err) { + if (videoInstance !== undefined && videoFieldsSave !== undefined) { + resetSequelizeInstance(videoInstance, videoFieldsSave) + } - const video = db.Video.build(videoData) + // This is just a debug because we will retry the insert + logger.debug('Cannot update the remote video.', err) + throw err + } +} - return callback(null, t, tagInstances, video) - }, +async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { + const options = { + arguments: [ videoToRemoveData, fromPod ], + errorMessage: 'Cannot remove the remote video channel with many retries.' + } - function generateThumbnail (t, tagInstances, video, callback) { - db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) { - if (err) { - logger.error('Cannot generate thumbnail from data.', { error: err }) - return callback(err) - } + await retryTransactionWrapper(removeRemoteVideo, options) +} - return callback(err, t, tagInstances, video) - }) - }, +async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { + logger.debug('Removing remote video "%s".', videoToRemoveData.uuid) - function insertVideoIntoDB (t, tagInstances, video, callback) { - const options = { - transaction: t - } + await db.sequelize.transaction(async t => { + // We need the instance because we have to remove some other stuffs (thumbnail etc) + const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t) + await videoInstance.destroy({ transaction: t }) + }) - video.save(options).asCallback(function (err, videoCreated) { - return callback(err, t, tagInstances, videoCreated) - }) - }, + logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid) +} - function associateTagsToVideo (t, tagInstances, video, callback) { - const options = { - transaction: t - } +async function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) { + const options = { + arguments: [ authorToCreateData, fromPod ], + errorMessage: 'Cannot insert the remote video author with many retries.' + } - video.setTags(tagInstances, options).asCallback(function (err) { - return callback(err, t) - }) - }, + await retryTransactionWrapper(addRemoteVideoAuthor, options) +} - commitTransaction +async function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) { + logger.debug('Adding remote video author "%s".', authorToCreateData.uuid) - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot insert the remote video.', { error: err }) - return rollbackTransaction(err, t, finalCallback) + await db.sequelize.transaction(async t => { + const authorInDatabase = await db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t) + if (authorInDatabase) throw new Error('Author with UUID ' + authorToCreateData.uuid + ' already exists.') + + const videoAuthorData = { + name: authorToCreateData.name, + uuid: authorToCreateData.uuid, + userId: null, // Not on our pod + podId: fromPod.id } - logger.info('Remote video %s inserted.', videoToCreateData.name) - return finalCallback(null) + const author = db.Author.build(videoAuthorData) + await author.save({ transaction: t }) }) + + logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid) } -// Handle retries on fail -function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { +async function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) { const options = { - arguments: [ videoAttributesToUpdate, fromPod ], - errorMessage: 'Cannot update the remote video with many retries' + arguments: [ authorAttributesToRemove, fromPod ], + errorMessage: 'Cannot remove the remote video author with many retries.' } - retryTransactionWrapper(updateRemoteVideo, options, finalCallback) + await retryTransactionWrapper(removeRemoteVideoAuthor, options) } -function updateRemoteVideo (videoAttributesToUpdate: any, fromPod: PodInstance, finalCallback: (err: Error) => void) { - logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId) +async function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) { + logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid) - waterfall([ + await db.sequelize.transaction(async t => { + const videoAuthor = await db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t) + await videoAuthor.destroy({ transaction: t }) + }) - startSerializableTransaction, + logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid) +} - function findVideo (t, callback) { - fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) { - return callback(err, t, videoInstance) - }) - }, +async function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) { + const options = { + arguments: [ videoChannelToCreateData, fromPod ], + errorMessage: 'Cannot insert the remote video channel with many retries.' + } - function findOrCreateTags (t, videoInstance, callback) { - const tags = videoAttributesToUpdate.tags + await retryTransactionWrapper(addRemoteVideoChannel, options) +} - db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { - return callback(err, t, videoInstance, tagInstances) - }) - }, +async function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) { + logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) - function updateVideoIntoDB (t, videoInstance, tagInstances, callback) { - const options = { transaction: t } + await db.sequelize.transaction(async t => { + const videoChannelInDatabase = await db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid) + if (videoChannelInDatabase) { + throw new Error('Video channel with UUID ' + videoChannelToCreateData.uuid + ' already exists.') + } - videoInstance.set('name', videoAttributesToUpdate.name) - videoInstance.set('category', videoAttributesToUpdate.category) - videoInstance.set('licence', videoAttributesToUpdate.licence) - videoInstance.set('language', videoAttributesToUpdate.language) - videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) - videoInstance.set('description', videoAttributesToUpdate.description) - videoInstance.set('infoHash', videoAttributesToUpdate.infoHash) - videoInstance.set('duration', videoAttributesToUpdate.duration) - videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) - videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) - videoInstance.set('extname', videoAttributesToUpdate.extname) - videoInstance.set('views', videoAttributesToUpdate.views) - videoInstance.set('likes', videoAttributesToUpdate.likes) - videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) + const authorUUID = videoChannelToCreateData.ownerUUID + const podId = fromPod.id - videoInstance.save(options).asCallback(function (err) { - return callback(err, t, videoInstance, tagInstances) - }) - }, + const author = await db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t) + if (!author) throw new Error('Unknown author UUID' + authorUUID + '.') - function associateTagsToVideo (t, videoInstance, tagInstances, callback) { - const options = { transaction: t } + const videoChannelData = { + name: videoChannelToCreateData.name, + description: videoChannelToCreateData.description, + uuid: videoChannelToCreateData.uuid, + createdAt: videoChannelToCreateData.createdAt, + updatedAt: videoChannelToCreateData.updatedAt, + remote: true, + authorId: author.id + } - videoInstance.setTags(tagInstances, options).asCallback(function (err) { - return callback(err, t) - }) - }, + const videoChannel = db.VideoChannel.build(videoChannelData) + await videoChannel.save({ transaction: t }) + }) - commitTransaction + logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid) +} - ], function (err: Error, t: Sequelize.Transaction) { - if (err) { - // This is just a debug because we will retry the insert - logger.debug('Cannot update the remote video.', { error: err }) - return rollbackTransaction(err, t, finalCallback) - } +async function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) { + const options = { + arguments: [ videoChannelAttributesToUpdate, fromPod ], + errorMessage: 'Cannot update the remote video channel with many retries.' + } + + await retryTransactionWrapper(updateRemoteVideoChannel, options) +} - logger.info('Remote video %s updated', videoAttributesToUpdate.name) - return finalCallback(null) +async function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) { + logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid) + + await db.sequelize.transaction(async t => { + const sequelizeOptions = { transaction: t } + + const videoChannelInstance = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t) + videoChannelInstance.set('name', videoChannelAttributesToUpdate.name) + videoChannelInstance.set('description', videoChannelAttributesToUpdate.description) + videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt) + videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt) + + await videoChannelInstance.save(sequelizeOptions) }) + + logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid) } -function removeRemoteVideo (videoToRemoveData: any, fromPod: PodInstance, callback: (err: Error) => void) { - // We need the instance because we have to remove some other stuffs (thumbnail etc) - fetchRemoteVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) { - // Do not return the error, continue the process - if (err) return callback(null) - - logger.debug('Removing remote video %s.', video.remoteId) - video.destroy().asCallback(function (err) { - // Do not return the error, continue the process - if (err) { - logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err }) - } +async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { + const options = { + arguments: [ videoChannelAttributesToRemove, fromPod ], + errorMessage: 'Cannot remove the remote video channel with many retries.' + } - return callback(null) - }) + await retryTransactionWrapper(removeRemoteVideoChannel, options) +} + +async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { + logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid) + + await db.sequelize.transaction(async t => { + const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t) + await videoChannel.destroy({ transaction: t }) }) + + logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid) } -function reportAbuseRemoteVideo (reportData: any, fromPod: PodInstance, callback: (err: Error) => void) { - fetchOwnedVideo(reportData.videoRemoteId, function (err, video) { - if (err || !video) { - if (!err) err = new Error('video not found') +async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { + const options = { + arguments: [ reportData, fromPod ], + errorMessage: 'Cannot create remote abuse video with many retries.' + } - logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId }) - // Do not return the error, continue the process - return callback(null) - } + await retryTransactionWrapper(reportAbuseRemoteVideo, options) +} - logger.debug('Reporting remote abuse for video %s.', video.id) +async function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { + logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID) + await db.sequelize.transaction(async t => { + const videoInstance = await fetchLocalVideoByUUID(reportData.videoUUID, t) const videoAbuseData = { reporterUsername: reportData.reporterUsername, reason: reportData.reportReason, reporterPodId: fromPod.id, - videoId: video.id + videoId: videoInstance.id } - db.VideoAbuse.create(videoAbuseData).asCallback(function (err) { - if (err) { - logger.error('Cannot create remote abuse video.', { error: err }) - } + await db.VideoAbuse.create(videoAbuseData) - return callback(null) - }) }) + + logger.info('Remote abuse for video uuid %s created', reportData.videoUUID) } -function fetchOwnedVideo (id: string, callback: (err: Error, video?: VideoInstance) => void) { - db.Video.load(id, function (err, video) { - if (err || !video) { - if (!err) err = new Error('video not found') +async function fetchLocalVideoByUUID (id: string, t: Sequelize.Transaction) { + try { + const video = await db.Video.loadLocalVideoByUUID(id, t) - logger.error('Cannot load owned video from id.', { error: err, id }) - return callback(err) - } + if (!video) throw new Error('Video ' + id + ' not found') - return callback(null, video) - }) + return video + } catch (err) { + logger.error('Cannot load owned video from id.', { error: err.stack, id }) + throw err + } } -function fetchRemoteVideo (podHost: string, remoteId: string, callback: (err: Error, video?: VideoInstance) => void) { - db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) { - if (err || !video) { - if (!err) err = new Error('video not found') +async function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) { + try { + const video = await db.Video.loadByHostAndUUID(podHost, uuid, t) + if (!video) throw new Error('Video not found') - logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId }) - return callback(err) - } - - return callback(null, video) - }) + return video + } catch (err) { + logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid }) + throw err + } }