3 const async = require('async')
4 const config = require('config')
5 const ffmpeg = require('fluent-ffmpeg')
6 const fs = require('fs')
7 const pathUtils = require('path')
8 const mongoose = require('mongoose')
10 const constants = require('../initializers/constants')
11 const customValidators = require('../helpers/customValidators')
12 const logger = require('../helpers/logger')
13 const utils = require('../helpers/utils')
14 const webtorrent = require('../lib/webtorrent')
16 const http = config.get('webserver.https') === true ? 'https' : 'http'
17 const host = config.get('webserver.host')
18 const port = config.get('webserver.port')
19 const uploadsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.uploads'))
20 const thumbnailsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.thumbnails'))
22 // ---------------------------------------------------------------------------
24 // TODO: add indexes on searchable columns
25 const VideoSchema = mongoose.Schema({
41 VideoSchema.path('name').validate(customValidators.isVideoNameValid)
42 VideoSchema.path('description').validate(customValidators.isVideoDescriptionValid)
43 VideoSchema.path('magnetUri').validate(customValidators.isVideoMagnetUriValid)
44 VideoSchema.path('podUrl').validate(customValidators.isVideoPodUrlValid)
45 VideoSchema.path('author').validate(customValidators.isVideoAuthorValid)
46 VideoSchema.path('duration').validate(customValidators.isVideoDurationValid)
47 // The tumbnail can be the path or the data in base 64
48 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
49 VideoSchema.path('thumbnail').validate(function (value) {
50 return customValidators.isVideoThumbnailValid(value) || customValidators.isVideoThumbnail64Valid(value)
52 VideoSchema.path('tags').validate(customValidators.isVideoTagsValid)
54 VideoSchema.methods = {
56 toFormatedJSON: toFormatedJSON,
57 toRemoteJSON: toRemoteJSON
60 VideoSchema.statics = {
61 getDurationFromFile: getDurationFromFile,
63 listByUrlAndMagnet: listByUrlAndMagnet,
64 listByUrls: listByUrls,
66 listRemotes: listRemotes,
69 seedAllExisting: seedAllExisting
72 VideoSchema.pre('remove', function (next) {
78 removeThumbnail(video, callback)
82 if (video.isOwned()) {
85 removeFile(video, callback)
88 removeTorrent(video, callback)
93 async.parallel(tasks, next)
96 VideoSchema.pre('save', function (next) {
100 if (video.isOwned()) {
101 const videoPath = pathUtils.join(uploadsDir, video.filename)
102 this.podUrl = http + '://' + host + ':' + port
105 function (callback) {
106 seed(videoPath, callback)
108 function (callback) {
109 createThumbnail(videoPath, callback)
113 async.parallel(tasks, function (err, results) {
114 if (err) return next(err)
116 video.magnetUri = results[0].magnetURI
117 video.thumbnail = results[1]
122 generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
123 if (err) return next(err)
125 video.thumbnail = thumbnailName
132 mongoose.model('Video', VideoSchema)
134 // ------------------------------ METHODS ------------------------------
136 function isOwned () {
137 return this.filename !== null
140 function toFormatedJSON () {
144 description: this.description,
145 podUrl: this.podUrl.replace(/^https?:\/\//, ''),
146 isLocal: this.isOwned(),
147 magnetUri: this.magnetUri,
149 duration: this.duration,
151 thumbnailPath: constants.THUMBNAILS_STATIC_PATH + '/' + this.thumbnail,
152 createdDate: this.createdDate
158 function toRemoteJSON (callback) {
161 // Convert thumbnail to base64
162 fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) {
164 logger.error('Cannot read the thumbnail of the video')
168 const remoteVideo = {
170 description: self.description,
171 magnetUri: self.magnetUri,
174 duration: self.duration,
175 thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
177 createdDate: self.createdDate,
181 return callback(null, remoteVideo)
185 // ------------------------------ STATICS ------------------------------
187 function getDurationFromFile (videoPath, callback) {
188 ffmpeg.ffprobe(videoPath, function (err, metadata) {
189 if (err) return callback(err)
191 return callback(null, Math.floor(metadata.format.duration))
195 function list (start, count, sort, callback) {
197 return findWithCount.call(this, query, start, count, sort, callback)
200 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
201 this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
204 function listByUrls (fromUrls, callback) {
205 this.find({ podUrl: { $in: fromUrls } }, callback)
208 function listOwned (callback) {
209 // If filename is not null this is *our* video
210 this.find({ filename: { $ne: null } }, 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 findWithCount.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 async.each(videos, function (video, callbackEach) {
238 const videoPath = pathUtils.join(uploadsDir, video.filename)
239 seed(videoPath, callbackEach)
244 // ---------------------------------------------------------------------------
246 function findWithCount (query, start, count, sort, callback) {
250 function (asyncCallback) {
251 self.find(query).skip(start).limit(start + count).sort(sort).exec(asyncCallback)
253 function (asyncCallback) {
254 self.count(query, asyncCallback)
256 ], function (err, results) {
257 if (err) return callback(err)
259 const videos = results[0]
260 const totalVideos = results[1]
261 return callback(null, videos, totalVideos)
265 function removeThumbnail (video, callback) {
266 fs.unlink(thumbnailsDir + video.thumbnail, callback)
269 function removeFile (video, callback) {
270 fs.unlink(uploadsDir + video.filename, callback)
273 // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
274 function removeTorrent (video, callback) {
276 webtorrent.remove(video.magnetUri, callback)
278 logger.warn('Cannot remove the torrent from WebTorrent', { err: err })
279 return callback(null)
283 function createThumbnail (videoPath, callback) {
284 const filename = pathUtils.basename(videoPath) + '.jpg'
286 .on('error', callback)
287 .on('end', function () {
288 callback(null, filename)
292 folder: thumbnailsDir,
293 size: constants.THUMBNAILS_SIZE,
298 function seed (path, callback) {
299 logger.info('Seeding %s...', path)
301 webtorrent.seed(path, function (torrent) {
302 logger.info('%s seeded (%s).', path, torrent.magnetURI)
304 return callback(null, torrent)
308 function generateThumbnailFromBase64 (data, callback) {
309 // Creating the thumbnail for this remote video
310 utils.generateRandomString(16, function (err, randomString) {
311 if (err) return callback(err)
313 const thumbnailName = randomString + '.jpg'
314 fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) {
315 if (err) return callback(err)
317 return callback(null, thumbnailName)