]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/controllers/api/remote/videos.js
Server: implement video views
[github/Chocobozzz/PeerTube.git] / server / controllers / api / remote / videos.js
index 6d768eae885a54d62b6fb306308ba56a090d1fe4..39c9579c1a21fd3c5346b9b6f3c4b5a7184b8656 100644 (file)
@@ -5,12 +5,22 @@ const express = require('express')
 const waterfall = require('async/waterfall')
 
 const db = require('../../../initializers/database')
+const constants = require('../../../initializers/constants')
 const middlewares = require('../../../middlewares')
 const secureMiddleware = middlewares.secure
 const videosValidators = middlewares.validators.remote.videos
 const signatureValidators = middlewares.validators.remote.signature
 const logger = require('../../../helpers/logger')
-const utils = require('../../../helpers/utils')
+const databaseUtils = require('../../../helpers/database-utils')
+
+const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.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
 
 const router = express.Router()
 
@@ -21,6 +31,20 @@ router.post('/',
   remoteVideos
 )
 
+router.post('/qadu',
+  signatureValidators.signature,
+  secureMiddleware.checkSignature,
+  videosValidators.remoteQaduVideos,
+  remoteVideosQadu
+)
+
+router.post('/events',
+  signatureValidators.signature,
+  secureMiddleware.checkSignature,
+  videosValidators.remoteEventsVideos,
+  remoteVideosEvents
+)
+
 // ---------------------------------------------------------------------------
 
 module.exports = router
@@ -36,49 +60,175 @@ function remoteVideos (req, res, next) {
   eachSeries(requests, function (request, callbackEach) {
     const data = request.data
 
-    switch (request.type) {
-      case 'add':
-        addRemoteVideoRetryWrapper(data, fromPod, callbackEach)
-        break
+    // 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)
+    }
 
-      case 'update':
-        updateRemoteVideoRetryWrapper(data, fromPod, callbackEach)
-        break
+    fun.call(this, data, fromPod, callbackEach)
+  }, function (err) {
+    if (err) logger.error('Error managing remote videos.', { error: err })
+  })
+
+  // We don't need to keep the other pod waiting
+  return res.type('json').status(204).end()
+}
 
-      case 'remove':
-        removeRemoteVideo(data, fromPod, callbackEach)
-        break
+function remoteVideosQadu (req, res, next) {
+  const requests = req.body.data
+  const fromPod = res.locals.secure.pod
 
-      case 'report-abuse':
-        reportAbuseRemoteVideo(data, fromPod, callbackEach)
-        break
+  eachSeries(requests, function (request, callbackEach) {
+    const videoData = request.data
 
-      default:
-        logger.error('Unkown remote request type %s.', request.type)
-    }
+    quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod, callbackEach)
   }, function (err) {
     if (err) logger.error('Error managing remote videos.', { error: err })
   })
 
-  // We don't need to keep the other pod waiting
   return res.type('json').status(204).end()
 }
 
