3 const config = require('config')
4 const eachLimit = require('async/eachLimit')
5 const ffmpeg = require('fluent-ffmpeg')
6 const fs = require('fs')
7 const parallel = require('async/parallel')
8 const pathUtils = require('path')
9 const mongoose = require('mongoose')
11 const constants = require('../initializers/constants')
12 const customVideosValidators = require('../helpers/custom-validators').videos
13 const logger = require('../helpers/logger')
14 const utils = require('../helpers/utils')
15 const webtorrent = require('../lib/webtorrent')
17 const http = config.get('webserver.https') === true ? 'https' : 'http'
18 const host = config.get('webserver.host')
19 const port = config.get('webserver.port')
20 const uploadsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.uploads'))
21 const thumbnailsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.thumbnails'))
23 // ---------------------------------------------------------------------------
25 // TODO: add indexes on searchable columns
26 const VideoSchema = mongoose.Schema({
42 VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
43 VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
44 VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
45 VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
46 VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
47 VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
48 // The tumbnail can be the path or the data in base 64
49 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
50 VideoSchema.path('thumbnail').validate(function (value) {
51 return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
53 VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
55 VideoSchema.methods = {
57 toFormatedJSON: toFormatedJSON,
58 toRemoteJSON: toRemoteJSON
61 VideoSchema.statics = {
62 getDurationFromFile: getDurationFromFile,
64 listByUrlAndMagnet: listByUrlAndMagnet,
65 listByUrls: listByUrls,
67 listRemotes: listRemotes,
70 seedAllExisting: seedAllExisting
73 VideoSchema.pre('remove', function (next) {
79 removeThumbnail(video, callback)
83 if (video.isOwned()) {
86 removeFile(video, callback)
89 removeTorrent(video, callback)
97 VideoSchema.pre('save', function (next) {
101 if (video.isOwned()) {
102 const videoPath = pathUtils.join(uploadsDir, video.filename)
103 this.podUrl = http + '://' + host + ':' + port
106 function (callback) {
107 seed(videoPath, callback)
109 function (callback) {
110 createThumbnail(videoPath, callback)
114 parallel(tasks, function (err, results) {
115 if (err) return next(err)
117 video.magnetUri = results[0].magnetURI
118 video.thumbnail = results[1]
123 generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
124 if (err) return next(err)
126 video.thumbnail = thumbnailName
133 mongoose.model('Video', VideoSchema)
135 // ------------------------------ METHODS ------------------------------
137 function isOwned () {
138 return this.filename !== null
141 function toFormatedJSON () {
145 description: this.description,
146 podUrl: this.podUrl.replace(/^https?:\/\//, ''),
147 isLocal: this.isOwned(),
148 magnetUri: this.magnetUri,
150 duration: this.duration,
152 thumbnailPath: constants.THUMBNAILS_STATIC_PATH + '/' + this.thumbnail,
153 createdDate: this.createdDate
159 function toRemoteJSON (callback) {
162 // Convert thumbnail to base64
163 fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) {
165 logger.error('Cannot read the thumbnail of the video')
169 const remoteVideo = {
171 description: self.description,
172 magnetUri: self.magnetUri,
175 duration: self.duration,
176 thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
178 createdDate: self.createdDate,
182 return callback(null, remoteVideo)
186 // ------------------------------ STATICS ------------------------------
188 function getDurationFromFile (videoPath, callback) {
189 ffmpeg.ffprobe(videoPath, function (err, metadata) {
190 if (err) return callback(err)
192 return callback(null, Math.floor(metadata.format.duration))
196 function list (start, count, sort, callback) {
198 return findWithCount.call(this, query, start, count, sort, callback)
201 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
202 this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
205 function listByUrls (fromUrls, callback) {
206 this.find({ podUrl: { $in: fromUrls } }, callback)
209 function listOwned (callback) {
210 // If filename is not null this is *our* video
211 this.find({ filename: { $ne: null } }, callback)
214 function listRemotes (callback) {
215 this.find({ filename: null }, callback)
218 function load (id, callback) {
219 this.findById(id, callback)
222 function search (value, field, start, count, sort, callback) {
224 // Make an exact search with the magnet
225 if (field === 'magnetUri' || field === 'tags') {
228 query[field] = new RegExp(value)
231 findWithCount.call(this, query, start, count, sort, callback)
234 function seedAllExisting (callback) {
235 listOwned.call(this, function (err, videos) {
236 if (err) return callback(err)
238 eachLimit(videos, constants.SEEDS_IN_PARALLEL, function (video, callbackEach) {
239 const videoPath = pathUtils.join(uploadsDir, video.filename)
240 seed(videoPath, callbackEach)
245 // ---------------------------------------------------------------------------
247 function findWithCount (query, start, count, sort, callback) {
251 function (asyncCallback) {
252 self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback)
254 function (asyncCallback) {
255 self.count(query, asyncCallback)
257 ], function (err, results) {
258 if (err) return callback(err)
260 const videos = results[0]
261 const totalVideos = results[1]
262 return callback(null, videos, totalVideos)
266 function removeThumbnail (video, callback) {
267 fs.unlink(thumbnailsDir + video.thumbnail, callback)
270 function removeFile (video, callback) {
271 fs.unlink(uploadsDir + video.filename, callback)
274 // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
275 function removeTorrent (video, callback) {
277 webtorrent.remove(video.magnetUri, callback)
279 logger.warn('Cannot remove the torrent from WebTorrent', { err: err })
280 return callback(null)
284 function createThumbnail (videoPath, callback) {
285 const filename = pathUtils.basename(videoPath) + '.jpg'
287 .on('error', callback)
288 .on('end', function () {
289 callback(null, filename)
293 folder: thumbnailsDir,
294 size: constants.THUMBNAILS_SIZE,
299 function seed (path, callback) {
300 logger.info('Seeding %s...', path)
302 webtorrent.seed(path, function (torrent) {
303 logger.info('%s seeded (%s).', path, torrent.magnetURI)
305 return callback(null, torrent)
309 function generateThumbnailFromBase64 (data, callback) {
310 // Creating the thumbnail for this remote video
311 utils.generateRandomString(16, function (err, randomString) {
312 if (err) return callback(err)
314 const thumbnailName = randomString + '.jpg'
315 fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) {
316 if (err) return callback(err)
318 return callback(null, thumbnailName)