X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo.ts;h=2ba6cf25f0cc4daa8b137f0c6e8045a506633f64;hb=14d3270f363245d2c83fcc2ac109e39743b5627e;hp=3bb74bf6d658734c26cddf659602eb283248ee6f;hpb=0a6658fdcbd779ada8f3758048c326e997902d5a;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 3bb74bf6d..2ba6cf25f 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1,14 +1,13 @@ import * as safeBuffer from 'safe-buffer' const Buffer = safeBuffer.Buffer -import * as ffmpeg from 'fluent-ffmpeg' import * as magnetUtil from 'magnet-uri' -import { map, values } from 'lodash' +import { map } from 'lodash' import * as parseTorrent from 'parse-torrent' import { join } from 'path' import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' +import { maxBy } from 'lodash' -import { database as db } from '../../initializers/database' import { TagInstance } from './tag-interface' import { logger, @@ -18,16 +17,18 @@ import { isVideoLanguageValid, isVideoNSFWValid, isVideoDescriptionValid, - isVideoInfoHashValid, isVideoDurationValid, readFileBufferPromise, unlinkPromise, renamePromise, writeFilePromise, - createTorrentPromise + createTorrentPromise, + statPromise, + generateImageFromVideoFile, + transcode, + getVideoFileHeight } from '../../helpers' import { - CONSTRAINTS_FIELDS, CONFIG, REMOTE_SCHEME, STATIC_PATHS, @@ -36,7 +37,9 @@ import { VIDEO_LANGUAGES, THUMBNAILS_SIZE } from '../../initializers' -import { JobScheduler, removeVideoToFriends } from '../../lib' +import { removeVideoToFriends } from '../../lib' +import { VideoResolution } from '../../../shared' +import { VideoFileInstance, VideoFileModel } from './video-file-interface' import { addMethodsToModel, getSort } from '../utils' import { @@ -47,19 +50,25 @@ import { } from './video-interface' let Video: Sequelize.Model +let getOriginalFile: VideoMethods.GetOriginalFile let generateMagnetUri: VideoMethods.GenerateMagnetUri let getVideoFilename: VideoMethods.GetVideoFilename let getThumbnailName: VideoMethods.GetThumbnailName let getPreviewName: VideoMethods.GetPreviewName -let getTorrentName: VideoMethods.GetTorrentName +let getTorrentFileName: VideoMethods.GetTorrentFileName let isOwned: VideoMethods.IsOwned -let toFormatedJSON: VideoMethods.ToFormatedJSON +let toFormattedJSON: VideoMethods.ToFormattedJSON let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON -let transcodeVideofile: VideoMethods.TranscodeVideofile +let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile +let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile +let createPreview: VideoMethods.CreatePreview +let createThumbnail: VideoMethods.CreateThumbnail +let getVideoFilePath: VideoMethods.GetVideoFilePath +let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash +let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData -let getDurationFromFile: VideoMethods.GetDurationFromFile let list: VideoMethods.List let listForApi: VideoMethods.ListForApi let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID @@ -71,6 +80,10 @@ let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags +let removeThumbnail: VideoMethods.RemoveThumbnail +let removePreview: VideoMethods.RemovePreview +let removeFile: VideoMethods.RemoveFile +let removeTorrent: VideoMethods.RemoveTorrent export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { Video = sequelize.define('Video', @@ -87,21 +100,17 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da type: DataTypes.STRING, allowNull: false, validate: { - nameValid: function (value) { + nameValid: value => { const res = isVideoNameValid(value) if (res === false) throw new Error('Video name is not valid.') } } }, - extname: { - type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), - allowNull: false - }, category: { type: DataTypes.INTEGER, allowNull: false, validate: { - categoryValid: function (value) { + categoryValid: value => { const res = isVideoCategoryValid(value) if (res === false) throw new Error('Video category is not valid.') } @@ -112,7 +121,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da allowNull: false, defaultValue: null, validate: { - licenceValid: function (value) { + licenceValid: value => { const res = isVideoLicenceValid(value) if (res === false) throw new Error('Video licence is not valid.') } @@ -122,7 +131,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da type: DataTypes.INTEGER, allowNull: true, validate: { - languageValid: function (value) { + languageValid: value => { const res = isVideoLanguageValid(value) if (res === false) throw new Error('Video language is not valid.') } @@ -132,7 +141,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da type: DataTypes.BOOLEAN, allowNull: false, validate: { - nsfwValid: function (value) { + nsfwValid: value => { const res = isVideoNSFWValid(value) if (res === false) throw new Error('Video nsfw attribute is not valid.') } @@ -142,27 +151,17 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da type: DataTypes.STRING, allowNull: false, validate: { - descriptionValid: function (value) { + descriptionValid: value => { const res = isVideoDescriptionValid(value) if (res === false) throw new Error('Video description is not valid.') } } }, - infoHash: { - type: DataTypes.STRING, - allowNull: false, - validate: { - infoHashValid: function (value) { - const res = isVideoInfoHashValid(value) - if (res === false) throw new Error('Video info hash is not valid.') - } - } - }, duration: { type: DataTypes.INTEGER, allowNull: false, validate: { - durationValid: function (value) { + durationValid: value => { const res = isVideoDurationValid(value) if (res === false) throw new Error('Video duration is not valid.') } @@ -215,9 +214,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da { fields: [ 'duration' ] }, - { - fields: [ 'infoHash' ] - }, { fields: [ 'views' ] }, @@ -229,8 +225,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da } ], hooks: { - beforeValidate, - beforeCreate, afterDestroy } } @@ -240,96 +234,46 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da associate, generateThumbnailFromData, - getDurationFromFile, list, listForApi, listOwnedAndPopulateAuthorAndTags, listOwnedByAuthor, load, - loadByUUID, - loadByHostAndUUID, loadAndPopulateAuthor, loadAndPopulateAuthorAndPodAndTags, + loadByHostAndUUID, + loadByUUID, loadByUUIDAndPopulateAuthorAndPodAndTags, - searchAndPopulateAuthorAndPodAndTags, - removeFromBlacklist + searchAndPopulateAuthorAndPodAndTags ] const instanceMethods = [ + createPreview, + createThumbnail, + createTorrentAndSetInfoHash, generateMagnetUri, - getVideoFilename, - getThumbnailName, getPreviewName, - getTorrentName, + getThumbnailName, + getTorrentFileName, + getVideoFilename, + getVideoFilePath, + getOriginalFile, isOwned, - toFormatedJSON, + removeFile, + removePreview, + removeThumbnail, + removeTorrent, toAddRemoteJSON, + toFormattedJSON, toUpdateRemoteJSON, - transcodeVideofile + optimizeOriginalVideofile, + transcodeOriginalVideofile, + getOriginalFileHeight ] addMethodsToModel(Video, classMethods, instanceMethods) return Video } -function beforeValidate (video: VideoInstance) { - // Put a fake infoHash if it does not exists yet - if (video.isOwned() && !video.infoHash) { - // 40 hexa length - video.infoHash = '0123456789abcdef0123456789abcdef01234567' - } -} - -function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { - if (video.isOwned()) { - const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - const tasks = [] - - tasks.push( - createTorrentFromVideo(video, videoPath), - createThumbnail(video, videoPath), - createPreview(video, videoPath) - ) - - if (CONFIG.TRANSCODING.ENABLED === true) { - // Put uuid because we don't have id auto incremented for now - const dataInput = { - videoUUID: video.uuid - } - - tasks.push( - JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput) - ) - } - - return Promise.all(tasks) - } - - return Promise.resolve() -} - -function afterDestroy (video: VideoInstance) { - const tasks = [] - - tasks.push( - removeThumbnail(video) - ) - - if (video.isOwned()) { - const removeVideoToFriendsParams = { - uuid: video.uuid - } - - tasks.push( - removeFile(video), - removeTorrent(video), - removePreview(video), - removeVideoToFriends(removeVideoToFriendsParams) - ) - } - - return Promise.all(tasks) -} - // ------------------------------ METHODS ------------------------------ function associate (models) { @@ -354,37 +298,52 @@ function associate (models) { }, onDelete: 'cascade' }) + + Video.hasMany(models.VideoFile, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) } -generateMagnetUri = function (this: VideoInstance) { - let baseUrlHttp - let baseUrlWs +function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { + const tasks = [] - if (this.isOwned()) { - baseUrlHttp = CONFIG.WEBSERVER.URL - baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT - } else { - baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host - baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host - } + tasks.push( + video.removeThumbnail() + ) - const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() - const announce = [ baseUrlWs + '/tracker/socket' ] - const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ] + if (video.isOwned()) { + const removeVideoToFriendsParams = { + uuid: video.uuid + } - const magnetHash = { - xs, - announce, - urlList, - infoHash: this.infoHash, - name: this.name + tasks.push( + video.removePreview(), + removeVideoToFriends(removeVideoToFriendsParams, options.transaction) + ) + + // Remove physical files and torrents + video.VideoFiles.forEach(file => { + video.removeFile(file), + video.removeTorrent(file) + }) } - return magnetUtil.encode(magnetHash) + return Promise.all(tasks) } -getVideoFilename = function (this: VideoInstance) { - return this.uuid + this.extname +getOriginalFile = function (this: VideoInstance) { + if (Array.isArray(this.VideoFiles) === false) return undefined + + // The original file is the file that have the higher resolution + return maxBy(this.VideoFiles, file => file.resolution) +} + +getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { + return this.uuid + '-' + videoFile.resolution + videoFile.extname } getThumbnailName = function (this: VideoInstance) { @@ -398,16 +357,88 @@ getPreviewName = function (this: VideoInstance) { return this.uuid + extension } -getTorrentName = function (this: VideoInstance) { +getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { const extension = '.torrent' - return this.uuid + extension + return this.uuid + '-' + videoFile.resolution + extension } isOwned = function (this: VideoInstance) { return this.remote === false } -toFormatedJSON = function (this: VideoInstance) { +createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { + return generateImageFromVideoFile( + this.getVideoFilePath(videoFile), + CONFIG.STORAGE.PREVIEWS_DIR, + this.getPreviewName() + ) +} + +createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { + return generateImageFromVideoFile( + this.getVideoFilePath(videoFile), + CONFIG.STORAGE.THUMBNAILS_DIR, + this.getThumbnailName(), + THUMBNAILS_SIZE + ) +} + +getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { + return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) +} + +createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) { + const options = { + announceList: [ + [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] + ], + urlList: [ + CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) + ] + } + + return createTorrentPromise(this.getVideoFilePath(videoFile), options) + .then(torrent => { + const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + logger.info('Creating torrent %s.', filePath) + + return writeFilePromise(filePath, torrent).then(() => torrent) + }) + .then(torrent => { + const parsedTorrent = parseTorrent(torrent) + + videoFile.infoHash = parsedTorrent.infoHash + }) +} + +generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance) { + let baseUrlHttp + let baseUrlWs + + if (this.isOwned()) { + baseUrlHttp = CONFIG.WEBSERVER.URL + baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + } else { + baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host + baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host + } + + const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) + const announce = [ baseUrlWs + '/tracker/socket' ] + const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ] + + const magnetHash = { + xs, + announce, + urlList, + infoHash: videoFile.infoHash, + name: this.name + } + + return magnetUtil.encode(magnetHash) +} + +toFormattedJSON = function (this: VideoInstance) { let podHost if (this.Author.Pod) { @@ -443,7 +474,6 @@ toFormatedJSON = function (this: VideoInstance) { description: this.description, podHost, isLocal: this.isOwned(), - magnetUri: this.generateMagnetUri(), author: this.Author.name, duration: this.duration, views: this.views, @@ -451,10 +481,32 @@ toFormatedJSON = function (this: VideoInstance) { dislikes: this.dislikes, tags: map(this.Tags, 'name'), thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), + previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), createdAt: this.createdAt, - updatedAt: this.updatedAt + updatedAt: this.updatedAt, + files: [] } + // Format and sort video files + json.files = this.VideoFiles + .map(videoFile => { + let resolutionLabel = videoFile.resolution + 'p' + + const videoFileJson = { + resolution: videoFile.resolution, + resolutionLabel, + magnetUri: this.generateMagnetUri(videoFile), + size: videoFile.size + } + + return videoFileJson + }) + .sort((a, b) => { + if (a.resolution < b.resolution) return 1 + if (a.resolution === b.resolution) return 0 + return -1 + }) + return json } @@ -471,19 +523,27 @@ toAddRemoteJSON = function (this: VideoInstance) { language: this.language, nsfw: this.nsfw, description: this.description, - infoHash: this.infoHash, author: this.Author.name, duration: this.duration, thumbnailData: thumbnailData.toString('binary'), tags: map(this.Tags, 'name'), createdAt: this.createdAt, updatedAt: this.updatedAt, - extname: this.extname, views: this.views, likes: this.likes, - dislikes: this.dislikes + dislikes: this.dislikes, + files: [] } + this.VideoFiles.forEach(videoFile => { + remoteVideo.files.push({ + infoHash: videoFile.infoHash, + resolution: videoFile.resolution, + extname: videoFile.extname, + size: videoFile.size + }) + }) + return remoteVideo }) } @@ -497,67 +557,139 @@ toUpdateRemoteJSON = function (this: VideoInstance) { language: this.language, nsfw: this.nsfw, description: this.description, - infoHash: this.infoHash, author: this.Author.name, duration: this.duration, tags: map(this.Tags, 'name'), createdAt: this.createdAt, updatedAt: this.updatedAt, - extname: this.extname, views: this.views, likes: this.likes, - dislikes: this.dislikes + dislikes: this.dislikes, + files: [] } + this.VideoFiles.forEach(videoFile => { + json.files.push({ + infoHash: videoFile.infoHash, + resolution: videoFile.resolution, + extname: videoFile.extname, + size: videoFile.size + }) + }) + return json } -transcodeVideofile = function (this: VideoInstance) { - const video = this - +optimizeOriginalVideofile = function (this: VideoInstance) { const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const newExtname = '.mp4' - const videoInputPath = join(videosDirectory, video.getVideoFilename()) - const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) - - return new Promise((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.', err) - }) - - return rej(err) - }) - }) - .run() + const inputVideoFile = this.getOriginalFile() + const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) + const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) + + const transcodeOptions = { + inputPath: videoInputPath, + outputPath: videoOutputPath + } + + return transcode(transcodeOptions) + .then(() => { + return unlinkPromise(videoInputPath) + }) + .then(() => { + // Important to do this before getVideoFilename() to take in account the new file extension + inputVideoFile.set('extname', newExtname) + + return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) + }) + .then(() => { + return statPromise(this.getVideoFilePath(inputVideoFile)) + }) + .then(stats => { + return inputVideoFile.set('size', stats.size) + }) + .then(() => { + return this.createTorrentAndSetInfoHash(inputVideoFile) + }) + .then(() => { + return inputVideoFile.save() + }) + .then(() => { + return undefined + }) + .catch(err => { + // Auto destruction... + this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) + + throw err + }) +} + +transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { + const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR + const extname = '.mp4' + + // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed + const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) + + const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({ + resolution, + extname, + size: 0, + videoId: this.id }) + const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) + + const transcodeOptions = { + inputPath: videoInputPath, + outputPath: videoOutputPath, + resolution + } + return transcode(transcodeOptions) + .then(() => { + return statPromise(videoOutputPath) + }) + .then(stats => { + newVideoFile.set('size', stats.size) + + return undefined + }) + .then(() => { + return this.createTorrentAndSetInfoHash(newVideoFile) + }) + .then(() => { + return newVideoFile.save() + }) + .then(() => { + return this.VideoFiles.push(newVideoFile) + }) + .then(() => undefined) +} + +getOriginalFileHeight = function (this: VideoInstance) { + const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) + + return getVideoFileHeight(originalFilePath) +} + +removeThumbnail = function (this: VideoInstance) { + const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) + return unlinkPromise(thumbnailPath) +} + +removePreview = function (this: VideoInstance) { + // Same name than video thumbnail + return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) +} + +removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) { + const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) + return unlinkPromise(filePath) +} + +removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { + const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + return unlinkPromise(torrentPath) } // ------------------------------ STATICS ------------------------------ @@ -572,22 +704,16 @@ generateThumbnailFromData = function (video: VideoInstance, thumbnailData: strin }) } -getDurationFromFile = function (videoPath: string) { - return new Promise((res, rej) => { - ffmpeg.ffprobe(videoPath, function (err, metadata) { - if (err) return rej(err) - - return res(Math.floor(metadata.format.duration)) - }) - }) -} - list = function () { - return Video.findAll() + const query = { + include: [ Video['sequelize'].models.VideoFile ] + } + + return Video.findAll(query) } listForApi = function (start: number, count: number, sort: string) { - // Exclude Blakclisted videos from the list + // Exclude blacklisted videos from the list const query = { distinct: true, offset: start, @@ -598,8 +724,8 @@ listForApi = function (start: number, count: number, sort: string) { model: Video['sequelize'].models.Author, include: [ { model: Video['sequelize'].models.Pod, required: false } ] }, - - Video['sequelize'].models.Tag + Video['sequelize'].models.Tag, + Video['sequelize'].models.VideoFile ], where: createBaseVideosWhere() } @@ -618,6 +744,9 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) { uuid }, include: [ + { + model: Video['sequelize'].models.VideoFile + }, { model: Video['sequelize'].models.Author, include: [ @@ -641,7 +770,11 @@ listOwnedAndPopulateAuthorAndTags = function () { where: { remote: false }, - include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] + include: [ + Video['sequelize'].models.VideoFile, + Video['sequelize'].models.Author, + Video['sequelize'].models.Tag + ] } return Video.findAll(query) @@ -653,6 +786,9 @@ listOwnedByAuthor = function (author: string) { remote: false }, include: [ + { + model: Video['sequelize'].models.VideoFile + }, { model: Video['sequelize'].models.Author, where: { @@ -673,14 +809,15 @@ loadByUUID = function (uuid: string) { const query = { where: { uuid - } + }, + include: [ Video['sequelize'].models.VideoFile ] } return Video.findOne(query) } loadAndPopulateAuthor = function (id: number) { const options = { - include: [ Video['sequelize'].models.Author ] + include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ] } return Video.findById(id, options) @@ -693,7 +830,8 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) { model: Video['sequelize'].models.Author, include: [ { model: Video['sequelize'].models.Pod, required: false } ] }, - Video['sequelize'].models.Tag + Video['sequelize'].models.Tag, + Video['sequelize'].models.VideoFile ] } @@ -710,7 +848,8 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { model: Video['sequelize'].models.Author, include: [ { model: Video['sequelize'].models.Pod, required: false } ] }, - Video['sequelize'].models.Tag + Video['sequelize'].models.Tag, + Video['sequelize'].models.VideoFile ] } @@ -734,7 +873,11 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s model: Video['sequelize'].models.Tag } - const query: Sequelize.FindOptions = { + const videoFileInclude: Sequelize.IncludeOptions = { + model: Video['sequelize'].models.VideoFile + } + + const query: Sequelize.FindOptions = { distinct: true, where: createBaseVideosWhere(), offset: start, @@ -744,8 +887,9 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s // Make an exact search with the magnet if (field === 'magnetUri') { - const infoHash = magnetUtil.decode(value).infoHash - query.where['infoHash'] = infoHash + videoFileInclude.where = { + infoHash: magnetUtil.decode(value).infoHash + } } else if (field === 'tags') { const escapedValue = Video['sequelize'].escape('%' + value + '%') query.where['id'].$in = Video['sequelize'].literal( @@ -778,7 +922,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s } query.include = [ - authorInclude, tagInclude + authorInclude, tagInclude, videoFileInclude ] return Video.findAndCountAll(query).then(({ rows, count }) => { @@ -800,87 +944,3 @@ function createBaseVideosWhere () { } } } - -function removeThumbnail (video: VideoInstance) { - const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) - return unlinkPromise(thumbnailPath) -} - -function removeFile (video: VideoInstance) { - const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - return unlinkPromise(filePath) -} - -function removeTorrent (video: VideoInstance) { - const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) - return unlinkPromise(torrenPath) -} - -function removePreview (video: VideoInstance) { - // Same name than video thumnail - return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName()) -} - -function createTorrentFromVideo (video: VideoInstance, videoPath: string) { - const options = { - announceList: [ - [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] - ], - urlList: [ - CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename() - ] - } - - 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) - return video.validate() - }) -} - -function createPreview (video: VideoInstance, videoPath: string) { - return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null) -} - -function createThumbnail (video: VideoInstance, videoPath: string) { - return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE) -} - -function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) { - const options = { - filename: imageName, - count: 1, - folder - } - - if (size) { - options['size'] = size - } - - return new Promise((res, rej) => { - ffmpeg(videoPath) - .on('error', rej) - .on('end', function () { - return res(imageName) - }) - .thumbnail(options) - }) -} - -function removeFromBlacklist (video: VideoInstance) { - // Find the blacklisted video - 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 - return video.destroy() - }) -}