-// Handle retries on fail
-function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
-  utils.transactionRetryer(
-    function (callback) {
-      return addRemoteVideo(videoToCreateData, fromPod, callback)
+function remoteVideosEvents (req, res, next) {
+  const requests = req.body.data
+  const fromPod = res.locals.secure.pod
+
+  eachSeries(requests, function (request, callbackEach) {
+    const eventData = request.data
+
+    processVideosEventsRetryWrapper(eventData, fromPod, callbackEach)
+  }, function (err) {
+    if (err) logger.error('Error managing remote videos.', { error: err })
+  })
+
+  return res.type('json').status(204).end()
+}
+
+function processVideosEventsRetryWrapper (eventData, fromPod, finalCallback) {
+  const options = {
+    arguments: [ eventData, fromPod ],
+    errorMessage: 'Cannot process videos events with many retries.'
+  }
+
+  databaseUtils.retryTransactionWrapper(processVideosEvents, options, finalCallback)
+}
+
+function processVideosEvents (eventData, fromPod, finalCallback) {
+  waterfall([
+    databaseUtils.startSerializableTransaction,
+
+    function findVideo (t, callback) {
+      fetchOwnedVideo(eventData.remoteId, function (err, videoInstance) {
+        return callback(err, t, videoInstance)
+      })
     },
-    function (err) {
-      if (err) {
-        logger.error('Cannot insert the remote video with many retries.', { error: err })
-        return finalCallback(err)
+
+    function updateVideoIntoDB (t, videoInstance, callback) {
+      const options = { transaction: t }
+
+      let columnToUpdate
+
+      switch (eventData.eventType) {
+        case constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS:
+          columnToUpdate = 'views'
+          break
+
+        case constants.REQUEST_VIDEO_EVENT_TYPES.LIKES:
+          columnToUpdate = 'likes'
+          break
+
+        case constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
+          columnToUpdate = 'dislikes'
+          break
+
+        default:
+          return callback(new Error('Unknown video event type.'))
       }
 
-      return finalCallback()
+      const query = {}
+      query[columnToUpdate] = eventData.count
+
+      videoInstance.increment(query, options).asCallback(function (err) {
+        return callback(err, t)
+      })
+    },
+
+    databaseUtils.commitTransaction
+
+  ], function (err, t) {
+    if (err) {
+      console.log(err)
+      logger.debug('Cannot process a video event.', { error: err })
+      return databaseUtils.rollbackTransaction(err, t, finalCallback)
     }
-  )
+
+    logger.info('Remote video event processed for video %s.', eventData.remoteId)
+    return finalCallback(null)
+  })
+}
+
+function quickAndDirtyUpdateVideoRetryWrapper (videoData, fromPod, finalCallback) {
+  const options = {
+    arguments: [ videoData, fromPod ],
+    errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
+  }
+
+  databaseUtils.retryTransactionWrapper(quickAndDirtyUpdateVideo, options, finalCallback)
+}
+
+function quickAndDirtyUpdateVideo (videoData, fromPod, finalCallback) {
+  waterfall([
+    databaseUtils.startSerializableTransaction,
+
+    function findVideo (t, callback) {
+      fetchRemoteVideo(fromPod.host, videoData.remoteId, function (err, videoInstance) {
+        return callback(err, t, videoInstance)
+      })
+    },
+
+    function updateVideoIntoDB (t, videoInstance, callback) {
+      const options = { transaction: t }
+
+      if (videoData.views) {
+        videoInstance.set('views', videoData.views)
+      }
+
+      if (videoData.likes) {
+        videoInstance.set('likes', videoData.likes)
+      }
+
+      if (videoData.dislikes) {
+        videoInstance.set('dislikes', videoData.dislikes)
+      }
+
+      videoInstance.save(options).asCallback(function (err) {
+        return callback(err, t)
+      })
+    },
+
+    databaseUtils.commitTransaction
+
+  ], function (err, t) {
+    if (err) {
+      logger.debug('Cannot quick and dirty update the remote video.', { error: err })
+      return databaseUtils.rollbackTransaction(err, t, finalCallback)
+    }
+
+    logger.info('Remote video %s quick and dirty updated', videoData.name)
+    return finalCallback(null)
+  })
+}
+
+// Handle retries on fail
+function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
+  const options = {
+    arguments: [ videoToCreateData, fromPod ],
+    errorMessage: 'Cannot insert the remote video with many retries.'
+  }
+
+  databaseUtils.retryTransactionWrapper(addRemoteVideo, options, finalCallback)
 }
 
 function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
@@ -86,9 +236,15 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
 
   waterfall([
 
-    function startTransaction (callback) {
-      db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
-        return callback(err, t)
+    databaseUtils.startSerializableTransaction,
+
+    function assertRemoteIdAndHostUnique (t, callback) {
+      db.Video.loadByHostAndRemoteId(fromPod.host, videoToCreateData.remoteId, function (err, video) {
+        if (err) return callback(err)
+
+        if (video) return callback(new Error('RemoteId and host pair is not unique.'))
+
+        return callback(null, t)
       })
     },
 
@@ -152,46 +308,37 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
     },
 
     function associateTagsToVideo (t, tagInstances, video, callback) {
-      const options = { transaction: t }
+      const options = {
+        transaction: t
+      }
 
       video.setTags(tagInstances, options).asCallback(function (err) {
         return callback(err, t)
       })
-    }
+    },
+
+    databaseUtils.commitTransaction
 
   ], function (err, t) {
     if (err) {
       // This is just a debug because we will retry the insert
       logger.debug('Cannot insert the remote video.', { error: err })
-
-      // Abort transaction?
-      if (t) t.rollback()
-
-      return finalCallback(err)
+      return databaseUtils.rollbackTransaction(err, t, finalCallback)
     }
 
-    // Commit transaction
-    t.commit()
-
-    return finalCallback()
+    logger.info('Remote video %s inserted.', videoToCreateData.name)
+    return finalCallback(null)
   })
 }
 
 // Handle retries on fail
 function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
