diff options
Diffstat (limited to 'server/models/video.js')
-rw-r--r-- | server/models/video.js | 388 |
1 files changed, 270 insertions, 118 deletions
diff --git a/server/models/video.js b/server/models/video.js index 330067cdf..8ef07c9e6 100644 --- a/server/models/video.js +++ b/server/models/video.js | |||
@@ -7,102 +7,93 @@ const magnetUtil = require('magnet-uri') | |||
7 | const parallel = require('async/parallel') | 7 | const parallel = require('async/parallel') |
8 | const parseTorrent = require('parse-torrent') | 8 | const parseTorrent = require('parse-torrent') |
9 | const pathUtils = require('path') | 9 | const pathUtils = require('path') |
10 | const mongoose = require('mongoose') | ||
11 | 10 | ||
12 | const constants = require('../initializers/constants') | 11 | const constants = require('../initializers/constants') |
13 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
14 | const logger = require('../helpers/logger') | 12 | const logger = require('../helpers/logger') |
15 | const modelUtils = require('./utils') | 13 | const modelUtils = require('./utils') |
16 | 14 | ||
17 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
18 | 16 | ||
17 | module.exports = function (sequelize, DataTypes) { | ||
19 | // TODO: add indexes on searchable columns | 18 | // TODO: add indexes on searchable columns |
20 | const VideoSchema = mongoose.Schema({ | 19 | const Video = sequelize.define('Video', |
21 | name: String, | 20 | { |
22 | extname: { | 21 | id: { |
23 | type: String, | 22 | type: DataTypes.UUID, |
24 | enum: [ '.mp4', '.webm', '.ogv' ] | 23 | defaultValue: DataTypes.UUIDV4, |
25 | }, | 24 | primaryKey: true |
26 | remoteId: mongoose.Schema.Types.ObjectId, | ||
27 | description: String, | ||
28 | magnet: { | ||
29 | infoHash: String | ||
30 | }, | ||
31 | podHost: String, | ||
32 | author: String, | ||
33 | duration: Number, | ||
34 | tags: [ String ], | ||
35 | createdDate: { | ||
36 | type: Date, | ||
37 | default: Date.now | ||
38 | } | ||
39 | }) | ||
40 | |||
41 | VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) | ||
42 | VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) | ||
43 | VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid) | ||
44 | VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) | ||
45 | VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) | ||
46 | VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) | ||
47 | |||
48 | VideoSchema.methods = { | ||
49 | generateMagnetUri, | ||
50 | getVideoFilename, | ||
51 | getThumbnailName, | ||
52 | getPreviewName, | ||
53 | getTorrentName, | ||
54 | isOwned, | ||
55 | toFormatedJSON, | ||
56 | toRemoteJSON | ||
57 | } | ||
58 | |||
59 | VideoSchema.statics = { | ||
60 | generateThumbnailFromBase64, | ||
61 | getDurationFromFile, | ||
62 | listForApi, | ||
63 | listByHostAndRemoteId, | ||
64 | listByHost, | ||
65 | listOwned, | ||
66 | listOwnedByAuthor, | ||
67 | listRemotes, | ||
68 | load, | ||
69 | search | ||
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 | }, | 25 | }, |
87 | function (callback) { | 26 | name: { |
88 | removeTorrent(video, callback) | 27 | type: DataTypes.STRING |
89 | }, | 28 | }, |
90 | function (callback) { | 29 | extname: { |
91 | removePreview(video, callback) | 30 | // TODO: enum? |
31 | type: DataTypes.STRING | ||
32 | }, | ||
33 | remoteId: { | ||
34 | type: DataTypes.UUID | ||
35 | }, | ||
36 | description: { | ||
37 | type: DataTypes.STRING | ||
38 | }, | ||
39 | infoHash: { | ||
40 | type: DataTypes.STRING | ||
41 | }, | ||
42 | duration: { | ||
43 | type: DataTypes.INTEGER | ||
44 | }, | ||
45 | tags: { | ||
46 | type: DataTypes.ARRAY(DataTypes.STRING) | ||
92 | } | 47 | } |
93 | ) | 48 | }, |
94 | } | 49 | { |
50 | classMethods: { | ||
51 | associate, | ||
52 | |||
53 | generateThumbnailFromBase64, | ||
54 | getDurationFromFile, | ||
55 | listForApi, | ||
56 | listByHostAndRemoteId, | ||
57 | listOwnedAndPopulateAuthor, | ||
58 | listOwnedByAuthor, | ||
59 | load, | ||
60 | loadAndPopulateAuthor, | ||
61 | loadAndPopulateAuthorAndPod, | ||
62 | searchAndPopulateAuthorAndPod | ||
63 | }, | ||
64 | instanceMethods: { | ||
65 | generateMagnetUri, | ||
66 | getVideoFilename, | ||
67 | getThumbnailName, | ||
68 | getPreviewName, | ||
69 | getTorrentName, | ||
70 | isOwned, | ||
71 | toFormatedJSON, | ||
72 | toRemoteJSON | ||
73 | }, | ||
74 | hooks: { | ||
75 | beforeCreate, | ||
76 | afterDestroy | ||
77 | } | ||
78 | } | ||
79 | ) | ||
95 | 80 | ||
96 | parallel(tasks, next) | 81 | return Video |
97 | }) | 82 | } |
98 | 83 | ||
99 | VideoSchema.pre('save', function (next) { | 84 | // TODO: Validation |
100 | const video = this | 85 | // VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid) |
86 | // VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid) | ||
87 | // VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid) | ||
88 | // VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid) | ||
89 | // VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid) | ||
90 | // VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid) | ||
91 | |||
92 | function beforeCreate (video, options, next) { | ||
101 | const tasks = [] | 93 | const tasks = [] |
102 | 94 | ||
103 | if (video.isOwned()) { | 95 | if (video.isOwned()) { |
104 | const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | 96 | const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) |
105 | this.podHost = constants.CONFIG.WEBSERVER.HOST | ||
106 | 97 | ||
107 | tasks.push( | 98 | tasks.push( |
108 | // TODO: refractoring | 99 | // TODO: refractoring |
@@ -123,7 +114,7 @@ VideoSchema.pre('save', function (next) { | |||
123 | if (err) return callback(err) | 114 | if (err) return callback(err) |
124 | 115 | ||
125 | const parsedTorrent = parseTorrent(torrent) | 116 | const parsedTorrent = parseTorrent(torrent) |
126 | video.magnet.infoHash = parsedTorrent.infoHash | 117 | video.infoHash = parsedTorrent.infoHash |
127 | 118 | ||
128 | callback(null) | 119 | callback(null) |
129 | }) | 120 | }) |
@@ -141,12 +132,46 @@ VideoSchema.pre('save', function (next) { | |||
141 | } | 132 | } |
142 | 133 | ||
143 | return next() | 134 | return next() |
144 | }) | 135 | } |
136 | |||
137 | function afterDestroy (video, options, next) { | ||
138 | const tasks = [] | ||
145 | 139 | ||
146 | mongoose.model('Video', VideoSchema) | 140 | tasks.push( |
141 | function (callback) { | ||
142 | removeThumbnail(video, callback) | ||
143 | } | ||
144 | ) | ||
145 | |||
146 | if (video.isOwned()) { | ||
147 | tasks.push( | ||
148 | function (callback) { | ||
149 | removeFile(video, callback) | ||
150 | }, | ||
151 | function (callback) { | ||
152 | removeTorrent(video, callback) | ||
153 | }, | ||
154 | function (callback) { | ||
155 | removePreview(video, callback) | ||
156 | } | ||
157 | ) | ||
158 | } | ||
159 | |||
160 | parallel(tasks, next) | ||
161 | } | ||
147 | 162 | ||
148 | // ------------------------------ METHODS ------------------------------ | 163 | // ------------------------------ METHODS ------------------------------ |
149 | 164 | ||
165 | function associate (models) { | ||
166 | this.belongsTo(models.Author, { | ||
167 | foreignKey: { | ||
168 | name: 'authorId', | ||
169 | allowNull: false | ||
170 | }, | ||
171 | onDelete: 'cascade' | ||
172 | }) | ||
173 | } | ||
174 | |||
150 | function generateMagnetUri () { | 175 | function generateMagnetUri () { |
151 | let baseUrlHttp, baseUrlWs | 176 | let baseUrlHttp, baseUrlWs |
152 | 177 | ||
@@ -154,8 +179,8 @@ function generateMagnetUri () { | |||
154 | baseUrlHttp = constants.CONFIG.WEBSERVER.URL | 179 | baseUrlHttp = constants.CONFIG.WEBSERVER.URL |
155 | baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT | 180 | baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT |
156 | } else { | 181 | } else { |
157 | baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.podHost | 182 | baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host |
158 | baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.podHost | 183 | baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.Author.Pod.host |
159 | } | 184 | } |
160 | 185 | ||
161 | const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() | 186 | const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() |
@@ -166,7 +191,7 @@ function generateMagnetUri () { | |||
166 | xs, | 191 | xs, |
167 | announce, | 192 | announce, |
168 | urlList, | 193 | urlList, |
169 | infoHash: this.magnet.infoHash, | 194 | infoHash: this.infoHash, |
170 | name: this.name | 195 | name: this.name |
171 | } | 196 | } |
172 | 197 | ||
@@ -174,20 +199,20 @@ function generateMagnetUri () { | |||
174 | } | 199 | } |
175 | 200 | ||
176 | function getVideoFilename () { | 201 | function getVideoFilename () { |
177 | if (this.isOwned()) return this._id + this.extname | 202 | if (this.isOwned()) return this.id + this.extname |
178 | 203 | ||
179 | return this.remoteId + this.extname | 204 | return this.remoteId + this.extname |
180 | } | 205 | } |
181 | 206 | ||
182 | function getThumbnailName () { | 207 | function getThumbnailName () { |
183 | // We always have a copy of the thumbnail | 208 | // We always have a copy of the thumbnail |
184 | return this._id + '.jpg' | 209 | return this.id + '.jpg' |
185 | } | 210 | } |
186 | 211 | ||
187 | function getPreviewName () { | 212 | function getPreviewName () { |
188 | const extension = '.jpg' | 213 | const extension = '.jpg' |
189 | 214 | ||
190 | if (this.isOwned()) return this._id + extension | 215 | if (this.isOwned()) return this.id + extension |
191 | 216 | ||
192 | return this.remoteId + extension | 217 | return this.remoteId + extension |
193 | } | 218 | } |
@@ -195,7 +220,7 @@ function getPreviewName () { | |||
195 | function getTorrentName () { | 220 | function getTorrentName () { |
196 | const extension = '.torrent' | 221 | const extension = '.torrent' |
197 | 222 | ||
198 | if (this.isOwned()) return this._id + extension | 223 | if (this.isOwned()) return this.id + extension |
199 | 224 | ||
200 | return this.remoteId + extension | 225 | return this.remoteId + extension |
201 | } | 226 | } |
@@ -205,18 +230,27 @@ function isOwned () { | |||
205 | } | 230 | } |
206 | 231 | ||
207 | function toFormatedJSON () { | 232 | function toFormatedJSON () { |
233 | let podHost | ||
234 | |||
235 | if (this.Author.Pod) { | ||
236 | podHost = this.Author.Pod.host | ||
237 | } else { | ||
238 | // It means it's our video | ||
239 | podHost = constants.CONFIG.WEBSERVER.HOST | ||
240 | } | ||
241 | |||
208 | const json = { | 242 | const json = { |
209 | id: this._id, | 243 | id: this.id, |
210 | name: this.name, | 244 | name: this.name, |
211 | description: this.description, | 245 | description: this.description, |
212 | podHost: this.podHost, | 246 | podHost, |
213 | isLocal: this.isOwned(), | 247 | isLocal: this.isOwned(), |
214 | magnetUri: this.generateMagnetUri(), | 248 | magnetUri: this.generateMagnetUri(), |
215 | author: this.author, | 249 | author: this.Author.name, |
216 | duration: this.duration, | 250 | duration: this.duration, |
217 | tags: this.tags, | 251 | tags: this.tags, |
218 | thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), | 252 | thumbnailPath: constants.STATIC_PATHS.THUMBNAILS + '/' + this.getThumbnailName(), |
219 | createdDate: this.createdDate | 253 | createdAt: this.createdAt |
220 | } | 254 | } |
221 | 255 | ||
222 | return json | 256 | return json |
@@ -236,13 +270,13 @@ function toRemoteJSON (callback) { | |||
236 | const remoteVideo = { | 270 | const remoteVideo = { |
237 | name: self.name, | 271 | name: self.name, |
238 | description: self.description, | 272 | description: self.description, |
239 | magnet: self.magnet, | 273 | infoHash: self.infoHash, |
240 | remoteId: self._id, | 274 | remoteId: self.id, |
241 | author: self.author, | 275 | author: self.Author.name, |
242 | duration: self.duration, | 276 | duration: self.duration, |
243 | thumbnailBase64: new Buffer(thumbnailData).toString('base64'), | 277 | thumbnailBase64: new Buffer(thumbnailData).toString('base64'), |
244 | tags: self.tags, | 278 | tags: self.tags, |
245 | createdDate: self.createdDate, | 279 | createdAt: self.createdAt, |
246 | extname: self.extname | 280 | extname: self.extname |
247 | } | 281 | } |
248 | 282 | ||
@@ -273,50 +307,168 @@ function getDurationFromFile (videoPath, callback) { | |||
273 | } | 307 | } |
274 | 308 | ||
275 | function listForApi (start, count, sort, callback) { | 309 | function listForApi (start, count, sort, callback) { |
276 | const query = {} | 310 | const query = { |
277 | return modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) | 311 | offset: start, |
312 | limit: count, | ||
313 | order: [ modelUtils.getSort(sort) ], | ||
314 | include: [ | ||
315 | { | ||
316 | model: this.sequelize.models.Author, | ||
317 | include: [ this.sequelize.models.Pod ] | ||
318 | } | ||
319 | ] | ||
320 | } | ||
321 | |||
322 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
323 | if (err) return callback(err) | ||
324 | |||
325 | return callback(null, result.rows, result.count) | ||
326 | }) | ||
278 | } | 327 | } |
279 | 328 | ||
280 | function listByHostAndRemoteId (fromHost, remoteId, callback) { | 329 | function listByHostAndRemoteId (fromHost, remoteId, callback) { |
281 | this.find({ podHost: fromHost, remoteId: remoteId }, callback) | 330 | const query = { |
282 | } | 331 | where: { |
332 | remoteId: remoteId | ||
333 | }, | ||
334 | include: [ | ||
335 | { | ||
336 | model: this.sequelize.models.Author, | ||
337 | include: [ | ||
338 | { | ||
339 | model: this.sequelize.models.Pod, | ||
340 | where: { | ||
341 | host: fromHost | ||
342 | } | ||
343 | } | ||
344 | ] | ||
345 | } | ||
346 | ] | ||
347 | } | ||
283 | 348 | ||
284 | function listByHost (fromHost, callback) { | 349 | return this.findAll(query).asCallback(callback) |
285 | this.find({ podHost: fromHost }, callback) | ||
286 | } | 350 | } |
287 | 351 | ||
288 | function listOwned (callback) { | 352 | function listOwnedAndPopulateAuthor (callback) { |
289 | // If remoteId is null this is *our* video | 353 | // If remoteId is null this is *our* video |
290 | this.find({ remoteId: null }, callback) | 354 | const query = { |
355 | where: { | ||
356 | remoteId: null | ||
357 | }, | ||
358 | include: [ this.sequelize.models.Author ] | ||
359 | } | ||
360 | |||
361 | return this.findAll(query).asCallback(callback) | ||
291 | } | 362 | } |
292 | 363 | ||
293 | function listOwnedByAuthor (author, callback) { | 364 | function listOwnedByAuthor (author, callback) { |
294 | this.find({ remoteId: null, author: author }, callback) | 365 | const query = { |
295 | } | 366 | where: { |
367 | remoteId: null | ||
368 | }, | ||
369 | include: [ | ||
370 | { | ||
371 | model: this.sequelize.models.Author, | ||
372 | where: { | ||
373 | name: author | ||
374 | } | ||
375 | } | ||
376 | ] | ||
377 | } | ||
296 | 378 | ||
297 | function listRemotes (callback) { | 379 | return this.findAll(query).asCallback(callback) |
298 | this.find({ remoteId: { $ne: null } }, callback) | ||
299 | } | 380 | } |
300 | 381 | ||
301 | function load (id, callback) { | 382 | function load (id, callback) { |
302 | this.findById(id, callback) | 383 | return this.findById(id).asCallback(callback) |
303 | } | 384 | } |
304 | 385 | ||
305 | function search (value, field, start, count, sort, callback) { | 386 | function loadAndPopulateAuthor (id, callback) { |
306 | const query = {} | 387 | const options = { |
388 | include: [ this.sequelize.models.Author ] | ||
389 | } | ||
390 | |||
391 | return this.findById(id, options).asCallback(callback) | ||
392 | } | ||
393 | |||
394 | function loadAndPopulateAuthorAndPod (id, callback) { | ||
395 | const options = { | ||
396 | include: [ | ||
397 | { | ||
398 | model: this.sequelize.models.Author, | ||
399 | include: [ this.sequelize.models.Pod ] | ||
400 | } | ||
401 | ] | ||
402 | } | ||
403 | |||
404 | return this.findById(id, options).asCallback(callback) | ||
405 | } | ||
406 | |||
407 | function searchAndPopulateAuthorAndPod (value, field, start, count, sort, callback) { | ||
408 | const podInclude = { | ||
409 | model: this.sequelize.models.Pod | ||
410 | } | ||
411 | const authorInclude = { | ||
412 | model: this.sequelize.models.Author, | ||
413 | include: [ | ||
414 | podInclude | ||
415 | ] | ||
416 | } | ||
417 | |||
418 | const query = { | ||
419 | where: {}, | ||
420 | include: [ | ||
421 | authorInclude | ||
422 | ], | ||
423 | offset: start, | ||
424 | limit: count, | ||
425 | order: [ modelUtils.getSort(sort) ] | ||
426 | } | ||
427 | |||
428 | // TODO: include our pod for podHost searches (we are not stored in the database) | ||
307 | // Make an exact search with the magnet | 429 | // Make an exact search with the magnet |
308 | if (field === 'magnetUri') { | 430 | if (field === 'magnetUri') { |
309 | const infoHash = magnetUtil.decode(value).infoHash | 431 | const infoHash = magnetUtil.decode(value).infoHash |
310 | query.magnet = { | 432 | query.where.infoHash = infoHash |
311 | infoHash | ||
312 | } | ||
313 | } else if (field === 'tags') { | 433 | } else if (field === 'tags') { |
314 | query[field] = value | 434 | query.where[field] = value |
435 | } else if (field === 'host') { | ||
436 | const whereQuery = { | ||
437 | '$Author.Pod.host$': { | ||
438 | $like: '%' + value + '%' | ||
439 | } | ||
440 | } | ||
441 | |||
442 | // Include our pod? (not stored in the database) | ||
443 | if (constants.CONFIG.WEBSERVER.HOST.indexOf(value) !== -1) { | ||
444 | query.where = { | ||
445 | $or: [ | ||
446 | whereQuery, | ||
447 | { | ||
448 | remoteId: null | ||
449 | } | ||
450 | ] | ||
451 | } | ||
452 | } else { | ||
453 | query.where = whereQuery | ||
454 | } | ||
455 | } else if (field === 'author') { | ||
456 | query.where = { | ||
457 | '$Author.name$': { | ||
458 | $like: '%' + value + '%' | ||
459 | } | ||
460 | } | ||
315 | } else { | 461 | } else { |
316 | query[field] = new RegExp(value, 'i') | 462 | query.where[field] = { |
463 | $like: '%' + value + '%' | ||
464 | } | ||
317 | } | 465 | } |
318 | 466 | ||
319 | modelUtils.listForApiWithCount.call(this, query, start, count, sort, callback) | 467 | return this.findAndCountAll(query).asCallback(function (err, result) { |
468 | if (err) return callback(err) | ||
469 | |||
470 | return callback(null, result.rows, result.count) | ||
471 | }) | ||
320 | } | 472 | } |
321 | 473 | ||
322 | // --------------------------------------------------------------------------- | 474 | // --------------------------------------------------------------------------- |