3 const config = require('config')
4 const createTorrent = require('create-torrent')
5 const ffmpeg = require('fluent-ffmpeg')
6 const fs = require('fs')
7 const parallel = require('async/parallel')
8 const parseTorrent = require('parse-torrent')
9 const pathUtils = require('path')
10 const magnet = require('magnet-uri')
11 const mongoose = require('mongoose')
13 const constants = require('../initializers/constants')
14 const customValidators = require('../helpers/custom-validators')
15 const logger = require('../helpers/logger')
16 const utils = require('../helpers/utils')
18 const http = config.get('webserver.https') === true ? 'https' : 'http'
19 const host = config.get('webserver.host')
20 const port = config.get('webserver.port')
21 const uploadsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.uploads'))
22 const thumbnailsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.thumbnails'))
23 const torrentsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.torrents'))
24 const webseedBaseUrl = http + '://' + host + ':' + port + constants.STATIC_PATHS.WEBSEED
26 // ---------------------------------------------------------------------------
28 // TODO: add indexes on searchable columns
29 const VideoSchema = mongoose.Schema({
45 VideoSchema.path('name').validate(customValidators.isVideoNameValid)
46 VideoSchema.path('description').validate(customValidators.isVideoDescriptionValid)
47 VideoSchema.path('magnetUri').validate(customValidators.isVideoMagnetUriValid)
48 VideoSchema.path('podUrl').validate(customValidators.isVideoPodUrlValid)
49 VideoSchema.path('author').validate(customValidators.isVideoAuthorValid)
50 VideoSchema.path('duration').validate(customValidators.isVideoDurationValid)
51 // The tumbnail can be the path or the data in base 64
52 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
53 VideoSchema.path('thumbnail').validate(function (value) {
54 return customValidators.isVideoThumbnailValid(value) || customValidators.isVideoThumbnail64Valid(value)
56 VideoSchema.path('tags').validate(customValidators.isVideoTagsValid)
58 VideoSchema.methods = {
60 toFormatedJSON: toFormatedJSON,
61 toRemoteJSON: toRemoteJSON
64 VideoSchema.statics = {
65 getDurationFromFile: getDurationFromFile,
67 listByUrlAndMagnet: listByUrlAndMagnet,
68 listByUrls: listByUrls,
70 listRemotes: listRemotes,
75 VideoSchema.pre('remove', function (next) {
81 removeThumbnail(video, callback)
85 if (video.isOwned()) {
88 removeFile(video, callback)
91 removeTorrent(video, callback)
99 VideoSchema.pre('save', function (next) {
103 if (video.isOwned()) {
104 const videoPath = pathUtils.join(uploadsDir, video.filename)
105 this.podUrl = http + '://' + host + ':' + port
108 // TODO: refractoring
109 function (callback) {
110 createTorrent(videoPath, { announceList: [ [ 'ws://' + host + ':' + port + '/tracker/socket' ] ], urlList: [ webseedBaseUrl + video.filename ] }, function (err, torrent) {
111 if (err) return callback(err)
113 fs.writeFile(torrentsDir + video.filename + '.torrent', torrent, function (err) {
114 if (err) return callback(err)
116 const parsedTorrent = parseTorrent(torrent)
117 parsedTorrent.xs = video.podUrl + constants.STATIC_PATHS.TORRENTS + video.filename + '.torrent'
118 video.magnetUri = magnet.encode(parsedTorrent)
124 function (callback) {
125 createThumbnail(videoPath, callback)
129 parallel(tasks, function (err, results) {
130 if (err) return next(err)
132 video.thumbnail = results[1]
137 generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
138 if (err) return next(err)
140 video.thumbnail = thumbnailName
147 mongoose.model('Video', VideoSchema)
149 // ------------------------------ METHODS ------------------------------
151 function isOwned () {
152 return this.filename !== null
155 function toFormatedJSON () {
159 description: this.description,
160 podUrl: this.podUrl.replace(/^https?:\/\//, ''),
161 isLocal: this.isOwned(),
162 magnetUri: this.magnetUri,
164 duration: this.duration,
166 thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.thumbnail,
167 createdDate: this.createdDate
173 function toRemoteJSON (callback) {
176 // Convert thumbnail to base64
177 fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) {
179 logger.error('Cannot read the thumbnail of the video')
183 const remoteVideo = {
185 description: self.description,
186 magnetUri: self.magnetUri,
189 duration: self.duration,
190 thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
192 createdDate: self.createdDate,
196 return callback(null, remoteVideo)
200 // ------------------------------ STATICS ------------------------------
202 function getDurationFromFile (videoPath, callback) {
203 ffmpeg.ffprobe(videoPath, function (err, metadata) {
204 if (err) return callback(err)
206 return callback(null, Math.floor(metadata.format.duration))
210 function list (start, count, sort, callback) {
212 return findWithCount.call(this, query, start, count, sort, callback)
215 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
216 this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
219 function listByUrls (fromUrls, callback) {
220 this.find({ podUrl: { $in: fromUrls } }, callback)
223 function listOwned (callback) {
224 // If filename is not null this is *our* video
225 this.find({ filename: { $ne: null } }, callback)
228 function listRemotes (callback) {
229 this.find({ filename: null }, callback)
232 function load (id, callback) {
233 this.findById(id, callback)
236 function search (value, field, start, count, sort, callback) {
238 // Make an exact search with the magnet
239 if (field === 'magnetUri' || field === 'tags') {
242 query[field] = new RegExp(value)
245 findWithCount.call(this, query, start, count, sort, callback)
248 // ---------------------------------------------------------------------------
250 function findWithCount (query, start, count, sort, callback) {
254 function (asyncCallback) {
255 self.find(query).skip(start).limit(count).sort(sort).exec(asyncCallback)
257 function (asyncCallback) {
258 self.count(query, asyncCallback)
260 ], function (err, results) {
261 if (err) return callback(err)
263 const videos = results[0]
264 const totalVideos = results[1]
265 return callback(null, videos, totalVideos)
269 function removeThumbnail (video, callback) {
270 fs.unlink(thumbnailsDir + video.thumbnail, callback)
273 function removeFile (video, callback) {
274 fs.unlink(uploadsDir + video.filename, callback)
277 // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
278 function removeTorrent (video, callback) {
279 fs.unlink(torrentsDir + video.filename + '.torrent')
282 function createThumbnail (videoPath, callback) {
283 const filename = pathUtils.basename(videoPath) + '.jpg'
285 .on('error', callback)
286 .on('end', function () {
287 callback(null, filename)
291 folder: thumbnailsDir,
292 size: constants.THUMBNAILS_SIZE,
297 function generateThumbnailFromBase64 (data, callback) {
298 // Creating the thumbnail for this remote video
299 utils.generateRandomString(16, function (err, randomString) {
300 if (err) return callback(err)
302 const thumbnailName = randomString + '.jpg'
303 fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) {
304 if (err) return callback(err)
306 return callback(null, thumbnailName)