Server: pod removing refractoring
[oweals/peertube.git] / server / models / video.js
1 'use strict'
2
3 const createTorrent = require('create-torrent')
4 const ffmpeg = require('fluent-ffmpeg')
5 const fs = require('fs')
6 const parallel = require('async/parallel')
7 const parseTorrent = require('parse-torrent')
8 const pathUtils = require('path')
9 const magnet = require('magnet-uri')
10 const mongoose = require('mongoose')
11
12 const constants = require('../initializers/constants')
13 const customVideosValidators = require('../helpers/custom-validators').videos
14 const logger = require('../helpers/logger')
15 const modelUtils = require('./utils')
16 const utils = require('../helpers/utils')
17
18 // ---------------------------------------------------------------------------
19
20 // TODO: add indexes on searchable columns
21 const VideoSchema = mongoose.Schema({
22   name: String,
23   filename: String,
24   description: String,
25   magnetUri: String,
26   podUrl: String,
27   author: String,
28   duration: Number,
29   thumbnail: String,
30   tags: [ String ],
31   createdDate: {
32     type: Date,
33     default: Date.now
34   }
35 })
36
37 VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
38 VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
39 VideoSchema.path('magnetUri').validate(customVideosValidators.isVideoMagnetUriValid)
40 VideoSchema.path('podUrl').validate(customVideosValidators.isVideoPodUrlValid)
41 VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
42 VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
43 // The tumbnail can be the path or the data in base 64
44 // The pre save hook will convert the base 64 data in a file on disk and replace the thumbnail key by the filename
45 VideoSchema.path('thumbnail').validate(function (value) {
46   return customVideosValidators.isVideoThumbnailValid(value) || customVideosValidators.isVideoThumbnail64Valid(value)
47 })
48 VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
49
50 VideoSchema.methods = {
51   isOwned,
52   toFormatedJSON,
53   toRemoteJSON
54 }
55
56 VideoSchema.statics = {
57   getDurationFromFile,
58   listForApi,
59   listByUrlAndMagnet,
60   listByUrl,
61   listOwned,
62   listOwnedByAuthor,
63   listRemotes,
64   load,
65   search
66 }
67
68 VideoSchema.pre('remove', function (next) {
69   const video = this
70   const tasks = []
71
72   tasks.push(
73     function (callback) {
74       removeThumbnail(video, callback)
75     }
76   )
77
78   if (video.isOwned()) {
79     tasks.push(
80       function (callback) {
81         removeFile(video, callback)
82       },
83       function (callback) {
84         removeTorrent(video, callback)
85       }
86     )
87   }
88
89   parallel(tasks, next)
90 })
91
92 VideoSchema.pre('save', function (next) {
93   const video = this
94   const tasks = []
95
96   if (video.isOwned()) {
97     const videoPath = pathUtils.join(constants.CONFIG.STORAGE.UPLOAD_DIR, video.filename)
98     this.podUrl = constants.CONFIG.WEBSERVER.URL
99
100     tasks.push(
101       // TODO: refractoring
102       function (callback) {
103         const options = {
104           announceList: [
105             [ constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOST + ':' + constants.CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
106           ],
107           urlList: [
108             constants.CONFIG.WEBSERVER.URL + constants.STATIC_PATHS.WEBSEED + video.filename
109           ]
110         }
111
112         createTorrent(videoPath, options, function (err, torrent) {
113           if (err) return callback(err)
114
115           fs.writeFile(constants.CONFIG.STORAGE.TORRENTS_DIR + video.filename + '.torrent', torrent, function (err) {
116             if (err) return callback(err)
117
118             const parsedTorrent = parseTorrent(torrent)
119             parsedTorrent.xs = video.podUrl + constants.STATIC_PATHS.TORRENTS + video.filename + '.torrent'
120             video.magnetUri = magnet.encode(parsedTorrent)
121
122             callback(null)
123           })
124         })
125       },
126       function (callback) {
127         createThumbnail(videoPath, callback)
128       }
129     )
130
131     parallel(tasks, function (err, results) {
132       if (err) return next(err)
133
134       video.thumbnail = results[1]
135
136       return next()
137     })
138   } else {
139     generateThumbnailFromBase64(video.thumbnail, function (err, thumbnailName) {
140       if (err) return next(err)
141
142       video.thumbnail = thumbnailName
143
144       return next()
145     })
146   }
147 })
148
149 mongoose.model('Video', VideoSchema)
150
151 // ------------------------------ METHODS ------------------------------
152
153 function isOwned () {
154   return this.filename !== null
155 }
156
157 function toFormatedJSON () {
158   const json = {
159     id: this._id,
160     name: this.name,
161     description: this.description,
162     podUrl: this.podUrl.replace(/^https?:\/\//, ''),
163     isLocal: this.isOwned(),
164     magnetUri: this.magnetUri,
165     author: this.author,
166     duration: this.duration,
167     tags: this.tags,
168     thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.thumbnail,
169     createdDate: this.createdDate
170   }
171
172   return json
173 }
174
175 function toRemoteJSON (callback) {
176   const self = this
177
178   // Convert thumbnail to base64
179   fs.readFile(pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.thumbnail), function (err, thumbnailData) {
180     if (err) {
181       logger.error('Cannot read the thumbnail of the video')
182       return callback(err)
183     }
184
185     const remoteVideo = {
186       name: self.name,
187       description: self.description,
188       magnetUri: self.magnetUri,
189       filename: null,
190       author: self.author,
191       duration: self.duration,
192       thumbnailBase64: new Buffer(thumbnailData).toString('base64'),
193       tags: self.tags,
194       createdDate: self.createdDate,
195       podUrl: self.podUrl
196     }
197
198     return callback(null, remoteVideo)
199   })
200 }
201
202 // ------------------------------ STATICS ------------------------------
203
204 function getDurationFromFile (videoPath, callback) {
205   ffmpeg.ffprobe(videoPath, function (err, metadata) {
206     if (err) return callback(err)
207
208     return callback(null, Math.floor(metadata.format.duration))
209   })
210 }
211
212 function listForApi (start, count, sort, callback) {
213   const query = {}
214   return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
215 }
216
217 function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
218   this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
219 }
220
221 function listByUrl (fromUrl, callback) {
222   this.find({ podUrl: fromUrl }, callback)
223 }
224
225 function listOwned (callback) {
226   // If filename is not null this is *our* video
227   this.find({ filename: { $ne: null } }, callback)
228 }
229
230 function listOwnedByAuthor (author, callback) {
231   this.find({ filename: { $ne: null }, author: author }, callback)
232 }
233
234 function listRemotes (callback) {
235   this.find({ filename: null }, callback)
236 }
237
238 function load (id, callback) {
239   this.findById(id, callback)
240 }
241
242 function search (value, field, start, count, sort, callback) {
243   const query = {}
244   // Make an exact search with the magnet
245   if (field === 'magnetUri' || field === 'tags') {
246     query[field] = value
247   } else {
248     query[field] = new RegExp(value)
249   }
250
251   modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback)
252 }
253
254 // ---------------------------------------------------------------------------
255
256 function removeThumbnail (video, callback) {
257   fs.unlink(constants.CONFIG.STORAGE.THUMBNAILS_DIR + video.thumbnail, callback)
258 }
259
260 function removeFile (video, callback) {
261   fs.unlink(constants.CONFIG.STORAGE.UPLOAD_DIR + video.filename, callback)
262 }
263
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(constants.CONFIG.STORAGE.TORRENTS_DIR + video.filename + '.torrent', callback)
267 }
268
269 function createThumbnail (videoPath, callback) {
270   const filename = pathUtils.basename(videoPath) + '.jpg'
271   ffmpeg(videoPath)
272     .on('error', callback)
273     .on('end', function () {
274       callback(null, filename)
275     })
276     .thumbnail({
277       count: 1,
278       folder: constants.CONFIG.STORAGE.THUMBNAILS_DIR,
279       size: constants.THUMBNAILS_SIZE,
280       filename: filename
281     })
282 }
283
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)
288
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)
292
293       return callback(null, thumbnailName)
294     })
295   })
296 }