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 customVideosValidators = require('../helpers/custom-validators').videos
15 const logger = require('../helpers/logger')
16 const modelUtils = require('./utils')
17 const utils = require('../helpers/utils')
19 const http = config.get('webserver.https') === true ? 'https' : 'http'
20 const host = config.get('webserver.host')
21 const port = config.get('webserver.port')
22 const uploadsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.uploads'))
23 const thumbnailsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.thumbnails'))
24 const torrentsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.torrents'))
25 const webseedBaseUrl = http + '://' + host + ':' + port + constants.STATIC_PATHS.WEBSEED
27 // ---------------------------------------------------------------------------
29 // TODO: add indexes on searchable columns
30 const VideoSchema = mongoose.Schema({
46 VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
47 VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
48 VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
49 VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
50 VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
51 VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
52 // The tumbnail can be the path or the data in base 64
53 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
54 VideoSchema.path('thumbnail').validate(function (value) {
55 return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
57 VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
59 VideoSchema.methods = {
65 VideoSchema.statics = {
77 VideoSchema.pre('remove', function (next) {
83 removeThumbnail(video, callback)
87 if (video.isOwned()) {
90 removeFile(video, callback)
93 removeTorrent(video, callback)
101 VideoSchema.pre('save', function (next) {
105 if (video.isOwned()) {
106 const videoPath = pathUtils.join(constants.CONFIG.STORAGE.UPLOAD_DIR, video.filename)
107 this.podUrl = constants.CONFIG.WEBSERVER.URL
110 // TODO: refractoring
111 function (callback) {
112 createTorrent(videoPath, { announceList: [ [ 'ws://' + host + ':' + port + '/tracker/socket' ] ], urlList: [ webseedBaseUrl + video.filename ] }, function (err, torrent) {
113 if (err) return callback(err)
115 fs.writeFile(torrentsDir + video.filename + '.torrent', torrent, function (err) {
116 if (err) return callback(err)
118 const parsedTorrent = parseTorrent(torrent)
119 parsedTorrent.xs = video.podUrl + constants.STATIC_PATHS.TORRENTS + video.filename + '.torrent'
120 video.magnetUri = magnet.encode(parsedTorrent)
126 function (callback) {
127 createThumbnail(videoPath, callback)
131 parallel(tasks, function (err, results) {
132 if (err) return next(err)
134 video.thumbnail = results[1]
139 generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
140 if (err) return next(err)
142 video.thumbnail = thumbnailName
149 mongoose.model('Video', VideoSchema)
151 // ------------------------------ METHODS ------------------------------
153 function isOwned () {
154 return this.filename !== null
157 function toFormatedJSON () {
161 description: this.description,
162 podUrl: this.podUrl.replace(/^https?:\/\//, ''),
163 isLocal: this.isOwned(),
164 magnetUri: this.magnetUri,
166 duration: this.duration,
168 thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.thumbnail,
169 createdDate: this.createdDate
175 function toRemoteJSON (callback) {
178 // Convert thumbnail to base64
179 fs.readFile(pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.thumbnail), function (err, thumbnailData) {
181 logger.error('Cannot read the thumbnail of the video')
185 const remoteVideo = {
187 description: self.description,
188 magnetUri: self.magnetUri,
191 duration: self.duration,
192 thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
194 createdDate: self.createdDate,
198 return callback(null, remoteVideo)
202 // ------------------------------ STATICS ------------------------------
204 function getDurationFromFile (videoPath, callback) {
205 ffmpeg.ffprobe(videoPath, function (err, metadata) {
206 if (err) return callback(err)
208 return callback(null, Math.floor(metadata.format.duration))
212 function listForApi (start, count, sort, callback) {
214 return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
217 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
218 this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
221 function listByUrls (fromUrls, callback) {
222 this.find({ podUrl: { $in: fromUrls } }, callback)
225 function listOwned (callback) {
226 // If filename is not null this is *our* video
227 this.find({ filename: { $ne: null } }, callback)
230 function listOwnedByAuthor (author, callback) {
231 this.find({ filename: { $ne: null }, author: author }, callback)
234 function listRemotes (callback) {
235 this.find({ filename: null }, callback)
238 function load (id, callback) {
239 this.findById(id, callback)
242 function search (value, field, start, count, sort, callback) {
244 // Make an exact search with the magnet
245 if (field === 'magnetUri' || field === 'tags') {
248 query[field] = new RegExp(value)
251 modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
254 // ---------------------------------------------------------------------------
256 function removeThumbnail (video, callback) {
257 fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.thumbnail, callback)
260 function removeFile (video, callback) {
261 fs.unlink(constants.CONFIG.STORAGE.UPLOAD_DIR + video.filename, callback)
264 // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
265 function removeTorrent (video, callback) {
266 fs.unlink(torrentsDir + video.filename + '.torrent')
269 function createThumbnail (videoPath, callback) {
270 const filename = pathUtils.basename(videoPath) + '.jpg'
272 .on('error', callback)
273 .on('end', function () {
274 callback(null, filename)
278 folder: constants.CONFIG.STORAGE.THUMBNAILS_DIR,
279 size: constants.THUMBNAILS_SIZE,
284 function generateThumbnailFromBase64 (data, callback) {
285 // Creating the thumbnail for this remote video
286 utils.generateRandomString(16, function (err, randomString) {
287 if (err) return callback(err)
289 const thumbnailName = randomString + '.jpg'
290 fs.writeFile(constants.CONFIG.STORAGE.THUMBNAILS_DIR + thumbnailName, data, { encoding: 'base64' }, function (err) {
291 if (err) return callback(err)
293 return callback(null, thumbnailName)