3 const eachLimit = require('async/eachLimit')
4 const ffmpeg = require('fluent-ffmpeg')
5 const fs = require('fs')
6 const parallel = require('async/parallel')
7 const pathUtils = require('path')
8 const mongoose = require('mongoose')
10 const constants = require('../initializers/constants')
11 const customVideosValidators = require('../helpers/custom-validators').videos
12 const logger = require('../helpers/logger')
13 const modelUtils = require('./utils')
14 const utils = require('../helpers/utils')
15 const webtorrent = require('../lib/webtorrent')
17 // ---------------------------------------------------------------------------
19 // TODO: add indexes on searchable columns
20 const VideoSchema = mongoose.Schema({
36 VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
37 VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
38 VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
39 VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
40 VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
41 VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
42 // The tumbnail can be the path or the data in base 64
43 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
44 VideoSchema.path('thumbnail').validate(function (value) {
45 return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
47 VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
49 VideoSchema.methods = {
55 VideoSchema.statics = {
68 VideoSchema.pre('remove', function (next) {
74 removeThumbnail(video, callback)
78 if (video.isOwned()) {
81 removeFile(video, callback)
84 removeTorrent(video, callback)
92 VideoSchema.pre('save', function (next) {
96 if (video.isOwned()) {
97 const videoPath = pathUtils.join(constants.CONFIG.STORAGE.UPLOAD_DIR, video.filename)
98 this.podUrl = constants.CONFIG.WEBSERVER.URL
101 function (callback) {
102 seed(videoPath, callback)
104 function (callback) {
105 createThumbnail(videoPath, callback)
109 parallel(tasks, function (err, results) {
110 if (err) return next(err)
112 video.magnetUri = results[0].magnetURI
113 video.thumbnail = results[1]
118 generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
119 if (err) return next(err)
121 video.thumbnail = thumbnailName
128 mongoose.model('Video', VideoSchema)
130 // ------------------------------ METHODS ------------------------------
132 function isOwned () {
133 return this.filename !== null
136 function toFormatedJSON () {
140 description: this.description,
141 podUrl: this.podUrl.replace(/^https?:\/\//, ''),
142 isLocal: this.isOwned(),
143 magnetUri: this.magnetUri,
145 duration: this.duration,
147 thumbnailPath: constants.THUMBNAILS_STATIC_PATH + '/' + this.thumbnail,
148 createdDate: this.createdDate
154 function toRemoteJSON (callback) {
157 // Convert thumbnail to base64
158 fs.readFile(pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.thumbnail), function (err, thumbnailData) {
160 logger.error('Cannot read the thumbnail of the video')
164 const remoteVideo = {
166 description: self.description,
167 magnetUri: self.magnetUri,
170 duration: self.duration,
171 thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
173 createdDate: self.createdDate,
177 return callback(null, remoteVideo)
181 // ------------------------------ STATICS ------------------------------
183 function getDurationFromFile (videoPath, callback) {
184 ffmpeg.ffprobe(videoPath, function (err, metadata) {
185 if (err) return callback(err)
187 return callback(null, Math.floor(metadata.format.duration))
191 function listForApi (start, count, sort, callback) {
193 return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
196 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
197 this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
200 function listByUrls (fromUrls, callback) {
201 this.find({ podUrl: { $in: fromUrls } }, callback)
204 function listOwned (callback) {
205 // If filename is not null this is *our* video
206 this.find({ filename: { $ne: null } }, callback)
209 function listOwnedByAuthor (author, callback) {
210 this.find({ filename: { $ne: null }, author: author }, callback)
213 function listRemotes (callback) {
214 this.find({ filename: null }, callback)
217 function load (id, callback) {
218 this.findById(id, callback)
221 function search (value, field, start, count, sort, callback) {
223 // Make an exact search with the magnet
224 if (field === 'magnetUri' || field === 'tags') {
227 query[field] = new RegExp(value)
230 modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
233 function seedAllExisting (callback) {
234 listOwned.call(this, function (err, videos) {
235 if (err) return callback(err)
237 eachLimit(videos, constants.SEEDS_IN_PARALLEL, function (video, callbackEach) {
238 const videoPath = pathUtils.join(constants.CONFIG.STORAGE.UPLOAD_DIR, video.filename)
239 seed(videoPath, callbackEach)
244 // ---------------------------------------------------------------------------
246 function removeThumbnail (video, callback) {
247 fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.thumbnail, callback)
250 function removeFile (video, callback) {
251 fs.unlink(constants.CONFIG.STORAGE.UPLOAD_DIR + video.filename, callback)
254 // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
255 function removeTorrent (video, callback) {
257 webtorrent.remove(video.magnetUri, callback)
259 logger.warn('Cannot remove the torrent from WebTorrent', { err: err })
260 return callback(null)
264 function createThumbnail (videoPath, callback) {
265 const filename = pathUtils.basename(videoPath) + '.jpg'
267 .on('error', callback)
268 .on('end', function () {
269 callback(null, filename)
273 folder: constants.CONFIG.STORAGE.THUMBNAILS_DIR,
274 size: constants.THUMBNAILS_SIZE,
279 function seed (path, callback) {
280 logger.info('Seeding %s...', path)
282 webtorrent.seed(path, function (torrent) {
283 logger.info('%s seeded (%s).', path, torrent.magnetURI)
285 return callback(null, torrent)
289 function generateThumbnailFromBase64 (data, callback) {
290 // Creating the thumbnail for this remote video
291 utils.generateRandomString(16, function (err, randomString) {
292 if (err) return callback(err)
294 const thumbnailName = randomString + '.jpg'
295 fs.writeFile(constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName, data, { encoding: 'base64' }, function (err) {
296 if (err) return callback(err)
298 return callback(null, thumbnailName)