'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,
getTorrentName,
isOwned,
toFormatedJSON,
- toRemoteJSON
+ toAddRemoteJSON,
+ toUpdateRemoteJSON
},
hooks: {
+ beforeValidate,
beforeCreate,
afterDestroy
}
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 = []
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) {
removeFile(video, callback)
},
+
function (callback) {
removeTorrent(video, callback)
},
+
function (callback) {
removePreview(video, callback)
+ },
+
+ function (callback) {
+ const params = {
+ remoteId: video.id
+ }
+
+ friends.removeVideoToFriends(params)
+
+ return callback()
}
)
}
},
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 () {
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) {
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
}
})
}
+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)
})
}
+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
]
}
})
}
-function listByHostAndRemoteId (fromHost, remoteId, callback) {
+function loadByHostAndRemoteId (fromHost, remoteId, callback) {
const query = {
where: {
remoteId: remoteId
include: [
{
model: this.sequelize.models.Pod,
+ required: true,
where: {
host: fromHost
}
]
}
- 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)
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: [
]
}
+ 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)