Video model refractoring -> use mongoose api
[oweals/peertube.git] / server / models / video.js
1 'use strict'
2
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')
9
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')
15
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'))
21
22 // ---------------------------------------------------------------------------
23
24 // TODO: add indexes on searchable columns
25 const VideoSchema = mongoose.Schema({
26   name: String,
27   namePath: String,
28   description: String,
29   magnetUri: String,
30   podUrl: String,
31   author: String,
32   duration: Number,
33   thumbnail: String,
34   tags: [ String ],
35   createdDate: {
36     type: Date,
37     default: Date.now
38   }
39 })
40
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)
51 })
52 VideoSchema.path('tags').validate(customValidators.isVideoTagsValid)
53
54 VideoSchema.methods = {
55   isOwned: isOwned,
56   toFormatedJSON: toFormatedJSON,
57   toRemoteJSON: toRemoteJSON
58 }
59
60 VideoSchema.statics = {
61   getDurationFromFile: getDurationFromFile,
62   list: list,
63   listByUrlAndMagnet: listByUrlAndMagnet,
64   listByUrls: listByUrls,
65   listOwned: listOwned,
66   listRemotes: listRemotes,
67   load: load,
68   search: search,
69   seedAllExisting: seedAllExisting
70 }
71
72 VideoSchema.pre('remove', function (next) {
73   const video = this
74   const tasks = []
75
76   tasks.push(
77     function (callback) {
78       removeThumbnail(video, callback)
79     }
80   )
81
82   if (video.isOwned()) {
83     tasks.push(
84       function (callback) {
85         removeFile(video, callback)
86       },
87       function (callback) {
88         removeTorrent(video, callback)
89       }
90     )
91   }
92
93   async.parallel(tasks, next)
94 })
95
96 VideoSchema.pre('save', function (next) {
97   const video = this
98   const tasks = []
99
100   if (video.isOwned()) {
101     const videoPath = pathUtils.join(uploadsDir, video.namePath)
102     this.podUrl = http + '://' + host + ':' + port
103
104     tasks.push(
105       function (callback) {
106         seed(videoPath, callback)
107       },
108       function (callback) {
109         createThumbnail(videoPath, callback)
110       }
111     )
112
113     async.parallel(tasks, function (err, results) {
114       if (err) return next(err)
115
116       video.magnetUri = results[0].magnetURI
117       video.thumbnail = results[1]
118
119       return next()
120     })
121   } else {
122     generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
123       if (err) return next(err)
124
125       video.thumbnail = thumbnailName
126
127       return next()
128     })
129   }
130 })
131
132 mongoose.model('Video', VideoSchema)
133
134 // ------------------------------ METHODS ------------------------------
135
136 function isOwned () {
137   return this.namePath !== null
138 }
139
140 function toFormatedJSON () {
141   const json = {
142     id: this._id,
143     name: this.name,
144     description: this.description,
145     podUrl: this.podUrl.replace(/^https?:\/\//, ''),
146     isLocal: this.isOwned(),
147     magnetUri: this.magnetUri,
148     author: this.author,
149     duration: this.duration,
150     tags: this.tags,
151     thumbnailPath: constants.THUMBNAILS_STATIC_PATH + '/' + this.thumbnail,
152     createdDate: this.createdDate
153   }
154
155   return json
156 }
157
158 function toRemoteJSON (callback) {
159   const self = this
160
161   // Convert thumbnail to base64
162   fs.readFile(pathUtils.join(thumbnailsDir, this.thumbnail), function (err, thumbnailData) {
163     if (err) {
164       logger.error('Cannot read the thumbnail of the video')
165       return callback(err)
166     }
167
168     const remoteVideo = {
169       name: self.name,
170       description: self.description,
171       magnetUri: self.magnetUri,
172       namePath: null,
173       author: self.author,
174       duration: self.duration,
175       thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
176       tags: self.tags,
177       createdDate: self.createdDate,
178       podUrl: self.podUrl
179     }
180
181     return callback(null, remoteVideo)
182   })
183 }
184
185 // ------------------------------ STATICS ------------------------------
186
187 function getDurationFromFile (videoPath, callback) {
188   ffmpeg.ffprobe(videoPath, function (err, metadata) {
189     if (err) return callback(err)
190
191     return callback(null, Math.floor(metadata.format.duration))
192   })
193 }
194
195 function list (start, count, sort, callback) {
196   const query = {}
197   return findWithCount.call(this, query, start, count, sort, callback)
198 }
199
200 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
201   this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
202 }
203
204 function listByUrls (fromUrls, callback) {
205   this.find({ podUrl: { $in: fromUrls } }, callback)
206 }
207
208 function listOwned (callback) {
209   // If namePath is not null this is *our* video
210   this.find({ namePath: { $ne: null } }, callback)
211 }
212
213 function listRemotes (callback) {
214   this.find({ namePath: null }, callback)
215 }
216
217 function load (id, callback) {
218   this.findById(id, callback)
219 }
220
221 function search (value, field, start, count, sort, callback) {
222   const query = {}
223   // Make an exact search with the magnet
224   if (field === 'magnetUri' || field === 'tags') {
225     query[field] = value
226   } else {
227     query[field] = new RegExp(value)
228   }
229
230   findWithCount.call(this, query, start, count, sort, callback)
231 }
232
233 // TODO
234 function seedAllExisting () {
235
236 }
237
238 // ---------------------------------------------------------------------------
239
240 function findWithCount (query, start, count, sort, callback) {
241   const self = this
242
243   async.parallel([
244     function (asyncCallback) {
245       self.find(query).skip(start).limit(start + count).sort(sort).exec(asyncCallback)
246     },
247     function (asyncCallback) {
248       self.count(query, asyncCallback)
249     }
250   ], function (err, results) {
251     if (err) return callback(err)
252
253     const videos = results[0]
254     const totalVideos = results[1]
255     return callback(null, videos, totalVideos)
256   })
257 }
258
259 function removeThumbnail (video, callback) {
260   fs.unlink(thumbnailsDir + video.thumbnail, callback)
261 }
262
263 function removeFile (video, callback) {
264   fs.unlink(uploadsDir + video.namePath, callback)
265 }
266
267 // Maybe the torrent is not seeded, but we catch the error to don't stop the removing process
268 function removeTorrent (video, callback) {
269   try {
270     webtorrent.remove(video.magnetUri, callback)
271   } catch (err) {
272     logger.warn('Cannot remove the torrent from WebTorrent', { err: err })
273     return callback(null)
274   }
275 }
276
277 function createThumbnail (videoPath, callback) {
278   const filename = pathUtils.basename(videoPath) + '.jpg'
279   ffmpeg(videoPath)
280     .on('error', callback)
281     .on('end', function () {
282       callback(null, filename)
283     })
284     .thumbnail({
285       count: 1,
286       folder: thumbnailsDir,
287       size: constants.THUMBNAILS_SIZE,
288       filename: filename
289     })
290 }
291
292 function seed (path, callback) {
293   logger.info('Seeding %s...', path)
294
295   webtorrent.seed(path, function (torrent) {
296     logger.info('%s seeded (%s).', path, torrent.magnetURI)
297
298     return callback(null, torrent)
299   })
300 }
301
302 function generateThumbnailFromBase64 (data, callback) {
303   // Creating the thumbnail for this remote video
304   utils.generateRandomString(16, function (err, randomString) {
305     if (err) return callback(err)
306
307     const thumbnailName = randomString + '.jpg'
308     fs.writeFile(thumbnailsDir + thumbnailName, data, { encoding: 'base64' }, function (err) {
309       if (err) return callback(err)
310
311       return callback(null, thumbnailName)
312     })
313   })
314 }