X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo.js;h=4c197a83502d8aad01a2c429c42d81e48929a9aa;hb=55fa55a9be566cca2ba95322f2ae23b434aed62a;hp=8ef07c9e68bda59383b6b6f8f77c6656d2e2c243;hpb=feb4bdfd9b46e87aadfa7c0d5338cde887d1f58c;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video.js b/server/models/video.js index 8ef07c9e6..4c197a835 100644 --- a/server/models/video.js +++ b/server/models/video.js @@ -1,65 +1,122 @@ 'use strict' +const Buffer = require('safe-buffer').Buffer const createTorrent = require('create-torrent') const ffmpeg = require('fluent-ffmpeg') const fs = require('fs') const magnetUtil = require('magnet-uri') +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 - }, - tags: { - type: DataTypes.ARRAY(DataTypes.STRING) + 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.') + } + } } }, { + indexes: [ + { + fields: [ 'authorId' ] + }, + { + fields: [ 'remoteId' ] + }, + { + fields: [ 'name' ] + }, + { + fields: [ 'createdAt' ] + }, + { + fields: [ 'duration' ] + }, + { + fields: [ 'infoHash' ] + } + ], classMethods: { associate, - generateThumbnailFromBase64, + generateThumbnailFromData, getDurationFromFile, + list, listForApi, - listByHostAndRemoteId, - listOwnedAndPopulateAuthor, + listOwnedAndPopulateAuthorAndTags, listOwnedByAuthor, load, + loadByHostAndRemoteId, loadAndPopulateAuthor, - loadAndPopulateAuthorAndPod, - searchAndPopulateAuthorAndPod + loadAndPopulateAuthorAndPodAndTags, + searchAndPopulateAuthorAndPodAndTags }, instanceMethods: { generateMagnetUri, @@ -69,9 +126,11 @@ module.exports = function (sequelize, DataTypes) { getTorrentName, isOwned, toFormatedJSON, - toRemoteJSON + toAddRemoteJSON, + toUpdateRemoteJSON }, hooks: { + beforeValidate, beforeCreate, afterDestroy } @@ -81,13 +140,14 @@ 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) { + if (video.isOwned()) { + // 40 hexa length + video.infoHash = '0123456789abcdef0123456789abcdef01234567' + } + + return next(null) +} function beforeCreate (video, options, next) { const tasks = [] @@ -114,9 +174,8 @@ function beforeCreate (video, options, next) { if (err) return callback(err) const parsedTorrent = parseTorrent(torrent) - video.infoHash = parsedTorrent.infoHash - - callback(null) + video.set('infoHash', parsedTorrent.infoHash) + video.validate().asCallback(callback) }) }) }, @@ -148,11 +207,24 @@ function afterDestroy (video, options, next) { function (callback) { removeFile(video, callback) }, + function (callback) { removeTorrent(video, callback) }, + function (callback) { removePreview(video, callback) + }, + + function (callback) { + const params = { + name: video.name, + remoteId: video.id + } + + friends.removeVideoToFriends(params) + + return callback() } ) } @@ -170,6 +242,20 @@ function associate (models) { }, onDelete: 'cascade' }) + + this.belongsToMany(models.Tag, { + foreignKey: 'videoId', + through: models.VideoTag, + onDelete: 'cascade' + }) + + this.hasMany(models.VideoAbuse, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) } function generateMagnetUri () { @@ -248,18 +334,19 @@ function toFormatedJSON () { magnetUri: this.generateMagnetUri(), author: this.Author.name, duration: this.duration, - tags: this.tags, + tags: map(this.Tags, 'name'), thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), - createdAt: this.createdAt + 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) { @@ -274,9 +361,10 @@ function toRemoteJSON (callback) { remoteId: self.id, author: self.Author.name, duration: self.duration, - thumbnailBase64: new Buffer(thumbnailData).toString('base64'), - tags: self.tags, + thumbnailData: thumbnailData.toString('binary'), + tags: map(self.Tags, 'name'), createdAt: self.createdAt, + updatedAt: self.updatedAt, extname: self.extname } @@ -284,14 +372,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) { + fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { if (err) return callback(err) return callback(null, thumbnailName) @@ -306,16 +411,23 @@ function getDurationFromFile (videoPath, callback) { }) } +function list (callback) { + return this.find().asCallback() +} + function listForApi (start, count, sort, callback) { const query = { offset: start, limit: count, - order: [ modelUtils.getSort(sort) ], + distinct: true, // For the count, a video can have many tags + order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ], include: [ { model: this.sequelize.models.Author, - include: [ this.sequelize.models.Pod ] - } + include: [ { model: this.sequelize.models.Pod, required: false } ] + }, + + this.sequelize.models.Tag ] } @@ -326,7 +438,7 @@ function listForApi (start, count, sort, callback) { }) } -function listByHostAndRemoteId (fromHost, remoteId, callback) { +function loadByHostAndRemoteId (fromHost, remoteId, callback) { const query = { where: { remoteId: remoteId @@ -337,6 +449,7 @@ function listByHostAndRemoteId (fromHost, remoteId, callback) { include: [ { model: this.sequelize.models.Pod, + required: true, where: { host: fromHost } @@ -346,16 +459,16 @@ function listByHostAndRemoteId (fromHost, remoteId, callback) { ] } - return this.findAll(query).asCallback(callback) + return this.findOne(query).asCallback(callback) } -function listOwnedAndPopulateAuthor (callback) { +function listOwnedAndPopulateAuthorAndTags (callback) { // If remoteId is null this is *our* video const query = { where: { remoteId: null }, - include: [ this.sequelize.models.Author ] + include: [ this.sequelize.models.Author, this.sequelize.models.Tag ] } return this.findAll(query).asCallback(callback) @@ -391,23 +504,26 @@ function loadAndPopulateAuthor (id, callback) { return this.findById(id, options).asCallback(callback) } -function loadAndPopulateAuthorAndPod (id, callback) { +function loadAndPopulateAuthorAndPodAndTags (id, callback) { const options = { include: [ { model: this.sequelize.models.Author, - include: [ this.sequelize.models.Pod ] - } + include: [ { model: this.sequelize.models.Pod, required: false } ] + }, + this.sequelize.models.Tag ] } return this.findById(id, options).asCallback(callback) } -function searchAndPopulateAuthorAndPod (value, field, start, count, sort, callback) { +function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, callback) { const podInclude = { - model: this.sequelize.models.Pod + model: this.sequelize.models.Pod, + required: false } + const authorInclude = { model: this.sequelize.models.Author, include: [ @@ -415,55 +531,61 @@ function searchAndPopulateAuthorAndPod (value, field, start, count, sort, callba ] } + const tagInclude = { + model: this.sequelize.models.Tag + } + const query = { where: {}, - include: [ - authorInclude - ], offset: start, limit: count, - order: [ modelUtils.getSort(sort) ] + distinct: true, // For the count, a video can have many tags + order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ] } - // TODO: include our pod for podHost searches (we are not stored in the database) // Make an exact search with the magnet if (field === 'magnetUri') { const infoHash = magnetUtil.decode(value).infoHash query.where.infoHash = infoHash } else if (field === 'tags') { - query.where[field] = value - } else if (field === 'host') { - const whereQuery = { - '$Author.Pod.host$': { - $like: '%' + value + '%' + const escapedValue = this.sequelize.escape('%' + value + '%') + query.where = { + id: { + $in: this.sequelize.literal( + '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' + ) } } - - // Include our pod? (not stored in the database) - if (constants.CONFIG.WEBSERVER.HOST.indexOf(value) !== -1) { - query.where = { - $or: [ - whereQuery, - { - remoteId: null - } - ] + } else if (field === 'host') { + // FIXME: Include our pod? (not stored in the database) + podInclude.where = { + host: { + $like: '%' + value + '%' } - } else { - query.where = whereQuery } + podInclude.required = true } else if (field === 'author') { - query.where = { - '$Author.name$': { + authorInclude.where = { + name: { $like: '%' + value + '%' } } + + // authorInclude.or = true } else { query.where[field] = { $like: '%' + value + '%' } } + query.include = [ + authorInclude, tagInclude + ] + + if (tagInclude.where) { + // query.include.push([ this.sequelize.models.Tag ]) + } + return this.findAndCountAll(query).asCallback(function (err, result) { if (err) return callback(err)