import * as safeBuffer from 'safe-buffer'
const Buffer = safeBuffer.Buffer
-import * as createTorrent from 'create-torrent'
import * as ffmpeg from 'fluent-ffmpeg'
-import * as fs from 'fs'
import * as magnetUtil from 'magnet-uri'
import { map, values } from 'lodash'
-import { parallel, series } from 'async'
import * as parseTorrent from 'parse-torrent'
import { join } from 'path'
import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
import { database as db } from '../../initializers/database'
-import { VideoTagInstance } from './video-tag-interface'
+import { TagInstance } from './tag-interface'
import {
logger,
isVideoNameValid,
isVideoNSFWValid,
isVideoDescriptionValid,
isVideoInfoHashValid,
- isVideoDurationValid
+ isVideoDurationValid,
+ readFileBufferPromise,
+ unlinkPromise,
+ renamePromise,
+ writeFilePromise,
+ createTorrentPromise
} from '../../helpers'
import {
CONSTRAINTS_FIELDS,
import { addMethodsToModel, getSort } from '../utils'
import {
- VideoClass,
VideoInstance,
VideoAttributes,
toFormatedJSON,
toAddRemoteJSON,
toUpdateRemoteJSON,
- transcodeVideofile,
+ transcodeVideofile
]
addMethodsToModel(Video, classMethods, instanceMethods)
}
function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
- return new Promise(function (resolve, reject) {
+ if (video.isOwned()) {
+ const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
const tasks = []
- if (video.isOwned()) {
- const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
-
- tasks.push(
- function createVideoTorrent (callback) {
- createTorrentFromVideo(video, videoPath, callback)
- },
-
- function createVideoThumbnail (callback) {
- createThumbnail(video, videoPath, callback)
- },
-
- function createVideoPreview (callback) {
- createPreview(video, videoPath, callback)
- }
- )
-
- if (CONFIG.TRANSCODING.ENABLED === true) {
- tasks.push(
- function createVideoTranscoderJob (callback) {
- const dataInput = {
- id: video.id
- }
+ tasks.push(
+ createTorrentFromVideo(video, videoPath),
+ createThumbnail(video, videoPath),
+ createPreview(video, videoPath)
+ )
- JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput, callback)
- }
- )
+ if (CONFIG.TRANSCODING.ENABLED === true) {
+ const dataInput = {
+ id: video.id
}
- return parallel(tasks, function (err) {
- if (err) return reject(err)
-
- return resolve()
- })
+ tasks.push(
+ JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput)
+ )
}
- return resolve()
- })
+ return Promise.all(tasks)
+ }
+
+ return Promise.resolve()
}
function afterDestroy (video: VideoInstance) {
- return new Promise(function (resolve, reject) {
- const tasks = []
-
- tasks.push(
- function (callback) {
- removeThumbnail(video, callback)
- }
- )
+ const tasks = []
- if (video.isOwned()) {
- tasks.push(
- function removeVideoFile (callback) {
- removeFile(video, callback)
- },
-
- function removeVideoTorrent (callback) {
- removeTorrent(video, callback)
- },
-
- function removeVideoPreview (callback) {
- removePreview(video, callback)
- },
-
- function notifyFriends (callback) {
- const params = {
- remoteId: video.id
- }
-
- removeVideoToFriends(params)
+ tasks.push(
+ removeThumbnail(video)
+ )
- return callback()
- }
- )
+ if (video.isOwned()) {
+ const removeVideoToFriendsParams = {
+ remoteId: video.id
}
- parallel(tasks, function (err) {
- if (err) return reject(err)
+ tasks.push(
+ removeFile(video),
+ removeTorrent(video),
+ removePreview(video),
+ removeVideoToFriends(removeVideoToFriendsParams)
+ )
+ }
- return resolve()
- })
- })
+ return Promise.all(tasks)
}
// ------------------------------ METHODS ------------------------------
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
- tags: map<VideoTagInstance, string>(this.Tags, 'name'),
+ tags: map<TagInstance, string>(this.Tags, 'name'),
thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
createdAt: this.createdAt,
updatedAt: this.updatedAt
return json
}
-toAddRemoteJSON = function (this: VideoInstance, callback: VideoMethods.ToAddRemoteJSONCallback) {
+toAddRemoteJSON = function (this: VideoInstance) {
// Get thumbnail data to send to the other pod
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
- fs.readFile(thumbnailPath, (err, thumbnailData) => {
- if (err) {
- logger.error('Cannot read the thumbnail of the video')
- return callback(err)
- }
+ return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
const remoteVideo = {
name: this.name,
category: this.category,
author: this.Author.name,
duration: this.duration,
thumbnailData: thumbnailData.toString('binary'),
- tags: map<VideoTagInstance, string>(this.Tags, 'name'),
+ tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
extname: this.extname,
dislikes: this.dislikes
}
- return callback(null, remoteVideo)
+ return remoteVideo
})
}
remoteId: this.id,
author: this.Author.name,
duration: this.duration,
- tags: map<VideoTagInstance, string>(this.Tags, 'name'),
+ tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
extname: this.extname,
return json
}
-transcodeVideofile = function (this: VideoInstance, finalCallback: VideoMethods.TranscodeVideofileCallback) {
+transcodeVideofile = function (this: VideoInstance) {
const video = this
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const videoInputPath = join(videosDirectory, video.getVideoFilename())
const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
- ffmpeg(videoInputPath)
- .output(videoOutputPath)
- .videoCodec('libx264')
- .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
- .outputOption('-movflags faststart')
- .on('error', finalCallback)
- .on('end', function () {
- series([
- function removeOldFile (callback) {
- fs.unlink(videoInputPath, callback)
- },
-
- function moveNewFile (callback) {
- // Important to do this before getVideoFilename() to take in account the new file extension
- video.set('extname', newExtname)
-
- const newVideoPath = join(videosDirectory, video.getVideoFilename())
- fs.rename(videoOutputPath, newVideoPath, callback)
- },
-
- function torrent (callback) {
- const newVideoPath = join(videosDirectory, video.getVideoFilename())
- createTorrentFromVideo(video, newVideoPath, callback)
- },
-
- function videoExtension (callback) {
- video.save().asCallback(callback)
- }
-
- ], function (err: Error) {
- if (err) {
- // Autodesctruction...
- video.destroy().asCallback(function (err) {
- if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err })
+ return new Promise<void>((res, rej) => {
+ ffmpeg(videoInputPath)
+ .output(videoOutputPath)
+ .videoCodec('libx264')
+ .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
+ .outputOption('-movflags faststart')
+ .on('error', rej)
+ .on('end', () => {
+
+ return unlinkPromise(videoInputPath)
+ .then(() => {
+ // Important to do this before getVideoFilename() to take in account the new file extension
+ video.set('extname', newExtname)
+
+ const newVideoPath = join(videosDirectory, video.getVideoFilename())
+ return renamePromise(videoOutputPath, newVideoPath)
})
+ .then(() => {
+ const newVideoPath = join(videosDirectory, video.getVideoFilename())
+ return createTorrentFromVideo(video, newVideoPath)
+ })
+ .then(() => {
+ return video.save()
+ })
+ .then(() => {
+ return res()
+ })
+ .catch(err => {
+ // Autodesctruction...
+ video.destroy().asCallback(function (err) {
+ if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err })
+ })
- return finalCallback(err)
- }
-
- return finalCallback(null)
+ return rej(err)
+ })
})
- })
- .run()
+ .run()
+ })
}
// ------------------------------ STATICS ------------------------------
-generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string, callback: VideoMethods.GenerateThumbnailFromDataCallback) {
+generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
// Creating the thumbnail for a remote video
const thumbnailName = video.getThumbnailName()
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
- fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) {
- if (err) return callback(err)
-
- return callback(null, thumbnailName)
+ return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
+ return thumbnailName
})
}
-getDurationFromFile = function (videoPath: string, callback: VideoMethods.GetDurationFromFileCallback) {
- ffmpeg.ffprobe(videoPath, function (err, metadata) {
- if (err) return callback(err)
+getDurationFromFile = function (videoPath: string) {
+ return new Promise<number>((res, rej) => {
+ ffmpeg.ffprobe(videoPath, function (err, metadata) {
+ if (err) return rej(err)
- return callback(null, Math.floor(metadata.format.duration))
+ return res(Math.floor(metadata.format.duration))
+ })
})
}
-list = function (callback: VideoMethods.ListCallback) {
- return Video.findAll().asCallback(callback)
+list = function () {
+ return Video.findAll()
}
-listForApi = function (start: number, count: number, sort: string, callback: VideoMethods.ListForApiCallback) {
+listForApi = function (start: number, count: number, sort: string) {
// Exclude Blakclisted videos from the list
const query = {
distinct: true,
where: createBaseVideosWhere()
}
- return Video.findAndCountAll(query).asCallback(function (err, result) {
- if (err) return callback(err)
-
- return callback(null, result.rows, result.count)
+ return Video.findAndCountAll(query).then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
})
}
-loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback: VideoMethods.LoadByHostAndRemoteIdCallback) {
+loadByHostAndRemoteId = function (fromHost: string, remoteId: string) {
const query = {
where: {
remoteId: remoteId
]
}
- return Video.findOne(query).asCallback(callback)
+ return Video.findOne(query)
}
-listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAndPopulateAuthorAndTagsCallback) {
+listOwnedAndPopulateAuthorAndTags = function () {
// If remoteId is null this is *our* video
const query = {
where: {
include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ]
}
- return Video.findAll(query).asCallback(callback)
+ return Video.findAll(query)
}
-listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedByAuthorCallback) {
+listOwnedByAuthor = function (author: string) {
const query = {
where: {
remoteId: null
]
}
- return Video.findAll(query).asCallback(callback)
+ return Video.findAll(query)
}
-load = function (id: string, callback: VideoMethods.LoadCallback) {
- return Video.findById(id).asCallback(callback)
+load = function (id: string) {
+ return Video.findById(id)
}
-loadAndPopulateAuthor = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorCallback) {
+loadAndPopulateAuthor = function (id: string) {
const options = {
include: [ Video['sequelize'].models.Author ]
}
- return Video.findById(id, options).asCallback(callback)
+ return Video.findById(id, options)
}
-loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorAndPodAndTagsCallback) {
+loadAndPopulateAuthorAndPodAndTags = function (id: string) {
const options = {
include: [
{
]
}
- return Video.findById(id, options).asCallback(callback)
+ return Video.findById(id, options)
}
-searchAndPopulateAuthorAndPodAndTags = function (
- value: string,
- field: string,
- start: number,
- count: number,
- sort: string,
- callback: VideoMethods.SearchAndPopulateAuthorAndPodAndTagsCallback
-) {
+searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
const podInclude: any = {
model: Video['sequelize'].models.Pod,
required: false
} else if (field === 'tags') {
const escapedValue = Video['sequelize'].escape('%' + value + '%')
query.where.id.$in = Video['sequelize'].literal(
- '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
+ `(SELECT "VideoTags"."videoId"
+ FROM "Tags"
+ INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
+ WHERE name ILIKE ${escapedValue}
+ )`
)
} else if (field === 'host') {
// FIXME: Include our pod? (not stored in the database)
podInclude.where = {
host: {
- $like: '%' + value + '%'
+ $iLike: '%' + value + '%'
}
}
podInclude.required = true
} else if (field === 'author') {
authorInclude.where = {
name: {
- $like: '%' + value + '%'
+ $iLike: '%' + value + '%'
}
}
// authorInclude.or = true
} else {
query.where[field] = {
- $like: '%' + value + '%'
+ $iLike: '%' + value + '%'
}
}
authorInclude, tagInclude
]
- if (tagInclude.where) {
- // query.include.push([ Video['sequelize'].models.Tag ])
- }
-
- return Video.findAndCountAll(query).asCallback(function (err, result) {
- if (err) return callback(err)
-
- return callback(null, result.rows, result.count)
+ return Video.findAndCountAll(query).then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
})
}
}
}
-function removeThumbnail (video: VideoInstance, callback: (err: Error) => void) {
+function removeThumbnail (video: VideoInstance) {
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
- fs.unlink(thumbnailPath, callback)
+ return unlinkPromise(thumbnailPath)
}
-function removeFile (video: VideoInstance, callback: (err: Error) => void) {
+function removeFile (video: VideoInstance) {
const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
- fs.unlink(filePath, callback)
+ return unlinkPromise(filePath)
}
-function removeTorrent (video: VideoInstance, callback: (err: Error) => void) {
+function removeTorrent (video: VideoInstance) {
const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
- fs.unlink(torrenPath, callback)
+ return unlinkPromise(torrenPath)
}
-function removePreview (video: VideoInstance, callback: (err: Error) => void) {
+function removePreview (video: VideoInstance) {
// Same name than video thumnail
- fs.unlink(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback)
+ return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName())
}
-function createTorrentFromVideo (video: VideoInstance, videoPath: string, callback: (err: Error) => void) {
+function createTorrentFromVideo (video: VideoInstance, videoPath: string) {
const options = {
announceList: [
[ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
]
}
- createTorrent(videoPath, options, function (err, torrent) {
- if (err) return callback(err)
-
- const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
- fs.writeFile(filePath, torrent, function (err) {
- if (err) return callback(err)
-
+ return createTorrentPromise(videoPath, options)
+ .then(torrent => {
+ const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
+ return writeFilePromise(filePath, torrent).then(() => torrent)
+ })
+ .then(torrent => {
const parsedTorrent = parseTorrent(torrent)
video.set('infoHash', parsedTorrent.infoHash)
- video.validate().asCallback(callback)
+ return video.validate()
})
- })
}
-function createPreview (video: VideoInstance, videoPath: string, callback: (err: Error) => void) {
- generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null, callback)
+function createPreview (video: VideoInstance, videoPath: string) {
+ return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null)
}
-function createThumbnail (video: VideoInstance, videoPath: string, callback: (err: Error) => void) {
- generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE, callback)
+function createThumbnail (video: VideoInstance, videoPath: string) {
+ return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE)
}
-type GenerateImageCallback = (err: Error, imageName: string) => void
-function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string, callback?: GenerateImageCallback) {
+function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
const options: any = {
filename: imageName,
count: 1,
options.size = size
}
- ffmpeg(videoPath)
- .on('error', callback)
- .on('end', function () {
- callback(null, imageName)
- })
- .thumbnail(options)
+ return new Promise<string>((res, rej) => {
+ ffmpeg(videoPath)
+ .on('error', rej)
+ .on('end', function () {
+ return res(imageName)
+ })
+ .thumbnail(options)
+ })
}
-function removeFromBlacklist (video: VideoInstance, callback: (err: Error) => void) {
+function removeFromBlacklist (video: VideoInstance) {
// Find the blacklisted video
- db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) {
- // If an error occured, stop here
- if (err) {
- logger.error('Error when fetching video from blacklist.', { error: err })
- return callback(err)
+ return db.BlacklistedVideo.loadByVideoId(video.id).then(video => {
+ // Not found the video, skip
+ if (!video) {
+ return null
}
// If we found the video, remove it from the blacklist
- if (video) {
- video.destroy().asCallback(callback)
- } else {
- // If haven't found it, simply ignore it and do nothing
- return callback(null)
- }
+ return video.destroy()
})
}