]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video.js
Server: make a basic "quick and dirty update" for videos
[github/Chocobozzz/PeerTube.git] / server / models / video.js
index 04478c8d74202965ec4e227493d2f9c4b6f3dc1b..daa273845f5610de2b90f85df4a405a2cda2cb59 100644 (file)
@@ -1,5 +1,6 @@
 'use strict'
 
+const Buffer = require('safe-buffer').Buffer
 const createTorrent = require('create-torrent')
 const ffmpeg = require('fluent-ffmpeg')
 const fs = require('fs')
@@ -8,54 +9,123 @@ const map = require('lodash/map')
 const parallel = require('async/parallel')
 const parseTorrent = require('parse-torrent')
 const pathUtils = require('path')
+const values = require('lodash/values')
 
 const constants = require('../initializers/constants')
 const logger = require('../helpers/logger')
+const friends = require('../lib/friends')
 const modelUtils = require('./utils')
+const customVideosValidators = require('../helpers/custom-validators').videos
 
 // ---------------------------------------------------------------------------
 
 module.exports = function (sequelize, DataTypes) {
-  // TODO: add indexes on searchable columns
   const Video = sequelize.define('Video',
     {
       id: {
         type: DataTypes.UUID,
         defaultValue: DataTypes.UUIDV4,
-        primaryKey: true
+        primaryKey: true,
+        validate: {
+          isUUID: 4
+        }
       },
       name: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          nameValid: function (value) {
+            const res = customVideosValidators.isVideoNameValid(value)
+            if (res === false) throw new Error('Video name is not valid.')
+          }
+        }
       },
       extname: {
-        // TODO: enum?
-        type: DataTypes.STRING
+        type: DataTypes.ENUM(values(constants.CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
+        allowNull: false
       },
       remoteId: {
-        type: DataTypes.UUID
+        type: DataTypes.UUID,
+        allowNull: true,
+        validate: {
+          isUUID: 4
+        }
       },
       description: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          descriptionValid: function (value) {
+            const res = customVideosValidators.isVideoDescriptionValid(value)
+            if (res === false) throw new Error('Video description is not valid.')
+          }
+        }
       },
       infoHash: {
-        type: DataTypes.STRING
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          infoHashValid: function (value) {
+            const res = customVideosValidators.isVideoInfoHashValid(value)
+            if (res === false) throw new Error('Video info hash is not valid.')
+          }
+        }
       },
       duration: {
-        type: DataTypes.INTEGER
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        validate: {
+          durationValid: function (value) {
+            const res = customVideosValidators.isVideoDurationValid(value)
+            if (res === false) throw new Error('Video duration is not valid.')
+          }
+        }
+      },
+      views: {
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        defaultValue: 0,
+        validate: {
+          min: 0,
+          isInt: true
+        }
       }
     },
     {
+      indexes: [
+        {
+          fields: [ 'authorId' ]
+        },
+        {
+          fields: [ 'remoteId' ]
+        },
+        {
+          fields: [ 'name' ]
+        },
+        {
+          fields: [ 'createdAt' ]
+        },
+        {
+          fields: [ 'duration' ]
+        },
+        {
+          fields: [ 'infoHash' ]
+        },
+        {
+          fields: [ 'views' ]
+        }
+      ],
       classMethods: {
         associate,
 
-        generateThumbnailFromBase64,
+        generateThumbnailFromData,
         getDurationFromFile,
         list,
         listForApi,
-        listByHostAndRemoteId,
         listOwnedAndPopulateAuthorAndTags,
         listOwnedByAuthor,
         load,
+        loadByHostAndRemoteId,
         loadAndPopulateAuthor,
         loadAndPopulateAuthorAndPodAndTags,
         searchAndPopulateAuthorAndPodAndTags
@@ -68,9 +138,11 @@ module.exports = function (sequelize, DataTypes) {
         getTorrentName,
         isOwned,
         toFormatedJSON,
-        toRemoteJSON
+        toAddRemoteJSON,
+        toUpdateRemoteJSON
       },
       hooks: {
+        beforeValidate,
         beforeCreate,
         afterDestroy
       }
@@ -80,13 +152,15 @@ module.exports = function (sequelize, DataTypes) {
   return Video
 }
 
-// TODO: Validation
-// VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
-// VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
-// VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid)
-// VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
-// VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
-// VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
+function beforeValidate (video, options, next) {
+  // Put a fake infoHash if it does not exists yet
+  if (video.isOwned() && !video.infoHash) {
+    // 40 hexa length
+    video.infoHash = '0123456789abcdef0123456789abcdef01234567'
+  }
+
+  return next(null)
+}
 
 function beforeCreate (video, options, next) {
   const tasks = []
@@ -95,8 +169,7 @@ function beforeCreate (video, options, next) {
     const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
 
     tasks.push(
-      // TODO: refractoring
-      function (callback) {
+      function createVideoTorrent (callback) {
         const options = {
           announceList: [
             [ constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
@@ -109,20 +182,22 @@ function beforeCreate (video, options, next) {
         createTorrent(videoPath, options, function (err, torrent) {
           if (err) return callback(err)
 
-          fs.writeFile(constants.CONFIG.STORAGE.TORRENTS_DIR + video.getTorrentName(), torrent, function (err) {
+          const filePath = pathUtils.join(constants.CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
+          fs.writeFile(filePath, torrent, function (err) {
             if (err) return callback(err)
 
             const parsedTorrent = parseTorrent(torrent)
-            video.infoHash = parsedTorrent.infoHash
-
-            callback(null)
+            video.set('infoHash', parsedTorrent.infoHash)
+            video.validate().asCallback(callback)
           })
         })
       },
-      function (callback) {
+
+      function createVideoThumbnail (callback) {
         createThumbnail(video, videoPath, callback)
       },
-      function (callback) {
+
+      function createVIdeoPreview (callback) {
         createPreview(video, videoPath, callback)
       }
     )
@@ -144,14 +219,26 @@ function afterDestroy (video, options, next) {
 
   if (video.isOwned()) {
     tasks.push(
-      function (callback) {
+      function removeVideoFile (callback) {
         removeFile(video, callback)
       },
-      function (callback) {
+
+      function removeVideoTorrent (callback) {
         removeTorrent(video, callback)
       },
-      function (callback) {
+
+      function removeVideoPreview (callback) {
         removePreview(video, callback)
+      },
+
+      function removeVideoToFriends (callback) {
+        const params = {
+          remoteId: video.id
+        }
+
+        friends.removeVideoToFriends(params)
+
+        return callback()
       }
     )
   }
@@ -175,6 +262,14 @@ function associate (models) {
     through: models.VideoTag,
     onDelete: 'cascade'
   })
+
+  this.hasMany(models.VideoAbuse, {
+    foreignKey: {
+      name: 'videoId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
 }
 
 function generateMagnetUri () {
@@ -253,18 +348,20 @@ function toFormatedJSON () {
     magnetUri: this.generateMagnetUri(),
     author: this.Author.name,
     duration: this.duration,
+    views: this.views,
     tags: map(this.Tags, 'name'),
-    thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(),
-    createdAt: this.createdAt
+    thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
+    createdAt: this.createdAt,
+    updatedAt: this.updatedAt
   }
 
   return json
 }
 
-function toRemoteJSON (callback) {
+function toAddRemoteJSON (callback) {
   const self = this
 
-  // Convert thumbnail to base64
+  // Get thumbnail data to send to the other pod
   const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
   fs.readFile(thumbnailPath, function (err, thumbnailData) {
     if (err) {
@@ -279,9 +376,10 @@ function toRemoteJSON (callback) {
       remoteId: self.id,
       author: self.Author.name,
       duration: self.duration,
-      thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
+      thumbnailData: thumbnailData.toString('binary'),
       tags: map(self.Tags, 'name'),
       createdAt: self.createdAt,
+      updatedAt: self.updatedAt,
       extname: self.extname
     }
 
@@ -289,14 +387,31 @@ function toRemoteJSON (callback) {
   })
 }
 
+function toUpdateRemoteJSON (callback) {
+  const json = {
+    name: this.name,
+    description: this.description,
+    infoHash: this.infoHash,
+    remoteId: this.id,
+    author: this.Author.name,
+    duration: this.duration,
+    tags: map(this.Tags, 'name'),
+    createdAt: this.createdAt,
+    updatedAt: this.updatedAt,
+    extname: this.extname
+  }
+
+  return json
+}
+
 // ------------------------------ STATICS ------------------------------
 
-function generateThumbnailFromBase64 (video, thumbnailData, callback) {
+function generateThumbnailFromData (video, thumbnailData, callback) {
   // Creating the thumbnail for a remote video
 
   const thumbnailName = video.getThumbnailName()
-  const thumbnailPath = constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName
-  fs.writeFile(thumbnailPath, thumbnailData, { encoding: 'base64' }, function (err) {
+  const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
+  fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) {
     if (err) return callback(err)
 
     return callback(null, thumbnailName)
@@ -312,7 +427,7 @@ function getDurationFromFile (videoPath, callback) {
 }
 
 function list (callback) {
-  return this.find().asCallback()
+  return this.findAll().asCallback(callback)
 }
 
 function listForApi (start, count, sort, callback) {
@@ -338,7 +453,7 @@ function listForApi (start, count, sort, callback) {
   })
 }
 
-function listByHostAndRemoteId (fromHost, remoteId, callback) {
+function loadByHostAndRemoteId (fromHost, remoteId, callback) {
   const query = {
     where: {
       remoteId: remoteId
@@ -359,7 +474,7 @@ function listByHostAndRemoteId (fromHost, remoteId, callback) {
     ]
   }
 
-  return this.findAll(query).asCallback(callback)
+  return this.findOne(query).asCallback(callback)
 }
 
 function listOwnedAndPopulateAuthorAndTags (callback) {
@@ -496,15 +611,18 @@ function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort,
 // ---------------------------------------------------------------------------
 
 function removeThumbnail (video, callback) {
-  fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.getThumbnailName(), callback)
+  const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
+  fs.unlink(thumbnailPath, callback)
 }
 
 function removeFile (video, callback) {
-  fs.unlink(constants.CONFIG.STORAGE.VIDEOS_DIR + video.getVideoFilename(), callback)
+  const filePath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
+  fs.unlink(filePath, callback)
 }
 
 function removeTorrent (video, callback) {
-  fs.unlink(constants.CONFIG.STORAGE.TORRENTS_DIR + video.getTorrentName(), callback)
+  const torrenPath = pathUtils.join(constants.CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
+  fs.unlink(torrenPath, callback)
 }
 
 function removePreview (video, callback) {