aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video.js')
-rw-r--r--server/models/video.js314
1 files changed, 314 insertions, 0 deletions
diff --git a/server/models/video.js b/server/models/video.js
new file mode 100644
index 000000000..8b14e9b35
--- /dev/null
+++ b/server/models/video.js
@@ -0,0 +1,314 @@
1'use strict'
2
3const async = require('async')
4const config = require('config')
5const ffmpeg = require('fluent-ffmpeg')
6const fs = require('fs')
7const pathUtils = require('path')
8const mongoose = require('mongoose')
9
10const constants = require('../initializers/constants')
11const customValidators = require('../helpers/customValidators')
12const logger = require('../helpers/logger')
13const utils = require('../helpers/utils')
14const webtorrent = require('../lib/webtorrent')
15
16const http = config.get('webserver.https') === true ? 'https' : 'http'
17const host = config.get('webserver.host')
18const port = config.get('webserver.port')
19const uploadsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.uploads'))
20const thumbnailsDir = pathUtils.join(__dirname, '..', '..', config.get('storage.thumbnails'))
21
22// ---------------------------------------------------------------------------
23
24// TODO: add indexes on searchable columns
25const 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
41VideoSchema.path('name').validate(customValidators.isVideoNameValid)
42VideoSchema.path('description').validate(customValidators.isVideoDescriptionValid)
43VideoSchema.path('magnetUri').validate(customValidators.isVideoMagnetUriValid)
44VideoSchema.path('podUrl').validate(customValidators.isVideoPodUrlValid)
45VideoSchema.path('author').validate(customValidators.isVideoAuthorValid)
46VideoSchema.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
49VideoSchema.path('thumbnail').validate(function (value) {
50 return customValidators.isVideoThumbnailValid(value) || customValidators.isVideoThumbnail64Valid(value)
51})
52VideoSchema.path('tags').validate(customValidators.isVideoTagsValid)
53
54VideoSchema.methods = {
55 isOwned: isOwned,
56 toFormatedJSON: toFormatedJSON,
57 toRemoteJSON: toRemoteJSON
58}
59
60VideoSchema.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
72VideoSchema.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
96VideoSchema.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
132mongoose.model('Video', VideoSchema)
133
134// ------------------------------ METHODS ------------------------------
135
136function isOwned () {
137 return this.namePath !== null
138}
139
140function 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
158function 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
187function 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
195function list (start, count, sort, callback) {
196 const query = {}
197 return findWithCount.call(this, query, start, count, sort, callback)
198}
199
200function listByUrlAndMagnet (fromUrl, magnetUri, callback) {
201 this.find({ podUrl: fromUrl, magnetUri: magnetUri }, callback)
202}
203
204function listByUrls (fromUrls, callback) {
205 this.find({ podUrl: { $in: fromUrls } }, callback)
206}
207
208function listOwned (callback) {
209 // If namePath is not null this is *our* video
210 this.find({ namePath: { $ne: null } }, callback)
211}
212
213function listRemotes (callback) {
214 this.find({ namePath: null }, callback)
215}
216
217function load (id, callback) {
218 this.findById(id, callback)
219}
220
221function 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
234function seedAllExisting () {
235
236}
237
238// ---------------------------------------------------------------------------
239
240function 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
259function removeThumbnail (video, callback) {
260 fs.unlink(thumbnailsDir + video.thumbnail, callback)
261}
262
263function 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
268function 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
277function 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
292function 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
302function 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}