-  utils.transactionRetryer(
-    function (callback) {
-      return updateRemoteVideo(videoAttributesToUpdate, fromPod, callback)
-    },
-    function (err) {
-      if (err) {
-        logger.error('Cannot update the remote video with many retries.', { error: err })
-        return finalCallback(err)
-      }
+  const options = {
+    arguments: [ videoAttributesToUpdate, fromPod ],
+    errorMessage: 'Cannot update the remote video with many retries'
+  }
 
-      return finalCallback()
-    }
-  )
+  databaseUtils.retryTransactionWrapper(updateRemoteVideo, options, finalCallback)
 }
 
 function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
@@ -199,14 +346,10 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
 
   waterfall([
 
-    function startTransaction (callback) {
-      db.sequelize.transaction().asCallback(function (err, t) {
-        return callback(err, t)
-      })
-    },
+    databaseUtils.startSerializableTransaction,
 
     function findVideo (t, callback) {
-      fetchVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
+      fetchRemoteVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
         return callback(err, t, videoInstance)
       })
     },
@@ -241,43 +384,48 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
       videoInstance.setTags(tagInstances, options).asCallback(function (err) {
         return callback(err, t)
       })
-    }
+    },
+
+    databaseUtils.commitTransaction
 
   ], function (err, t) {
     if (err) {
       // This is just a debug because we will retry the insert
       logger.debug('Cannot update the remote video.', { error: err })
-
-      // Abort transaction?
-      if (t) t.rollback()
-
-      return finalCallback(err)
+      return databaseUtils.rollbackTransaction(err, t, finalCallback)
     }
 
-    // Commit transaction
-    t.commit()
-
-    return finalCallback()
+    logger.info('Remote video %s updated', videoAttributesToUpdate.name)
+    return finalCallback(null)
   })
 }
 
 function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
   // We need the instance because we have to remove some other stuffs (thumbnail etc)
-  fetchVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
-    if (err) return callback(err)
+  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(callback)
+    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 })
+      }
+
+      return callback(null)
+    })
   })
 }
 
 function reportAbuseRemoteVideo (reportData, fromPod, callback) {
-  db.Video.load(reportData.videoRemoteId, function (err, video) {
+  fetchOwnedVideo(reportData.videoRemoteId, function (err, video) {
     if (err || !video) {
       if (!err) err = new Error('video not found')
 
       logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
-      return callback(err)
+      // Do not return the error, continue the process
+      return callback(null)
     }
 
     logger.debug('Reporting remote abuse for video %s.', video.id)
@@ -289,11 +437,30 @@ function reportAbuseRemoteVideo (reportData, fromPod, callback) {
       videoId: video.id
     }
 
-    db.VideoAbuse.create(videoAbuseData).asCallback(callback)
+    db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
+      if (err) {
+        logger.error('Cannot create remote abuse video.', { error: err })
+      }
+
+      return callback(null)
+    })
+  })
+}
+
+function fetchOwnedVideo (id, callback) {
+  db.Video.load(id, function (err, video) {
+    if (err || !video) {
+      if (!err) err = new Error('video not found')
+
+      logger.error('Cannot load owned video from id.', { error: err, id })
+      return callback(err)
+    }
+
+    return callback(null, video)
   })
 }
 
-function fetchVideo (podHost, remoteId, callback) {
+function fetchRemoteVideo (podHost, remoteId, callback) {
   db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
     if (err || !video) {
       if (!err) err = new Error('video not found')