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.js388
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')
7const parallel = require('async/parallel') 7const parallel = require('async/parallel')
8const parseTorrent = require('parse-torrent') 8const parseTorrent = require('parse-torrent')
9const pathUtils = require('path') 9const pathUtils = require('path')
10const mongoose = require('mongoose')
11 10
12const constants = require('../initializers/constants') 11const constants = require('../initializers/constants')
13const customVideosValidators = require('../helpers/custom-validators').videos
14const logger = require('../helpers/logger') 12const logger = require('../helpers/logger')
15const modelUtils = require('./utils') 13const modelUtils = require('./utils')
16 14
17// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
18 16
17module.exports = function (sequelize, DataTypes) {
19// TODO: add indexes on searchable columns 18// TODO: add indexes on searchable columns
20const 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
41VideoSchema.path('name').validate(customVideosValidators.isVideoNameValid)
42VideoSchema.path('description').validate(customVideosValidators.isVideoDescriptionValid)
43VideoSchema.path('podHost').validate(customVideosValidators.isVideoPodHostValid)
44VideoSchema.path('author').validate(customVideosValidators.isVideoAuthorValid)
45VideoSchema.path('duration').validate(customVideosValidators.isVideoDurationValid)
46VideoSchema.path('tags').validate(customVideosValidators.isVideoTagsValid)
47
48VideoSchema.methods = {
49 generateMagnetUri,
50 getVideoFilename,
51 getThumbnailName,
52 getPreviewName,
53 getTorrentName,
54 isOwned,
55 toFormatedJSON,
56 toRemoteJSON
57}
58
59VideoSchema.statics = {
60 generateThumbnailFromBase64,
61 getDurationFromFile,
62 listForApi,
63 listByHostAndRemoteId,
64 listByHost,
65 listOwned,
66 listOwnedByAuthor,
67 listRemotes,
68 load,
69 search
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 }, 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
99VideoSchema.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
92function 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
137function afterDestroy (video, options, next) {
138 const tasks = []
145 139
146mongoose.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
165function associate (models) {
166 this.belongsTo(models.Author, {
167 foreignKey: {
168 name: 'authorId',
169 allowNull: false
170 },
171 onDelete: 'cascade'
172 })
173}
174
150function generateMagnetUri () { 175function 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
176function getVideoFilename () { 201function 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
182function getThumbnailName () { 207function 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
187function getPreviewName () { 212function 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 () {
195function getTorrentName () { 220function 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
207function toFormatedJSON () { 232function 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
275function listForApi (start, count, sort, callback) { 309function 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
280function listByHostAndRemoteId (fromHost, remoteId, callback) { 329function 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
284function listByHost (fromHost, callback) { 349 return this.findAll(query).asCallback(callback)
285 this.find({ podHost: fromHost }, callback)
286} 350}
287 351
288function listOwned (callback) { 352function 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
293function listOwnedByAuthor (author, callback) { 364function 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
297function listRemotes (callback) { 379 return this.findAll(query).asCallback(callback)
298 this.find({ remoteId: { $ne: null } }, callback)
299} 380}
300 381
301function load (id, callback) { 382function load (id, callback) {
302 this.findById(id, callback) 383 return this.findById(id).asCallback(callback)
303} 384}
304 385
305function search (value, field, start, count, sort, callback) { 386function 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
394function 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
407function 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// ---------------------------------------------------------------------------